Spring webflux custom authentication for API

匿名 (未验证) 提交于 2019-12-03 01:48:02

问题:

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    } } 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!