example - Asegurar la API REST utilizando tokens personalizados(sin estado, sin interfaz de usuario, sin cookies, sin autenticación básica, sin OAuth, sin página de inicio de sesión)
oauth2 (5)
Hay muchas pautas, ejemplos de códigos que muestran cómo proteger la API REST con Spring Security, pero la mayoría asume un cliente web y habla de la página de inicio de sesión, la redirección, el uso de cookies, etc. Puede ser incluso un filtro simple que busca el token personalizado en el encabezado HTTP podría ser suficiente. ¿Cómo implemento la seguridad para los requisitos a continuación? ¿Hay algún proyecto de github que haga lo mismo? Mi conocimiento sobre la seguridad de primavera es limitado, así que si hay una forma más sencilla de implementarlo con la seguridad de primavera, háganmelo saber.
- API REST servida por back-end sin estado sobre HTTPS
- el cliente podría ser una aplicación web, una aplicación móvil, cualquier aplicación de estilo SPA, API de terceros
- sin autenticación básica, sin cookies, sin interfaz de usuario (sin JSP / HTML / recursos estáticos), sin redirecciones, sin proveedor de OAuth.
- token personalizado configurado en encabezados HTTPS
- La validación del token realizada contra el almacenamiento externo (como MemCached / Redis / o incluso cualquier RDBMS)
- Todas las API deben ser autenticadas, excepto para las rutas seleccionadas (como / login, / signup, / public, etc.)
Uso Springboot, seguridad de primavera, etc. prefiero una solución con configuración de Java (sin XML)
El código protege todos los puntos finales, pero estoy seguro de que puedes jugar con eso :). El token se almacena en Redis usando Spring Boot Starter Security y usted tiene que definir nuestro propio UserDetailsService
que pasa a AuthenticationManagerBuilder
.
Para abreviar, copie y pegue EmbeddedRedisConfiguration
y SecurityConfig
y reemplace AuthenticationManagerBuilder
por su lógica.
HTTP:
Token de solicitud: envía contenido HTTP auth básico en un encabezado de solicitud. Un token se devuelve en un encabezado de respuesta.
http --print=hH -a user:password localhost:8080/v1/users
GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af
La misma solicitud pero usando token:
http --print=hH localhost:8080/v1/users ''x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af''
GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Si pasa un nombre de usuario / contraseña o un token incorrecto, obtiene 401.
JAVA
build.gradle
esas dependencias en build.gradle
compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")
Luego la configuración de Redis
@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {
private static RedisServer redisServer;
@Bean
public JedisConnectionFactory connectionFactory() throws IOException {
redisServer = new RedisServer(Protocol.DEFAULT_PORT);
redisServer.start();
return new JedisConnectionFactory();
}
@PreDestroy
public void destroy() {
redisServer.stop();
}
}
Configuración de seguridad:
@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.httpBasic();
}
@Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}
Usualmente en los tutoriales se encuentra AuthenticationManagerBuilder
usando inMemoryAuthentication
pero hay muchas más opciones (LDAP, ...) Solo eche un vistazo a la definición de la clase. Estoy usando userDetailsService
que requiere el objeto UserDetailsService
.
Y finalmente mi servicio al usuario usando CrudRepository
.
@Service
public class UserService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserAccount userAccount = userRepository.findByEmail(username);
if (userAccount == null) {
return null;
}
return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
Mi aplicación de ejemplo hace exactamente esto: asegurar los puntos finales REST usando Spring Security en un escenario sin estado. Las llamadas REST individuales se autentican usando un encabezado HTTP. La información de autenticación se almacena en el servidor en una memoria caché en memoria y proporciona la misma semántica que las ofrecidas por la sesión HTTP en una aplicación web típica. La aplicación utiliza la infraestructura completa de Spring Security con un código personalizado mínimo. Sin filtros desnudos, sin código fuera de la infraestructura de Spring Security.
La idea básica es implementar los siguientes cuatro componentes de Spring Security:
-
org.springframework.security.web.AuthenticationEntryPoint
para atrapar llamadas REST que requieren autenticación pero faltan el token de autenticación requerido y, por lo tanto, denegar las solicitudes. -
org.springframework.security.core.Authentication
para contener la información de autenticación requerida para la API REST. -
org.springframework.security.authentication.AuthenticationProvider
para realizar la autenticación real (en una base de datos, un servidor LDAP, un servicio web, etc.). -
org.springframework.security.web.context.SecurityContextRepository
para mantener el token de autenticación entre solicitudes HTTP. En la muestra, la implementación guarda el token en una instancia de EHCACHE.
El ejemplo usa la configuración XML pero puede encontrar fácilmente la configuración Java equivalente.
Otro ejemplo de proyecto que utiliza JWT - Jhipster
Intente generar una aplicación de Microservice utilizando JHipster. Genera una plantilla con integración fuera de la caja entre Spring Security y JWT.
Recomiendo JSON Web Tokens http://jwt.io/ , es apátrida y escalable.
Aquí hay un proyecto de ejemplo, https://github.com/brahalla/Cerberus
Tienes razón, no es fácil y no hay muchos buenos ejemplos. Los ejemplos que vi lo hicieron para que no pudieras usar otros elementos de seguridad el uno al lado del otro. Hice algo similar recientemente, esto es lo que hice.
Necesita un token personalizado para mantener el valor de su encabezado
public class CustomToken extends AbstractAuthenticationToken {
private final String value;
//Getters and Constructor. Make sure getAutheticated returns false at first.
//I made mine "immutable" via:
@Override
public void setAuthenticated(boolean isAuthenticated) {
//It doesn''t make sense to let just anyone set this token to authenticated, so we block it
//Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
if (isAuthenticated) {
throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
}
super.setAuthenticated(false);
}
}
Necesita un filtro de seguridad de primavera para extraer el encabezado y pedirle al administrador que lo autentique, algo así como texto enfatizado
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
public CustomFilter(RequestMatcher requestMatcher) {
super(requestMatcher);
this.setAuthenticationSuccessHandler((request, response, authentication) -> {
/*
* On success the desired action is to chain through the remaining filters.
* Chaining is not possible through the success handlers, because the chain is not accessible in this method.
* As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
});
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String tokenValue = request.getHeader("SOMEHEADER");
if(StringUtils.isEmpty(tokenValue)) {
//Doing this check is kinda dumb because we check for it up above in doFilter
//..but this is a public method and we can''t do much if we don''t have the header
//also we can''t do the check only here because we don''t have the chain available
return null;
}
CustomToken token = new CustomToken(tokenValue);
token.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(token);
}
/*
* Overriding this method to maintain the chaining on authentication success.
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//if this isn''t called, then no auth is set in the security context holder
//and subsequent security filters can still execute.
//so in SOME cases you might want to conditionally call this
super.successfulAuthentication(request, response, chain, authResult);
//Continue the chain
chain.doFilter(request, response);
}
}
Registre su filtro personalizado en la cadena de seguridad de primavera
@Configuration
public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {
//Note, we don''t register this as a bean as we don''t want it to be added to the main Filter chain, just the spring security filter chain
protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
filter.setAuthenticationManager(this.authenticationManagerBean());
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//fyi: This adds it to the spring security proxy filter chain
.addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
}
}
Un proveedor de autenticación personalizado para validar ese token extraído con el filtro.
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
CustomToken token = (CustomToken)auth;
try{
//Authenticate token against redis or whatever you want
//This i found weird, you need a Principal in your Token...I use User
//I found this to be very redundant in spring security, but Controller param resolving will break if you don''t do this...anoying
org.springframework.security.core.userdetails.User principal = new User(...);
//Our token resolved to a username so i went with this token...you could make your CustomToken take the principal. getCredentials returns "NO_PASSWORD"..it gets cleared out anyways. also the getAuthenticated for the thing you return should return true now
return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
} catch(Expection e){
//TODO throw appropriate AuthenticationException types
throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
}
}
@Override
public boolean supports(Class<?> authentication) {
return CustomToken.class.isAssignableFrom(authentication);
}
}
Finalmente, registre su proveedor como un bean para que el administrador de autenticación lo encuentre en alguna clase @Configuration. Probablemente podrías simplemente @Component también, prefiero este método
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies) {
return new CustomAuthenticationProvider(injectedDependencies);
}