Spring WebFlux自定义API身份验证

30

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

我成功禁用了基本身份验证。 但是,在使用http.authorizeExchange().anyExchange().authenticated();时仍然会得到登录提示。
我想只给出403而不是提示。 所以要通过“某物”(是过滤器吗?)覆盖登录提示,检查Authorization标头的令牌。

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

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

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

4个回答

38

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

您需要一个包含所有配置的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和logout,以便进行自定义身份验证。

通过设置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。如果用户已经进行了身份验证,我会返回一种不同的身份验证令牌。


1
请注意:ServerSecurityContextRepository#load返回Mono<SecurityContext>,因此您应该返回Mono.just(new SecurityContextImpl(authentication)) - Saljack
2
我检查了ServerHttpSecurity.build()方法的实现,发现ReactiveAuthenticationManager仅在HttpBasic和FromLogin中使用,如果您禁用它,则永远不会被调用。因此,创建一个ReactiveAuthenticationManager是没有意义的。如果您想要使用它,您需要注册一个带有您的ReactiveAuthenticationManagerAuthenticationWebFilter。如果我有错误,请纠正我。 - Saljack
您的AuthenticationToken应该得到验证。您可以在UsernamePasswordAuthenticationToken中检查代码,其中在构造函数中有super.setAuthenticated(true); - Saljack

12

对于那些遇到相同问题(Webflux + 自定义身份验证 + 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();
    }

@Autowired private lateinit var userDetailsService: ReactiveUserDetailsService

@ Autowired是一个Spring框架的注解,用于自动装配bean。在这个例子中,它将ReactiveUserDetailsService类型的bean注入到userDetailsService变量中。lateinit关键字表示该变量将在稍后的时间点进行初始化。

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

注意:您可以在https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java找到TokenProvider类。


请确认 JwtAuthenticationConverter 的转换方法是否在 AUTH_WHITELIST urls 上被调用了? - user3454581

3
感谢Jan的示例,它帮助我在Spring Webflux应用程序中自定义认证并安全访问API方面提供了很大的帮助。
在我的情况下,我只需要读取一个标头来设置用户角色,并希望Spring Security检查用户授权以保护对我的方法的访问。
你在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
   }
}

-2
在我的旧项目中,我使用了这个配置:
@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。
希望这对你有用。
安吉洛。

2
谢谢!但是WebFlux安全性工作方式有很大不同...但我相信我可以使用其中的一些部分。 - Jan Wytze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接