可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I am creating an API for an Angular 5 application. I would like to use JWT for authentication.
I would like to use the features that are provided by spring security so I can easily work with roles.
I managed to disable basic authentication. But when using http.authorizeExchange().anyExchange().authenticated();
I still get a login prompt.
I would like to just give a 403 instead of the prompt. So overriding the login prompt by a "thing"(Is it a filter?) that checks the Authorization
header for the token.
The login I just want to do in a controller that will return a JWT token. But what spring security bean I should use for checking user credentials? I can build my own services and repositories, but I would like to use the features provided by spring security as much as possible.
The short version of this question is just:
How can I customize spring security's authentication?
What beans do I have to create?
Where do I have to put the configuration? (I now have a bean of SecurityWebFilterChain
)
The only documentation I could find about authentication in webflux with spring security is this: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux
回答1:
After a lot of searching and trying I think I have found the solution:
You need a bean of SecurityWebFilterChain
that contains all configuration.
This is mine:
@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(); } }
I've disabled httpBasic, formLogin, csrf and logout so I could make my custom authentication.
By setting the AuthenticationManager
and SecurityContextRepository
I overridden the default spring security configuration for checking if a user is authenticated/authorized for a request.
The authentication manager:
@Component public class AuthenticationManager implements ReactiveAuthenticationManager { @Override public Mono authenticate(Authentication authentication) { // JwtAuthenticationToken is my custom token. if (authentication instanceof JwtAuthenticationToken) { authentication.setAuthenticated(true); } return Mono.just(authentication); } }
I am not fully sure where the authentication manager is for, but I think for doing the final authentication, so setting authentication.setAuthenticated(true);
when everything is right.
SecurityContextRepository:
@Component public class SecurityContextRepository implements ServerSecurityContextRepository { @Override public Mono save(ServerWebExchange serverWebExchange, SecurityContext securityContext) { // Don't know yet where this is for. return null; } @Override public Mono 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); } }
In the load I will check based on the headers in the serverWebExchange
if the user is authenticated. I use https://github.com/jwtk/jjwt. I return a different kind of authentication token if the user is authenticated or not.
回答2:
In my old project I used this 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; } }
Where JWTAuthorizationFilter
is:
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); }
And JWTAuthenticationFilter
is
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; } }
The user detail is a classical userservicedetail
@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()); } }
Please note I didn't use Spring webflux
I hope it's useful
Angelo
回答3:
Thanks Jan you helped me a lot with your example to customize authentication in my Spring Webflux application and secure access to apis.
In my case I just need to read a header to set user roles and I want Spring security to check user authorizations to secure access to my methods.
You gave the key with custom http.securityContextRepository(this.securityContextRepository);
in SecurityConfiguration (no need of a custom authenticationManager).
Thanks to this SecurityContextRepository I was able to build and set a custom authentication (simplified below).
@Override public Mono 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)); }
And thus I can secure my methods using these roles:
@Component public class MyService { @PreAuthorize("hasRole('ADMIN')") public Mono checkAdmin() { // my secure method } }