『Spring Security』(九) SpringSecurity与WebFlux集成

·  阅读 1468

最近在公司中尝试搭建微服务网关,选型为Spring Cloud GateWay,GateWay是响应式编程(WebFlux)范式的,所以与以往项目有一点不同。

网关嘛,这里不细说。本篇记录WebFlux 如何与 Spring Security 、JWT 结合。

思路梳理

经过前面 MVC 与 Spring Security 集成的学习,我们可以归纳出一下要点:

  1. 认证的入口。在此处,我们需要把 Request 中 Body,转化为实体类,并封装到自定义 Security 令牌中。
  2. 认证逻辑处理。
  3. 鉴权入口、鉴权逻辑处理。
  4. Token 的处理。

简单归纳一下,主要也就是认证和鉴权两方面。下面正式开搞。

自定义 Security 认证令牌

public class MyAuthenticationToken extends AbstractAuthenticationToken {
 
    private final Object principal;
    private final Object credentials;
    private LoginData loginData;
 
    public MyAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
    }
 
    public MyAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }
 
    public MyAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials, LoginData loginData) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        this.loginData = loginData;
    }
 
    @Override
    public Object getCredentials() {
        return this.credentials;
    }
 
    @Override
    public Object getPrincipal() {
        return this.principal;
    }
 
    public LoginData getLoginData() {
        return this.loginData;
    }
 
    public void setLoginData(LoginData loginData) {
        this.loginData = loginData;
    }
 
    @Override
    public boolean implies(Subject subject) {
        return false;
    }
}
复制代码

修改为Post,application/json 请求方式

@Slf4j
@Component
public class MyAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
 
    @Override
    public Mono<Authentication> convert(ServerWebExchange exchange) {
 
        HttpMethod method = exchange.getRequest().getMethod();
        MediaType contentType = exchange.getRequest().getHeaders().getContentType();
 
        return exchange
                .getRequest()
                .getBody()
                .next()
                .flatMap(body -> {
                    // 读取请求体
                    LoginData loginData = new LoginData();
                    try {
                        loginData = JSONObject.parseObject(body.asInputStream(), LoginData.class, Feature.OrderedField);
                    } catch (IOException e) {
                        return Mono.error(new AuthenticationServiceException("Error while parsing credentials"));
                    }
 
                    log.debug(loginData.toString());
 
                    // 封装 security 的自定义令牌
                    String username = loginData.getUsername();
                    String password = loginData.getPassword();
                    username = username == null ? "" : username;
                    username = username.trim();
                    password = password == null ? "" : password;
 
                    MyAuthenticationToken myAuthToken = new MyAuthenticationToken(username, password);
                    myAuthToken.setLoginData(loginData);
                    return Mono.just(myAuthToken);
                });
    }
}
复制代码

认证处理逻辑

/**
* 从 token 中提取用户凭证
*/
@Component
@Slf4j
public class MySecurityContextRepository implements ServerSecurityContextRepository {
 
    @Resource
    private MyAuthenticationManager myAuthenticationManager;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        return Mono.empty();
    }
 
    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
 
        log.debug("{}", exchange.toString());
 
        // 获取 token
        HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
        String authorization = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.isBlank(authorization)) {
            return Mono.empty();
        }
 
        // 解析 token
        String token = authorization.substring(AuthConstant.TOKEN_HEAD.length());
        if (StringUtils.isBlank(token)) {
            return Mono.empty();
        }
 
        Claims claims = JwtUtil.getClaims(token);
        String username = claims.getSubject();
        String userId = claims.get(AuthConstant.USER_ID_KEY, String.class);
        String rolesStr = claims.get(AuthConstant.ROLES_STRING_KEY, String.class);
        List<AuthRole> list = Arrays.stream(rolesStr.split(","))
                .map(roleName -> new AuthRole().setName(roleName))
                .collect(Collectors.toList());
 
        // 构建用户令牌
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setId(userId);
        myUserDetails.setUsername(username);
        myUserDetails.setRoleList(list);
 
        // 确认 token 有效性
        checkToken(token, userId);
 
        // 构建 Security 的认证凭据
        MyAuthenticationToken authToken = new MyAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());
        log.debug("从 token 中解析出的用户信息:{}", myUserDetails);
 
        // 从请求头中删除token,并添加解析出来的信息
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header(AuthConstant.USER_ID_KEY, userId)
                .header(AuthConstant.USERNAME_KEY, username)
                .header(AuthConstant.ROLES_STRING_KEY, rolesStr)
                .headers(headers -> headers.remove(HttpHeaders.AUTHORIZATION))
                .build();
        exchange.mutate().request(request).build();
 
        return myAuthenticationManager
                .authenticate(authToken)
                .map(SecurityContextImpl::new);
    }
复制代码
@Component
@Primary
@Slf4j
public class MyAuthenticationManager implements ReactiveAuthenticationManager {
 
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private MyUserDetailsServiceImpl userDetailsService;
 
    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
 
        // 已经通过验证,直接返回
        if (authentication.isAuthenticated()) {
            return Mono.just(authentication);
        }
 
