Je crée une API pour une application Angular 5. Je voudrais utiliser JWT pour l'authentification.
Je souhaite utiliser les fonctionnalités fournies par Spring Security pour pouvoir facilement travailler avec des rôles.
J'ai réussi à désactiver l'authentification de base. Mais lorsque j'utilise http.authorizeExchange().anyExchange().authenticated();
j'obtiens toujours une invite de connexion.
Je voudrais juste donner un 403 au lieu de l'invite. Donc, remplacer l'invite de connexion par une "chose" (est-ce un filtre?) Qui vérifie l'en-tête Authorization
pour le jeton.
La connexion que je veux simplement faire dans un contrôleur qui renverra un jeton JWT. Mais quel bean de sécurité de printemps dois-je utiliser pour vérifier les informations d'identification de l'utilisateur? Je peux créer mes propres services et référentiels, mais j'aimerais utiliser autant que possible les fonctionnalités fournies par Spring Security.
La version courte de cette question est juste:
Comment puis-je personnaliser l'authentification de Spring Security?
Quels haricots dois-je créer?
Où dois-je mettre la configuration? (J'ai maintenant un bean de SecurityWebFilterChain
)
La seule documentation que j'ai pu trouver sur l'authentification dans webflux avec sécurité Spring est la suivante: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/ # jc-webflux
Après beaucoup de recherches et d'essais, je pense avoir trouvé la solution:
Vous avez besoin d'un bean de SecurityWebFilterChain
qui contient toute la configuration.
C'est à moi:
@Configuration
public class SecurityConfiguration {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// Disable default security.
http.httpBasic().disable();
http.formLogin().disable();
http.csrf().disable();
http.logout().disable();
// Add custom security.
http.authenticationManager(this.authenticationManager);
http.securityContextRepository(this.securityContextRepository);
// Disable authentication for `/auth/**` routes.
http.authorizeExchange().pathMatchers("/auth/**").permitAll();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
}
J'ai désactivé httpBasic, formLogin, csrf et logout pour pouvoir faire mon authentification personnalisée.
En définissant AuthenticationManager
et SecurityContextRepository
, j'ai remplacé la configuration de sécurité par défaut du printemps pour vérifier si un utilisateur est authentifié/autorisé pour une demande.
Le gestionnaire d'authentification:
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// JwtAuthenticationToken is my custom token.
if (authentication instanceof JwtAuthenticationToken) {
authentication.setAuthenticated(true);
}
return Mono.just(authentication);
}
}
Je ne sais pas exactement où le gestionnaire d'authentification est destiné, mais je pense que pour faire l'authentification finale, définissez donc authentication.setAuthenticated(true);
quand tout va bien.
SecurityContextRepository:
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
@Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
// Don't know yet where this is for.
return null;
}
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
// JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ?
new JwtAuthenticationToken(...) :
new GuestAuthenticationToken();
return new SecurityContextImpl(authentication);
}
}
Dans la charge, je vérifierai sur la base des en-têtes dans le serverWebExchange
si l'utilisateur est authentifié. J'utilise https://github.com/jwtk/jjwt . Je renvoie un autre type de jeton d'authentification si l'utilisateur est authentifié ou non.
Dans mon ancien projet, j'ai utilisé cette configuration:
@Configuration
@EnableWebSecurity
@Import(WebMvcConfig.class)
@PropertySource(value = { "classpath:config.properties" }, encoding = "UTF-8", ignoreResourceNotFound = false)
public class WebSecWebSecurityCfg extends WebSecurityConfigurerAdapter
{
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("objectMapper")
private ObjectMapper mapper;
@Autowired
@Qualifier("passwordEncoder")
private PasswordEncoder passwordEncoder;
@Autowired
private Environment env;
public WebSecWebSecurityCfg(UserDetailsService userDetailsService)
{
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
JWTAuthorizationFilter authFilter = new JWTAuthorizationFilter
( authenticationManager(),//Auth mgr
env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
env.getProperty("config.jwt.header.string"), //nome header
env.getProperty("config.jwt.token.prefix") //Prefisso token
);
JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter
(
authenticationManager(), //Authentication Manager
env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
Long.valueOf(env.getProperty("config.jwt.token.duration")),//Durata del token in millisecondi
env.getProperty("config.jwt.header.string"), //nome header
env.getProperty("config.jwt.token.prefix"), //Prefisso token
mapper
);
http
.cors()
.and()
.csrf()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilter(authenticationFilter)
.addFilter(authFilter)
// Disabilitiamo la creazione di sessione in spring
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
@Bean
CorsConfigurationSource corsConfigurationSource()
{
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
Où JWTAuthorizationFilter
est:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter
{
private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class.getName());
private String secretKey;
private String headerString;
private String tokenPrefix;
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, String secretKey, String headerString, String tokenPrefix)
{
super(authenticationManager, authenticationEntryPoint);
this.secretKey = secretKey;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
}
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, String secretKey, String headerString, String tokenPrefix)
{
super(authenticationManager);
this.secretKey = secretKey;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
}
@Override
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException
{
AuthenticationErrorEnum customErrorCode = null;
StringBuilder builder = new StringBuilder();
if( failed.getCause() instanceof MissingJwtTokenException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_MANCANTE;
}
else if( failed.getCause() instanceof ExpiredJwtException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_SCADUTO;
}
else if( failed.getCause() instanceof MalformedJwtException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NON_CORRETTO;
}
else if( failed.getCause() instanceof MissingUserSubjectException )
{
customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NESSUN_UTENTE_TROVATO;
}
else if( ( failed.getCause() instanceof GenericJwtAuthorizationException ) || ( failed.getCause() instanceof Exception ) )
{
customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
}
builder.append("Errore duranre l'autorizzazione. ");
builder.append(failed.getMessage());
JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
String errore = ( new ObjectMapper() ).writeValueAsString(apiError);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
}
Et JWTAuthenticationFilter
est
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
{
private AuthenticationManager authenticationManager;
private String secretKey;
private long tokenDurationMillis;
private String headerString;
private String tokenPrefix;
private ObjectMapper mapper;
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
{
AuthenticationErrorEnum customErrorCode = null;
StringBuilder builder = new StringBuilder();
if( failed instanceof BadCredentialsException )
{
customErrorCode = AuthenticationErrorEnum.CREDENZIALI_SERVIZIO_ERRATE;
}
else
{
//Teoricamente nella fase di autenticazione all'errore generico non dovrebbe mai arrivare
customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
}
builder.append("Errore durante l'autenticazione del servizio. ");
builder.append(failed.getMessage());
JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
String errore = mapper.writeValueAsString(apiError);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
}
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, String secretKey, long tokenDurationMillis, String headerString, String tokenPrefix, ObjectMapper mapper)
{
super();
this.authenticationManager = authenticationManager;
this.secretKey = secretKey;
this.tokenDurationMillis = tokenDurationMillis;
this.headerString = headerString;
this.tokenPrefix = tokenPrefix;
this.mapper = mapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException
{
try
{
ServiceLoginDto creds = new ObjectMapper().readValue(req.getInputStream(), ServiceLoginDto.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getCodiceServizio(), creds.getPasswordServizio(), new ArrayList<>()));
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException
{
DateTime dt = new DateTime();
Date expirationTime = dt.plus(getTokenDurationMillis()).toDate();
String token = Jwts
.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(expirationTime)
.signWith(SignatureAlgorithm.HS512, getSecretKey().getBytes())
.compact();
res.addHeader(getHeaderString(), getTokenPrefix() + token);
res.addHeader("jwtExpirationDate", expirationTime.toString());
res.addHeader("jwtTokenDuration", String.valueOf(TimeUnit.MILLISECONDS.toMinutes(getTokenDurationMillis()))+" minuti");
}
public String getSecretKey()
{
return secretKey;
}
public void setSecretKey(String secretKey)
{
this.secretKey = secretKey;
}
public long getTokenDurationMillis()
{
return tokenDurationMillis;
}
public void setTokenDurationMillis(long tokenDurationMillis)
{
this.tokenDurationMillis = tokenDurationMillis;
}
public String getHeaderString()
{
return headerString;
}
public void setHeaderString(String headerString)
{
this.headerString = headerString;
}
public String getTokenPrefix()
{
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix)
{
this.tokenPrefix = tokenPrefix;
}
}
Le détail de l'utilisateur est un détail de service utilisateur classique
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Autowired
private IServizioService service;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
Service svc;
try
{
svc = service.findBySvcCode(username);
}
catch (DbException e)
{
throw new UsernameNotFoundException("Errore durante il processo di autenticazione; "+e.getMessage(), e);
}
if (svc == null)
{
throw new UsernameNotFoundException("Nessun servizio trovato per il codice servizio "+username);
}
else if( !svc.getAbilitato().booleanValue() )
{
throw new UsernameNotFoundException("Servizio "+username+" non abilitato");
}
return new User(svc.getCodiceServizio(), svc.getPasswordServizio(), Collections.emptyList());
}
}
Veuillez noter que je n'ai pas utilisé Spring Webflux
J'espère que c'est utile
Angelo
Merci Jan, vous m'avez beaucoup aidé avec votre exemple pour personnaliser l'authentification dans mon application Spring Webflux et sécuriser l'accès aux API.
Dans mon cas, j'ai juste besoin de lire un en-tête pour définir les rôles utilisateur et je veux que la sécurité Spring vérifie les autorisations utilisateur pour sécuriser l'accès à mes méthodes.
Vous avez donné la clé avec une http.securityContextRepository(this.securityContextRepository);
personnalisée dans SecurityConfiguration (pas besoin d'un gestionnaire d'authentification personnalisé).
Grâce à ce SecurityContextRepository, j'ai pu créer et définir une authentification personnalisée (simplifiée ci-dessous).
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
String role = serverWebExchange.getRequest().getHeaders().getFirst("my-header");
Authentication authentication =
new AnonymousAuthenticationToken("authenticated-user", someUser, AuthorityUtils.createAuthorityList(role) );
return Mono.just(new SecurityContextImpl(authentication));
}
Et ainsi je peux sécuriser mes méthodes en utilisant ces rôles:
@Component
public class MyService {
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> checkAdmin() {
// my secure method
}
}
Pour ceux qui ont le même problème (Webflux + Custom Authentication + JWT
) J'ai résolu en utilisant AuthenticationWebFilter
, personnalisé ServerAuthenticationConverter
et ReactiveAuthenticationManager
, en suivant le code, j'espère que cela pourrait aider quelqu'un à l'avenir. Testé avec la dernière version (spring-boot 2.1.1.RELEASE
).
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SpringSecurityConfiguration {
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
return http
.csrf()
.disable()
.headers()
.frameOptions().disable()
.cache().disable()
.and()
.authorizeExchange()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.build();
}
private AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
authenticationWebFilter.setServerAuthenticationConverter(new JwtAuthenticationConverter(tokenProvider));
NegatedServerWebExchangeMatcher negateWhiteList = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST));
authenticationWebFilter.setRequiresAuthenticationMatcher(negateWhiteList);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
authenticationWebFilter.setAuthenticationFailureHandler(responseError());
return authenticationWebFilter;
}
}
public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
private final TokenProvider tokenProvider;
public JwtAuthenticationConverter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
private Mono<String> resolveToken(ServerWebExchange exchange) {
log.debug("servletPath: {}", exchange.getRequest().getPath());
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
.filter(t -> t.startsWith("Bearer "))
.map(t -> t.substring(7));
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return resolveToken(exchange)
.filter(tokenProvider::validateToken)
.map(tokenProvider::getAuthentication);
}
}
public class CustomReactiveAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager {
public CustomReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
super(userDetailsService);
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
return super.authenticate(authentication);
}
}
PS: La classe TokenProvider que vous trouverez sur https://github.com/jhipster/jhipster-registry/blob/master/src/main/Java/io/github/jhipster/registry/security/jwt/TokenProvider. Java