【问题标题】:Spring webflux custom authentication for API用于 API 的 Spring webflux 自定义身份验证
【发布时间】:2018-05-01 10:31:20
【问题描述】:

我正在为 Angular 5 应用程序创建 API。我想使用 JWT 进行身份验证。
我想使用 spring security 提供的功能,以便我可以轻松地使用角色。

我设法禁用了基本身份验证。但是当使用http.authorizeExchange().anyExchange().authenticated(); 时,我仍然会收到登录提示。
我只想给出 403 而不是提示。因此,通过检查 Authorization 标头中的令牌的“事物”(它是过滤器吗?)覆盖登录提示。

我只想在将返回 JWT 令牌的控制器中进行登录。但是我应该使用什么 spring 安全 bean 来检查用户凭据?我可以构建自己的服务和存储库,但我想尽可能使用 Spring Security 提供的功能。

这个问题的简短版本只是:
如何自定义spring security的身份验证?
我必须创建什么 bean?
我必须把配置放在哪里? (我现在有一个SecurityWebFilterChain的bean)

我能找到的关于在 webflux 中使用 Spring Security 进行身份验证的唯一文档是:https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux

【问题讨论】:

    标签: spring authentication spring-security spring-webflux


    【解决方案1】:

    对于那些有同样问题的人(Webflux + Custom Authentication + JWT),我使用AuthenticationWebFilter、自定义ServerAuthenticationConverterReactiveAuthenticationManager 解决了问题,希望可以在将来对某人有所帮助。 用最新版本测试(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();
        }
    

    @自动连线 private lateinit var userDetailsS​​ervice: ReactiveUserDetailsS​​ervice

    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:您在https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java 找到的 TokenProvider 类

    【讨论】:

    • 你能确认一下 JwtAuthenticationConverter 的转换方法被 AUTH_WHITELIST url 调用了吗?
    【解决方案2】:

    感谢 Jan,您的示例为我在 Spring Webflux 应用程序中自定义身份验证和安全访问 api 提供了很多帮助。
    在我的情况下,我只需要读取一个标题来设置用户角色,并且我希望 Spring 安全检查用户授权以保护对我的方法的访问。
    您在 SecurityConfiguration 中使用自定义 http.securityContextRepository(this.securityContextRepository); 提供了密钥(不需要自定义 authenticationManager)。

    感谢这个 SecurityContextRepository,我能够构建和设置自定义身份验证(如下所示)。

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

    因此我可以使用这些角色来保护我的方法:

    @Component
    public class MyService {
        @PreAuthorize("hasRole('ADMIN')")
        public Mono<String> checkAdmin() {
            // my secure method
       }
    }
    

    【讨论】:

      【解决方案3】:

      经过大量搜索和尝试,我想我找到了解决方案:

      您需要一个包含所有配置的SecurityWebFilterChain bean。
      这是我的:

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

      我已禁用 httpBasic、formLogin、csrf 和注销,以便进行自定义身份验证。

      通过设置 AuthenticationManagerSecurityContextRepository,我覆盖了默认的 spring 安全配置,以检查用户是否已通过请求的身份验证/授权。

      身份验证管理器:

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

      我不完全确定身份验证管理器的用途,但我认为是为了进行最终身份验证,所以在一切正常时设置authentication.setAuthenticated(true);

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

      在加载时,我将根据serverWebExchange 中的标头检查用户是否已通过身份验证。我使用https://github.com/jwtk/jjwt。无论用户是否通过身份验证,我都会返回不同类型的身份验证令牌。

      【讨论】:

      • 请注意:ServerSecurityContextRepository#load 返回 Mono&lt;SecurityContext&gt; 所以你应该返回 Mono.just(new SecurityContextImpl(authentication))
      • 我检查了ServerHttpSecurity.build() 方法的实现,ReactiveAuthenticationManager 仅在 HttpBasic 和 FromLogin 中使用,如果禁用它,它永远不会被调用。所以创建ReactiveAuthenticationManager 是没有意义的。如果你想使用它,你需要用你的ReactiveAuthenticationManager注册一个AuthenticationWebFilter。如果我错了,你可以纠正我。
      • 您的 AuthenticationToken 应该经过身份验证。您可以检查UsernamePasswordAuthenticationToken 中的代码,其中super.setAuthenticated(true); 在构造函数中。
      【解决方案4】:

      在我的旧项目中,我使用了这个配置:

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

      JWTAuthorizationFilter 在哪里:

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

      JWTAuthenticationFilter

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

      用户详细信息是经典的用户服务详细信息

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

      请注意我没有使用 Spring webflux

      希望有用

      安杰洛

      【讨论】:

      • 谢谢!但是 webflux 安全的工作方式有很大不同。但我确信我可以使用一些部分。
      猜你喜欢
      • 2017-03-17
      • 2019-02-07
      • 2016-08-04
      • 2014-04-20
      • 2014-12-13
      • 1970-01-01
      • 2011-03-13
      • 2016-03-15
      • 1970-01-01
      相关资源
      最近更新 更多