        // 转换为自定义security令牌
        MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
        log.debug("{}", myAuthenticationToken.toString());
 
        // 获取登录参数
        LoginData loginData = myAuthenticationToken.getLoginData();
        if (loginData == null) {
            throw new AuthenticationServiceException("未获取到登陆参数");
        }
        String loginType = loginData.getLoginType();
        if (StringUtils.isBlank(loginType)) {
            throw new AuthenticationServiceException("登陆方式不可为空");
        }
 
        // 获取用户实体。此处为登录方式的逻辑实现。
        UserDetails userDetails;
        if (LoginType.USERNAME_CODE.equals(loginType)) {
 
            this.checkVerifyCode(loginData.getUsername(), loginData.getCommonLoginVerifyCode());
            userDetails = userDetailsService.loadByUsername(loginData.getUsername());
            if (!passwordEncoder.matches(loginData.getPassword(), userDetails.getPassword())) {
                return Mono.error(new BadCredentialsException("用户不存在或者密码错误"));
            }
 
        } else if (LoginType.PHONE_CODE.equals(loginType)) {
 
            this.checkPhoneVerifyCode(loginData.getPhone(), loginData.getPhoneVerifyCode());
            userDetails = userDetailsService.loadUserByPhone(loginData.getPhone());
 
        } else {
            throw new AuthenticationServiceException("不支持的登陆方式");
        }
 
        MyAuthenticationToken authenticationToken = new MyAuthenticationToken(userDetails, myAuthenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
 
        return Mono.just(authenticationToken);
    }
复制代码

鉴权处理逻辑

/**
* 授权逻辑处理中心
*/
@Component
@Slf4j
public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
 
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
 
        log.debug("{}", authentication.toString());
        ServerWebExchange exchange = authorizationContext.getExchange();
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        log.debug(path);
 
        // 从redis中获取当前路径可访问的角色列表
        Object obj = redisTemplate.opsForHash().get(AuthConstant.ROLES_REDIS_KEY, path);
        List<String> needAuthorityList = JSONObject.parseArray(JSONObject.toJSONString(obj), String.class);
        needAuthorityList = needAuthorityList.stream().map(role -> role = AuthConstant.ROLE_PRE + role).collect(Collectors.toList());
 
        //认证通过且角色匹配的用户可访问当前路径
        return authentication
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(auth -> {
                    log.debug(auth.getAuthorities().toString());
                    return auth.getAuthorities();
                } )
                .map(GrantedAuthority::getAuthority)
                .any(needAuthorityList::contains)
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }
 
    @Override
    public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
        return check(authentication, object)
                .filter(AuthorizationDecision::isGranted)
                .switchIfEmpty(Mono.defer(() -> {
                    String body = JSONObject.toJSONString(ResultVO.error(SimpleResultEnum.PERMISSION_DENIED));
                    return Mono.error(new AccessDeniedException(body));
                }))
                .flatMap(d -> Mono.empty());
    }
复制代码

未认证处理器

/**
* 未认证处理处理器
*/
@Component
public class MyAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
 
    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
        return Mono.defer(() -> Mono.just(exchange.getResponse()))
                .flatMap(response -> {
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                    DataBufferFactory dataBufferFactory = response.bufferFactory();
                    String result = JSONObject.toJSONString(ResultVO.error(SimpleResultEnum.PERMISSION_DENIED));
                    DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
                            Charset.defaultCharset()));
                    return response.writeWith(Mono.just(buffer));
                });
    }
}
复制代码

认证成功处理器

/**
* 认证成功处理器
* @Desc 在此进行登录成功后,生成 token 等操作。
* @Author DaMai
* @Date 2021/3/23 15:26
* 但行好事,莫问前程。
*/
@Component
public class MyAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
 
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        return Mono.defer(() -> Mono
                .just(webFilterExchange.getExchange().getResponse())
                .flatMap(response -> {
                    DataBufferFactory dataBufferFactory = response.bufferFactory();
 
                    // 生成JWT token
                    Map<String, Object> map = new HashMap<>();
                    MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
                    map.put(AuthConstant.USER_ID_KEY, userDetails.getId());
                    map.put(AuthConstant.USERNAME_KEY, userDetails.getUsername());
                    String rolesStr = userDetails.getRoleList().stream().map(AuthRole::getName).collect(Collectors.joining(","));
                    map.put(AuthConstant.ROLES_STRING_KEY, rolesStr);
                    String token = JwtUtil.createToken(map, userDetails.getUsername());
 
                    // 组装返回参数
                    UserLoginVO result = new UserLoginVO();
                    UserInfoVO userInfo = new UserInfoVO();
                    BeanUtils.copyProperties(userDetails, userInfo);
                    result.setUserInfo(userInfo);
                    result.setToken(token);
 
                    // 存到redis
                 redisTemplate.opsForHash().put(AuthConstant.TOKEN_REDIS_KEY,userDetails.getId(),token);
                    DataBuffer dataBuffer =dataBufferFactory.wrap(JSONObject.toJSONString(ResultVO.success(result)).getBytes());
                    response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 
                    return response.writeWith(Mono.just(dataBuffer));
                }));
    }
}
复制代码

