Совместное использование ресурсов с помощью Spring Безопасность
Я пытаюсь сделать CORS хорошо с Spring Security, но это не соответствует. Я внес изменения, описанные в этой статье, и изменение этой строки в applicationContext-security.xml
получило запросы POST и GET для моего приложения (временно предоставляет методы контроллера, поэтому я могу проверить CORS):
- До:
<intercept-url pattern="/**" access="isAuthenticated()" />
- После:
<intercept-url pattern="/**" access="permitAll" />
К сожалению, следующий URL-адрес, который позволяет Spring входам в систему безопасности через AJAX, не отвечает: http://localhost:8080/mutopia-server/resources/j_spring_security_check
. Я делаю запрос AJAX от http://localhost:80
до http://localhost:8080
.
В Chrome
При попытке доступа к j_spring_security_check
я получаю (pending)
в Chrome для запроса предварительной проверки OPTIONS, а вызов AJAX возвращается с кодом состояния HTTP 0 и сообщением "ошибка".
В Firefox
Предполетный код преуспевает с кодом состояния HTTP 302, и я все же получаю обратный вызов ошибки для моего запроса AJAX непосредственно после HTTP-статуса 0 и сообщения "ошибка".
![enter image description here]()
![enter image description here]()
Код запроса AJAX
function get(url, json) {
var args = {
type: 'GET',
url: url,
// async: false,
// crossDomain: true,
xhrFields: {
withCredentials: false
},
success: function(response) {
console.debug(url, response);
},
error: function(xhr) {
console.error(url, xhr.status, xhr.statusText);
}
};
if (json) {
args.contentType = 'application/json'
}
$.ajax(args);
}
function post(url, json, data, dataEncode) {
var args = {
type: 'POST',
url: url,
// async: false,
crossDomain: true,
xhrFields: {
withCredentials: false
},
beforeSend: function(xhr){
// This is always added by default
// Ignoring this prevents preflight - but expects browser to follow 302 location change
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader("X-Ajax-call", "true");
},
success: function(data, textStatus, xhr) {
// var location = xhr.getResponseHeader('Location');
console.error('success', url, xhr.getAllResponseHeaders());
},
error: function(xhr) {
console.error(url, xhr.status, xhr.statusText);
console.error('fail', url, xhr.getAllResponseHeaders());
}
}
if (json) {
args.contentType = 'application/json'
}
if (typeof data != 'undefined') {
// Send JSON raw in the body
args.data = dataEncode ? JSON.stringify(data) : data;
}
console.debug('args', args);
$.ajax(args);
}
var loginJSON = {"j_username": "username", "j_password": "password"};
// Fails
post('http://localhost:8080/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);
// Works
post('http://localhost/mutopia-server/resources/j_spring_security_check', false, loginJSON, false);
// Works
get('http://localhost:8080/mutopia-server/landuses?projectId=6', true);
// Works
post('http://localhost:8080/mutopia-server/params', true, {
"name": "testing",
"local": false,
"generated": false,
"project": 6
}, true);
Обратите внимание: я могу отправить POST на любой другой URL-адрес в моем приложении через CORS, кроме Spring Security login. Я просмотрел множество статей, поэтому любое понимание этой странной проблемы было бы весьма полезно:)
Ответы
Ответ 1
Я смог сделать это, расширив UsernamePasswordAuthenticationFilter... мой код находится в Groovy, надеюсь, что ОК:
public class CorsAwareAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
static final String ORIGIN = 'Origin'
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
if (request.getHeader(ORIGIN)) {
String origin = request.getHeader(ORIGIN)
response.addHeader('Access-Control-Allow-Origin', origin)
response.addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
response.addHeader('Access-Control-Allow-Credentials', 'true')
response.addHeader('Access-Control-Allow-Headers',
request.getHeader('Access-Control-Request-Headers'))
}
if (request.method == 'OPTIONS') {
response.writer.print('OK')
response.writer.flush()
return
}
return super.attemptAuthentication(request, response)
}
}
Важные биты выше:
- Добавляйте только заголовки CORS для ответа, если обнаружен запрос CORS
- Отвечайте на запрос перед полетом OPTIONS с простым непустым ответом 200, который также содержит заголовки CORS.
Вам нужно объявить этот bean в конфигурации Spring. Есть много статей, показывающих, как это сделать, поэтому я не буду копировать это здесь.
В моей собственной реализации я использую белый список доменов, поскольку я разрешаю только CORS для внутреннего доступа разработчика. Вышеупомянутая упрощенная версия того, что я делаю, может нуждаться в настройке, но это должно дать вам общую идею.
Ответ 2
well Это мой код работает очень хорошо и идеально подходит для меня: я провел два дня, работая над этим и понимая безопасность spring, поэтому, надеюсь, вы согласитесь на это как на ответ, lol
public class CorsFilter extends OncePerRequestFilter {
static final String ORIGIN = "Origin";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println(request.getHeader(ORIGIN));
System.out.println(request.getMethod());
if (request.getHeader(ORIGIN).equals("null")) {
String origin = request.getHeader(ORIGIN);
response.setHeader("Access-Control-Allow-Origin", "*");//* or origin as u prefer
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers",
request.getHeader("Access-Control-Request-Headers"));
}
if (request.getMethod().equals("OPTIONS")) {
try {
response.getWriter().print("OK");
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
}else{
filterChain.doFilter(request, response);
}
}
}
ну, тогда вам также нужно настроить ваш фильтр:
<security:http use-expressions="true" .... >
...
//your other configs
<security:custom-filter ref="corsHandler" after="PRE_AUTH_FILTER"/> // this goes to your filter
</security:http>
Ну и вам нужен bean для созданного вами настраиваемого фильтра:
<bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />
Ответ 3
Поскольку Spring Security 4.1, это правильный способ сделать Spring поддержку безопасности CORS (также необходимо в Spring Boot 1.4/1.5):
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
}
}
и
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.csrf().disable();
http.cors();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(ImmutableList.of("*"));
configuration.setAllowedMethods(ImmutableList.of("HEAD",
"GET", "POST", "PUT", "DELETE", "PATCH"));
// setAllowCredentials(true) is important, otherwise:
// The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request credentials mode is 'include'.
configuration.setAllowCredentials(true);
// setAllowedHeaders is important! Without it, OPTIONS preflight request
// will fail with 403 Invalid CORS request
configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Не делайте ни одного из приведенных ниже действий, которые являются неправильным способом решения проблемы:
-
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
-
web.ignoring().antMatchers(HttpMethod.OPTIONS);
Ссылка: http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html
Ответ 4
В основном запрос OPTIONS не содержит cookie для аутентификации безопасности spring.
Чтобы разрешить это, можно изменить конфигурацию безопасности spring, чтобы разрешить ОПЦИИ запрос без проверки подлинности.
Я много разбираюсь и получаю два решения:
1. Использование конфигурации Java с конфигурацией безопасности spring,
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/path/to/allow").permitAll()//allow CORS option calls
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
2. Использование XML ( note. не может писать "POST, GET" ):
<http auto-config="true">
<intercept-url pattern="/client/edit" access="isAuthenticated" method="GET" />
<intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="POST" />
<intercept-url pattern="/client/edit" access="hasRole('EDITOR')" method="GET" />
</http>
В конце есть источник для решения...:)
Ответ 5
В моем случае response.getWriter(). flush() не работал
Изменен код, как показано ниже, и он начал работать
public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
LOGGER.info("Start API::CORSFilter");
HttpServletRequest oRequest = (HttpServletRequest) request;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST,PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
" Origin, X-Requested-With, Content-Type, Accept,AUTH-TOKEN");
if (oRequest.getMethod().equals("OPTIONS")) {
response.flushBuffer();
} else {
chain.doFilter(request, response);
}
}
Ответ 6
Для меня проблема заключалась в том, что проверка preflight OPTIONS
не прошла проверку подлинности, поскольку учетные данные не были переданы этому вызову.
Это работает для меня:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@EnableAsync
@EnableScheduling
@EnableSpringDataWebSupport
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().and()
.authorizeRequests()
.anyRequest().authenticated()
.and().anonymous().disable()
.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
if(HttpMethod.OPTIONS.matches(request.getMethod())){
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}else{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
});
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
Соответствующая часть:
.exceptionHandling().authenticationEntryPoint(new BasicAuthenticationEntryPoint() {
@Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
if(HttpMethod.OPTIONS.matches(request.getMethod())){
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HttpHeaders.ORIGIN));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}else{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
});
Это исправляет проблему перед отправкой OPTIONS
. Что происходит, когда вы получаете вызов и аутентификация терпит неудачу, вы проверяете, является ли это вызовом OPTIONS
, и если это так, просто дайте ему пройти и пусть он делает все, что он хочет сделать. Это по существу отключает проверку передних полетов на стороне браузера, но нормальная политика crossdomain по-прежнему применяется.
Когда вы используете последнюю версию Spring, вы можете использовать приведенный ниже код, чтобы разрешать запросы на кросс-поиск по всему миру (для всех ваших контроллеров):
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Component
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:3000");
}
}
Обратите внимание, что это редко бывает хорошей идеей, чтобы просто скопировать его так. В нескольких компаниях, над которыми я работал, разрешенные корни настраивались через портал администратора, поэтому на средах разработки вы могли бы добавить все необходимые вам происхождение.
Ответ 7
Я полностью согласен с ответом Bludream, но у меня есть некоторые замечания:
Я бы расширил предложение if в фильтре CORS с помощью проверки NULL в заголовке источника:
public class CorsFilter extends OncePerRequestFilter {
private static final String ORIGIN = "Origin";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getHeader(ORIGIN) == null || request.getHeader(ORIGIN).equals("null")) {
response.addHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Max-Age", "10");
String reqHead = request.getHeader("Access-Control-Request-Headers");
if (!StringUtils.isEmpty(reqHead)) {
response.addHeader("Access-Control-Allow-Headers", reqHead);
}
}
if (request.getMethod().equals("OPTIONS")) {
try {
response.getWriter().print("OK");
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
} else{
filterChain.doFilter(request, response);
}
}
}
Кроме того, я заметил следующее нежелательное поведение: если я пытаюсь получить доступ к REST API с несанкционированной ролью, Spring security возвращает мне статус HTTP 403: FORBIDDEN и возвращаются заголовки CORS. Однако, если я использую неизвестный токен или токен, который больше недействителен, статус HTTP 401: UNAUTHORIZED возвращается без заголовков CORS.
Мне удалось заставить его работать, изменив конфигурацию фильтра в XML-безопасности следующим образом:
<security:http use-expressions="true" .... >
...
//your other configs
<sec:custom-filter ref="corsFilter" before="HEADERS_FILTER"/>
</security:http>
И следующий bean для нашего настраиваемого фильтра:
<bean id="corsFilter" class="<<location of the CORS filter class>>" />
Ответ 8
Поскольку основная часть вопроса касается неаронизированного запроса CORS POST для входа в точку входа, я сразу же укажу вам на шаг 2.
Но в отношении ответов на вопрос, это самый актуальный вопрос для запроса Spring Security CORS.
Поэтому я опишу более элегантное решение для настройки CORS с помощью Spring Security.
Потому что, за исключением редких ситуаций, нет необходимости создавать фильтры/перехватчики/... чтобы что-то в ответ.
Мы сделаем это декларативно Spring.
Поскольку Spring Framework 4.2, мы имеем CORS-материал, такой как фильтр, процессор и т.д. Из коробки.
И некоторые ссылки для чтения 1 2.
Отпустите:
1. Подготовьте источник конфигурации CORS.
Это можно сделать по-разному:
-
как глобальный Spring Конфигурация MVC CORS (в классах конфигурации, таких как WebMvcConfigurerAdapter
)
...
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
...
}
-
как отдельный corsConfigurationSource
bean
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
}
-
как внешний класс (который можно использовать с помощью конструктора или автоматически в качестве компонента)
// @Component // <- for autowiring
class CorsConfig extends UrlBasedCorsConfigurationSource {
CorsConfig() {
orsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues(); // <- frequantly used values
this.registerCorsConfiguration("/**", config);
}
}
2. включить поддержку CORS с определенной конфигурацией
Мы включим поддержку CORS в Spring классах безопасности, таких как WebSecurityConfigurerAdapter
. Убедитесь, что corsConfigurationSource
доступен для этой поддержки.
Else предоставить его с помощью @Resource
автоувеличивания или установить явно (см. Пример).
Также мы разрешаем несанкционированный доступ к некоторым конечным точкам, таким как login:
...
// @Resource // <- for autowired solution
// CorseConfigurationSource corsConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
// or autowiring
// http.cors().configurationSource(corsConfig);
// or direct set
// http.cors().configurationSource(new CorsConfig());
http.authorizeRequests()
.antMatchers("/login").permitAll() // without this line login point will be unaccessible for authorized access
.antMatchers("/*").hasAnyAuthority(Authority.all()); // <- all other security stuff
}
3. настройте конфигурацию CORS
Если базовая конфигурация работает, мы можем настроить сопоставления, происхождение и т.д. Даже добавьте несколько конфигураций для разных сопоставлений.
Например, я явно объявляю все параметры CORS и позволяю UrlPathHelper не обрезать мой путь сервлета:
class RestCorsConfig extends UrlBasedCorsConfigurationSource {
RestCorsConfig() {
this.setCorsConfigurations(Collections.singletonMap("/**", corsConfig()));
this.setAlwaysUseFullPath(true);
}
private static CorsConfiguration corsConfig() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.setMaxAge(3600L);
return config;
}
}
4. устранение неполадок
Чтобы отладить мою проблему, я отслеживал метод org.springframework.web.filter.CorsFilter#doFilterInternal
.
И я увидел, что поиск CorsConfiguration возвращает null
, потому что Spring глобальная конфигурация CORS MVC не была обнаружена Spring Security.
Поэтому я использовал решение с прямым использованием внешнего класса:
http.cors().configurationSource(corsConfig);