java - authorities - roles en spring security
Intercambio de recursos de origen cruzado con Spring Security (8)
Dado que la parte principal de la pregunta es sobre la solicitud de POST de CORS no autorizada para el punto de inicio de sesión, le indico inmediatamente al paso 2
Pero con respecto a las respuestas, esta es la pregunta más relevante para la solicitud CORS de Spring Security . Así que describiré una solución más elegante para configurar CORS con Spring Security. Porque, salvo en situaciones raras, no es necesario crear filtros / interceptores / ... para poner algo en respuesta. Lo haremos declarativamente en primavera. Desde Spring Framework 4.2 tenemos CORS-stuff como filtro, procesador, etc., listos para usar. Y algunos enlaces para leer 1 2 .
Vamonos:
1. Preparar la fuente de configuración de CORS.
Se puede hacer de diferentes maneras:
como configuración global de Spring MVC CORS (en clases de configuración como
WebMvcConfigurerAdapter
)... @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") ... }
como
corsConfigurationSource
separado@Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.applyPermitDefaultValues(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); }
como clase externa (que se puede usar a través de un constructor o autowired como componente)
// @Component // <- for autowiring class CorsConfig extends UrlBasedCorsConfigurationSource { CorsConfig() { orsConfiguration config = new CorsConfiguration(); config.applyPermitDefaultValues(); // <- frequantly used values this.registerCorsConfiguration("/**", config); } }
2. habilitar el soporte CORS con la configuración definida
Permitiremos el soporte de CORS en las clases de Spring Security como WebSecurityConfigurerAdapter
. Asegúrese de que corsConfigurationSource
sea accesible para este soporte. De lo contrario, proporcione a través de @Resource
autowiring o establezca explícitamente (ver en el ejemplo). También permitimos el acceso no autorizado a algunos puntos finales como inicio de sesión:
...
// @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. personalizar la configuración de CORS
Si la configuración básica funciona, entonces podemos personalizar mapeos, orígenes, etc. Incluso agregar varias configuraciones para diferentes mapeos. Por ejemplo, declaro explícitamente todos los parámetros CORS y permito que UrlPathHelper no recorte mi ruta de servlet:
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. solución de problemas
Para depurar mi problema estaba rastreando org.springframework.web.filter.CorsFilter#doFilterInternal
método org.springframework.web.filter.CorsFilter#doFilterInternal
. Y vi que la búsqueda de CorsConfiguration devuelve null
porque Spring MVC la configuración global CORS no fue vista por Spring Security. Entonces utilicé la solución con el uso directo de la clase externa:
http.cors().configurationSource(corsConfig);
Estoy tratando de hacer que CORS juegue bien con Spring Security pero no está cumpliendo. Hice los cambios descritos en este artículo y al cambiar esta línea en applicationContext-security.xml
POST y GET funcionan para mi aplicación (expone temporalmente los métodos del controlador, por lo que puedo probar CORS):
- Antes:
<intercept-url pattern="/**" access="isAuthenticated()" />
- Después:
<intercept-url pattern="/**" access="permitAll" />
Desafortunadamente, la siguiente URL que permite los inicios de sesión de Spring Security a través de AJAX no responde: http://localhost:8080/mutopia-server/resources/j_spring_security_check
. Estoy haciendo la solicitud AJAX de http://localhost:80
a http://localhost:8080
.
En cromo
Cuando intento acceder a j_spring_security_check
obtengo (pending)
en Chrome para la solicitud de verificación previa de OPTIONS y la llamada AJAX regresa con el código de estado HTTP 0 y el mensaje "error".
En firefox
La verificación previa es exitosa con el código de estado HTTP 302 y todavía recibo la devolución de llamada de error para mi solicitud AJAX directamente después con el estado HTTP 0 y el mensaje "error".
Código de solicitud 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);
Tenga en cuenta que puedo enviar por correo electrónico a cualquier otra URL de mi aplicación a través de CORS, excepto el inicio de sesión de Spring Security. He revisado muchos artículos, por lo que cualquier información sobre este extraño problema sería muy apreciada.
Desde Spring Security 4.1, esta es la forma correcta de hacer que Spring Security sea compatible con CORS (también se necesita en 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");
}
}
y:
@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''s 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;
}
}
No haga nada de lo siguiente, que es la forma incorrecta de intentar resolver el problema:
-
http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
-
web.ignoring().antMatchers(HttpMethod.OPTIONS);
Referencia: http://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/cors.html
En mi caso, response.getWriter (). Flush () no estaba funcionando
Cambié el código como abajo y comenzó a funcionar.
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);
}
}
Estoy totalmente de acuerdo con la respuesta dada por Bludream, pero tengo algunas observaciones:
Extendería la cláusula if en el filtro CORS con una comprobación NULA en el encabezado de origen:
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);
}
}
}
Además, noté el siguiente comportamiento no deseado: si intento acceder a una API REST con una función no autorizada, Spring Security me devuelve un estado HTTP 403: se devuelven FORBIDDEN y los encabezados CORS. Sin embargo, si utilizo un token desconocido o un token que ya no es válido, se devuelve un estado HTTP 401: NO AUTORIZADO SIN encabezados CORS.
Logré hacer que funcionara cambiando la configuración del filtro en el XML de seguridad de esta manera:
<security:http use-expressions="true" .... >
...
//your other configs
<sec:custom-filter ref="corsFilter" before="HEADERS_FILTER"/>
</security:http>
Y el siguiente bean para nuestro filtro personalizado:
<bean id="corsFilter" class="<<location of the CORS filter class>>" />
Para mí, el problema fue que la verificación de verificación previa de OPTIONS
falló en la autenticación, porque las credenciales no se pasaron en esa llamada.
Esto funciona para mí:
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());
}
}
La parte relevante es:
.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());
}
}
});
Eso corrige el problema de verificación previa de OPTIONS
. Lo que sucede aquí es que cuando recibe una llamada y falla la autenticación, verifica si es una llamada de OPTIONS
y, si es así, simplemente déjela pasar y deje que haga todo lo que quiere hacer. Básicamente, esto desactiva todas las comprobaciones de verificación previa del lado del navegador, pero la política normal de dominio cruzado aún se aplica.
Cuando está utilizando la última versión de Spring, puede usar el siguiente código para permitir las solicitudes de origen cruzado globalmente (para todos sus controladores):
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");
}
}
Tenga en cuenta que esto rara vez es una buena idea simplemente codificarlo de esta manera. En algunas de las compañías en las que he trabajado, los orígenes permitidos se podían configurar a través de un portal de administración, por lo que en los entornos de desarrollo podría agregar todos los orígenes que necesita.
Principalmente, la solicitud de OPCIONES no lleva cookies para la autenticación de la seguridad de Spring.
Para resolver esto, puede modificar la configuración de la seguridad de Spring para permitir la solicitud de OPCIONES sin autenticación.
Investigo mucho y consigo dos soluciones:
1.Utilizando la configuración de Java con la configuración de seguridad de primavera,
@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.Utilizando XML ( nota. No puedo escribir "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>
Al final, está la fuente de la solución ... :)
Pude hacer esto extendiendo UsernamePasswordAuthenticationFilter ... mi código está en Groovy, espero que esté bien:
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)
}
}
Los bits importantes arriba:
- Solo agregue encabezados CORS a la respuesta si se detecta una solicitud CORS
- Responda a la solicitud de OPCIONES previas al vuelo con una simple respuesta no vacía 200, que también contiene los encabezados CORS.
Necesita declarar este bean en su configuración de Spring. Hay muchos artículos que muestran cómo hacer esto, así que no los copiaré aquí.
En mi propia implementación, uso una lista blanca de dominios de origen, ya que estoy permitiendo que CORS solo tenga acceso a desarrolladores internos. Lo anterior es una versión simplificada de lo que estoy haciendo, por lo que puede necesitar ajustes, pero esto debería darle una idea general.
bueno, este es mi código que funciona muy bien y es perfecto para mí: pasé dos días trabajando en él y entendiendo la seguridad de primavera, así que espero que lo acepten como la respuesta, jajaja
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);
}
}
}
Bueno, entonces también necesitas configurar tu filtro para ser invocado:
<security:http use-expressions="true" .... >
...
//your other configs
<security:custom-filter ref="corsHandler" after="PRE_AUTH_FILTER"/> // this goes to your filter
</security:http>
Bueno, y necesitas un bean para el filtro personalizado que creaste:
<bean id="corsHandler" class="mobilebackbone.mesoft.config.CorsFilter" />