认证失败处理器

@Component
@Slf4j
public class MyAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
 
    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        return Mono.defer(() -> Mono.just(webFilterExchange.getExchange().getResponse()).flatMap(response -> {
 
            log.debug(response.toString());
 
            DataBufferFactory dataBufferFactory = response.bufferFactory();
            ResultVO<Object> result = ResultVO.error(ResultEnum.GATEWAY_SYS_ERROR);
 
            // 账号不存在
            if (exception instanceof UsernameNotFoundException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_NOT_EXIST);
                // 用户名或密码错误
            } else if (exception instanceof BadCredentialsException) {
                result = ResultVO.error(ResultEnum.LOGIN_PASSWORD_ERROR);
                // 账号已过期
            } else if (exception instanceof AccountExpiredException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_EXPIRED);
                // 账号已被锁定
            } else if (exception instanceof LockedException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_LOCKED);
                // 用户凭证已失效
            } else if (exception instanceof CredentialsExpiredException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_CREDENTIAL_EXPIRED);
                // 账号已被禁用
            } else if (exception instanceof DisabledException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_DISABLE);
            } else if (exception instanceof AuthenticationServiceException) {
                result.setMsg(exception.getMessage());
            }
 
            DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(result).getBytes());
            response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
            return response.writeWith(Mono.just(dataBuffer));
        }));
    }
}
复制代码

鉴权失败处理器

/**
* 鉴权错误处理器
*/
@Component
public class MyAccessDeniedHandler implements ServerAccessDeniedHandler {
 
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
 
        return Mono
                .defer(() -> Mono.just(exchange.getResponse()))
                .flatMap(response -> {
                    response.setStatusCode(HttpStatus.OK);
                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                    DataBufferFactory dataBufferFactory = response.bufferFactory();
                    String result = JSONObject.toJSONString(ResultVO.error(SimpleResultEnum.PERMISSION_DENIED));
                    DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
                            Charset.defaultCharset()));
                    return response.writeWith(Mono.just(buffer));
                });
 
    }
}
复制代码

登出处理器

@Component
@Slf4j
public class MyLogoutSuccessHandler implements ServerLogoutSuccessHandler {
 
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    @Override
    public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
        ServerHttpResponse response = exchange.getExchange().getResponse();
        // 定义返回值
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSONObject.toJSONString(ResultVO.success()).getBytes());
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 
        // 转换为自定义security令牌
        MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
        MyUserDetails userDetails = (MyUserDetails) myAuthenticationToken.getPrincipal();
 
        // 删除 token
        redisTemplate.opsForHash().delete(AuthConstant.TOKEN_REDIS_KEY, userDetails.getId());
        log.info("登出成功:{}", myAuthenticationToken.toString());
 
        return response.writeWith(Mono.just(dataBuffer));
    }
}
复制代码

总配置

/**
* security 核心配置类
* @Desc 在此详细配置 security
* @Author DaMai
* @Date 2021/3/23 15:26
* 但行好事,莫问前程。
*/
@EnableWebFluxSecurity
@Slf4j
public class SecurityConfig {
 
    @Resource
    private MyAuthorizationManager myAuthorizationManager;
 
    @Resource
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
 
    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
 
    @Resource
    private MyAuthenticationManager myAuthenticationManager;
 
    @Resource
    private MySecurityContextRepository mySecurityContextRepository;
 
    @Resource
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
 
    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;
 
    @Resource
    private MyAuthenticationConverter myAuthenticationConverter;
 
    @Resource
    private MyLogoutSuccessHandler myLogoutSuccessHandler;
 
    @Autowired
    SecurityUrlsConfig urlsConfig;
 
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
 
        httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .securityContextRepository(mySecurityContextRepository)
                .authorizeExchange(exchange -> {
 
                    List<String> urlList = urlsConfig.getIgnoreUrls();
                    String[] pattern = urlList.toArray(new String[urlList.size()]);
                    log.debug("securityWebFilterChain ignoreUrls:" + Arrays.toString(pattern));
                    // 过滤不需要拦截的url
                    exchange.pathMatchers(pattern).permitAll()
                            // 拦截认证
                            .pathMatchers(HttpMethod.OPTIONS).permitAll()
                            .anyExchange().access(myAuthorizationManager);
                })
                .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .logout().logoutSuccessHandler(myLogoutSuccessHandler)
        ;
        return httpSecurity.build();
    }
 
    private AuthenticationWebFilter authenticationWebFilter() {
        AuthenticationWebFilter filter = new AuthenticationWebFilter(reactiveAuthenticationManager());
 
        filter.setSecurityContextRepository(mySecurityContextRepository);
        filter.setServerAuthenticationConverter(myAuthenticationConverter);
        filter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        filter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login")
        );
 
        return filter;
    }
 
    /**
     * 用户信息验证管理器,可按需求添加多个按顺序执行
     */
    @Bean
    ReactiveAuthenticationManager reactiveAuthenticationManager() {
        LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
        managers.add(myAuthenticationManager);
        return new DelegatingReactiveAuthenticationManager(managers);
    }
复制代码
分类:
后端
标签:
分类:
后端
标签: