Spring webflux custom authentication for API

前端 未结 4 1205
囚心锁ツ
囚心锁ツ 2020-12-07 23:07

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

相关标签:
4条回答
  • 2020-12-07 23:48

    For those that have same issue(Webflux + Custom Authentication + JWT) I solved using AuthenticationWebFilter, custom ServerAuthenticationConverter and ReactiveAuthenticationManager, following the code hope could help someone in the future. Tested with latest version(spring-boot 2.2.4.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();
        }
    

    @Autowired private lateinit var userDetailsService: ReactiveUserDetailsService

    class CustomReactiveAuthenticationManager(userDetailsService: ReactiveUserDetailsService?) : UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService) {
    
        override fun authenticate(authentication: Authentication): Mono<Authentication> {
            return if (authentication.isAuthenticated) {
                Mono.just<Authentication>(authentication)
            } else super.authenticate(authentication)
        }
    }
    
    private fun responseError() : ServerAuthenticationFailureHandler{
        return ServerAuthenticationFailureHandler{ webFilterExchange: WebFilterExchange, _: AuthenticationException ->
            webFilterExchange.exchange.response.statusCode = HttpStatus.UNAUTHORIZED
            webFilterExchange.exchange.response.headers.addIfAbsent(HttpHeaders.LOCATION,"/")
            webFilterExchange.exchange.response.setComplete();
        }
    }
    
        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: The TokenProvider class you find at https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java

    0 讨论(0)
  • 2020-12-07 23:52

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

    And thus I can secure my methods using these roles:

    @Component
    public class MyService {
        @PreAuthorize("hasRole('ADMIN')")
        public Mono<String> checkAdmin() {
            // my secure method
       }
    }
    
    0 讨论(0)
  • 2020-12-07 23:58

    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

    0 讨论(0)
  • 2020-12-08 00:03

    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<Authentication> 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<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);
        }
    }
    

    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.

    0 讨论(0)
提交回复
热议问题