Spring WebFlux 支持 Spring Security 实现 JWT 鉴权

移动端项目,鉴权需求比较简单,Spring Cloud Gateway 只做 JWT 校验及角色鉴权,登录之类的全部是自定义处理的,微服务间传递 JWS 达到传递凭证的目的,下游服务无需鉴权也不依赖 Spring Security,需要当前用户的代码直接解析 JWS 获取当前用户

还有就是 Spring Security 5.4 + WebSecurityConfigurerAdapter 已被标记为弃用,非 reactive 项目也开始推荐使用 Bean 注入了,所以下面的代码也可以算是 5.4 + 的迁移指南了

核心代码:

JwsService 提供 JWS 的签名与校验,返回对应的 JwsPayload 对象,JwsPayload 为 POJO,提供当前 principal,包含一个 List<Authority> authoritys 属性,Authority 为 POJO,因为放在了 lib 模块中,没有引入 Spring Security 依赖,直接继承自 Object,也可以看到下面代码手动进行转换为 GrantedAuthority

package com.seliote.bubble.gwsvr.security;

import com.seliote.bubble.svrlib.config.JwsProps;
import com.seliote.bubble.svrlib.service.JwsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * JWS Filter
 * If authorization header exists, will set to security context
 *
 * @author seliote
 * @since 2022-07-06
 */
@Slf4j
@Component
public class JwsFilter implements WebFilter {

    private final JwsService jwsService;
    private final String headerName;

    @Autowired
    public JwsFilter(JwsService jwsService, JwsProps jwsProps) {
        this.jwsService = jwsService;
        this.headerName = jwsProps.getHeader();
    }

    @NonNull
    @Override
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        var header = exchange.getRequest().getHeaders().getFirst(headerName);
        var payloadOpt = jwsService.verify(header);
        if (payloadOpt.isPresent() && payloadOpt.get().available()) {
            var payload = payloadOpt.get();
            List<? extends GrantedAuthority> authorities = new ArrayList<>();
            if (payload.getAuthorities() != null && payload.getAuthorities().size() != 0) {
                authorities = payload.getAuthorities().stream().map(r -> (GrantedAuthority) r::getAuthority).toList();
            }
            var authentication = new UsernamePasswordAuthenticationToken(payload, null, authorities);
            log.trace("Set security context {}", payload);
            return chain.filter(exchange)
                    .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
        }
        return chain.filter(exchange);
    }
}

因为是完全自定义去实现登录之类的操作,所以提供了一个空的 ReactiveAuthenticationManager,对应 MVC 里默认实现的 authenticationManager()

package com.seliote.bubble.gwsvr.security;

import com.seliote.bubble.svrlib.domain.Authority;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;

/**
 * Security context config
 *
 * @author seliote
 * @since 2022-07-06
 */
@Slf4j
@EnableWebFluxSecurity
public class SecurityConfig {

    private final JwsFilter jwsFilter;

    @Autowired
    public SecurityConfig(JwsFilter jwsFilter) {
        this.jwsFilter = jwsFilter;
    }

    @Bean
    public ReactiveAuthenticationManager authenticationManager() {
        return authentication -> Mono.empty();
    }

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
        final var permitAll = new String[]{"/user-svr/country/list"};
        return http.csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .authorizeExchange().pathMatchers(permitAll).permitAll()
                .pathMatchers("/**").hasAuthority(Authority.USER)
                .anyExchange().authenticated()
                .and().addFilterAt(jwsFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .build();
    }
}
posted @ 2022-07-08 23:29  seliote  阅读(414)  评论(0)    收藏  举报