4、WebFlux中使用

分布式开发时,微服务会有很多,但是网关是请求的第一入口,所以一般会把客户端请求的权限验证统一放在网关进行认证与鉴权。SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

SpringMVC和WebFlux中Security概念对应

WebFlux SpringMVC
@EnableWebFluxSecurity @EnableWebSecurity
ReactiveSecurityContextHolder SecurityContextHolder
AuthenticationWebFilter FilterSecurityInterceptor
ReactiveAuthenticationManager AuthenticationManager
ReactiveUserDetailsService UserDetailsService
ReactiveAuthorizationManager AccessDecisionManager

使用

1、引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

2、自定义授权失败处理类

public class AccessDeniedHandlerImpl implements ServerAccessDeniedHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
        ServerHttpResponse response = serverWebExchange.getResponse();
        response.getHeaders().add("Content-Type","application/json;charset=utf-8");
        ResultVO<Object> result = ResultVO.result("无权限进行此操作", ResultCode.NOACCESS, null);
        String jsonString = JSON.toJSONString(result);
        DataBuffer wrap = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(wrap));
    }
}

3、自定义认证失败处理类

public class AuthenticationEntryPointImpl implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange serverWebExchange, AuthenticationException e) {
        ServerHttpResponse response = serverWebExchange.getResponse();
        response.getHeaders().add("Content-Type","application/json;charset=utf-8");
        ResultVO<Object> result = ResultVO.result("认证失败或已在别处登录,请进行登录", ResultCode.UNAUTHORIZED, null);
        String jsonString = JSON.toJSONString(result);
        DataBuffer wrap = response.bufferFactory().wrap(jsonString.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(wrap));
    }
}

4、获取请求头中带过来的token值,解析并认证用户信息

@Component
public class TokenSecurityContextRepository implements ServerSecurityContextRepository {

    @Autowired
    private RoleMapper roleMapper;

    /**
     * 由于无状态,所以不需要保存用户登录状态
     * @param serverWebExchange
     * @param securityContext
     * @return
     */
    @Override
    public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
        return Mono.empty();
    }

    /**
     * 对用户身份进行验证
     * @param exchange
     * @return
     */
    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        // 从请求头中获取jwt
        String authToken = exchange.getRequest().getHeaders().getFirst("login-token");
        // 如果请求头没有token,直接放行,交给下面的过滤器处理
        // 如果当前请求的接口需要认证或权限,下面的过滤器会拦截
        if (Objects.isNull(authToken) || !StringUtils.hasText(authToken)){
            return Mono.empty();
        }
        // 验证token,以及redis中是否存在该token
        if (JwtUtil.verifyToken(authToken)){
            Map<String, String> payload = JwtUtil.getPayload(authToken);
            String id = payload.get("id");
            String sub = payload.get("sub");
            String s = RedisFactory.select(0).opsForValue().get(sub);
            // 验证jwt中的id和redis中的id是否一致
            if (!Objects.isNull(s) && s.equals(id)) {
                //完成续签,刷新token的有效时间
                RedisFactory.select(0).expire(sub, 100000, TimeUnit.SECONDS);
                //获取用户权限信息
                List<String> list = roleMapper.selectUserRolesByEmail(sub);
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for (String authority : list){
                    authorities.add(new SimpleGrantedAuthority(authority));
                }
                SecurityContext securityContext = new SecurityContextImpl();
                // 创建一个认证对象,第一个参数为用户信息,第三个参数为用户权限
                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sub,null,authorities);
                // 传入认证对象到SecurityContext中,证明该用户已经认证
                securityContext.setAuthentication(token);
                return Mono.justOrEmpty(securityContext);
            }
        }
        return Mono.empty();
    }
}

5、SercurityConf配置类

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig{

    @Autowired
    private TokenSecurityContextRepository tokenSecurityContextRepository;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf().disable()
                .formLogin().disable()
                .logout().disable()
                .securityContextRepository(tokenSecurityContextRepository)
                .authorizeExchange()
                .pathMatchers(HttpMethod.GET).permitAll() // 放开所有get请求
                .pathMatchers("/auth-server/user/login").permitAll() // 放开账密登录请求
                .anyExchange().authenticated() //其余请求都需要验证
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new AuthenticationEntryPointImpl()) //授权失败处理器
                .accessDeniedHandler(new AccessDeniedHandlerImpl()); // 验证失败处理器
        return http.build();
    }
}