Il existe de nombreuses directives, des exemples de codes qui montrent comment sécuriser REST avec Spring Security, mais la plupart d'entre eux supposent un client Web et parlent de la page de connexion, de la redirection, de l'utilisation de cookies, etc. être même un simple filtre qui vérifie le jeton personnalisé dans l'en-tête HTTP peut être suffisant. Comment puis-je implémenter la sécurité pour les exigences ci-dessous? Existe-t-il un projet Gist/github faisant de même? Mes connaissances en sécurité de printemps sont limitées, donc s'il y a un moyen plus simple de mettre en œuvre cela avec la sécurité du printemps, veuillez me le faire savoir.
J'utilise Springboot, la sécurité du printemps, etc. préfère une solution avec Java config (no XML)
Mon exemple d'application fait exactement cela - sécuriser REST points de terminaison à l'aide de Spring Security dans un scénario sans état. Individuel REST sont authentifiés à l'aide d'un En-tête HTTP. Les informations d'authentification sont stockées côté serveur dans un cache en mémoire et fournissent la même sémantique que celles proposées par la session HTTP dans une application Web classique. L'application utilise l'infrastructure Spring Security complète avec un code personnalisé très minimal. Non filtres nus, pas de code en dehors de l'infrastructure Spring Security.
L'idée de base est d'implémenter les quatre composants de sécurité Spring suivants:
org.springframework.security.web.AuthenticationEntryPoint
pour intercepter REST appels nécessitant une authentification mais manquant le jeton d'authentification requis et ainsi refuser les demandes.org.springframework.security.core.Authentication
pour conserver les informations d'authentification requises pour l'API REST.org.springframework.security.authentication.AuthenticationProvider
pour effectuer l'authentification réelle (contre une base de données, un serveur LDAP, un service Web, etc.).org.springframework.security.web.context.SecurityContextRepository
pour contenir le jeton d'authentification entre les requêtes HTTP. Dans l'exemple, l'implémentation enregistre le jeton dans une instance EHCACHE.L'exemple utilise la configuration XML mais vous pouvez facilement trouver l'équivalent Java config.
Vous avez raison, ce n'est pas facile et il n'y a pas beaucoup de bons exemples. Les exemples que j'ai vus l'ont fait pour que vous ne puissiez pas utiliser d'autres éléments de sécurité à ressort côte à côte. J'ai fait quelque chose de similaire récemment, voici ce que j'ai fait.
Vous avez besoin d'un jeton personnalisé pour conserver votre valeur d'en-tête
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);
}
}
Vous avez besoin d'un filtre de sécurité à ressort pour extraire l'en-tête et demander au gestionnaire de l'authentifier, quelque chose comme ça texte souligné
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);
}
}
Enregistrez votre filtre personnalisé dans la chaîne de sécurité à ressort
@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 fournisseur d'authentification personnalisé pour valider ce jeton extrait avec le filtre.
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);
}
}
Enfin, enregistrez votre fournisseur en tant que bean pour que le gestionnaire d'authentification le trouve dans une classe @Configuration. Vous pourriez probablement aussi @Component, je préfère cette méthode
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies) {
return new CustomAuthenticationProvider(injectedDependencies);
}
Le code sécurise tous les points de terminaison - mais je suis sûr que vous pouvez jouer avec ça :). Le jeton est stocké dans Redis à l'aide de Spring Boot Starter Security et vous devez définir notre propre UserDetailsService
que vous passez dans AuthenticationManagerBuilder
.
Longue histoire - copiez-collez EmbeddedRedisConfiguration
et SecurityConfig
et remplacez AuthenticationManagerBuilder
dans votre logique.
HTTP:
Jeton de demande - envoi du contenu d'authentification HTTP de base dans un en-tête de demande. Un jeton est rendu dans un en-tête de réponse.
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
Même demande mais en utilisant un jeton:
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 vous transmettez un nom d'utilisateur/mot de passe ou un jeton incorrect, vous obtenez 401.
Java
J'ai ajouté ces dépendances dans 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")
Puis Redis configration
@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();
}
}
Configuration de sécurité:
@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();
}
}
Habituellement, dans les didacticiels, vous trouvez AuthenticationManagerBuilder
en utilisant inMemoryAuthentication
mais il y a beaucoup plus de choix (LDAP, ...) Jetez simplement un œil à la définition de classe. J'utilise userDetailsService
qui nécessite un objet UserDetailsService
.
Et enfin mon service utilisateur en utilisant 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"));
}
}
Un autre exemple de projet qui utilise JWT - Jhipster
Essayez de générer une application de microservice à l'aide de JHipster. Il génère un modèle avec une intégration prête à l'emploi entre Spring Security et JWT.