本文场景: 使用Spring Security做权限控制,系统包含普通用户和系统管理员两种类型,希望有不同的登录入口;并且在Spring Gateway上配置,Gateway使用的是WebFlux,无法兼容MVC,所以使用WebFlux配置Security。纯记录,目前项目还是小demo,贴的示例代码可以正常运行,但是业务逻辑还有点问题。

1. SecurityWebFilterChain配置

  1. 创建两个SecurityWebFilterChainbean,管理员配置限制范围为/manage/**,用户将该路径设置为白名单,权限配置不限制范围。

  2. 管理员配置必须指定比用户配置更小的@Order,使其具有更高的优先级,能够在用户的SecurityWebFilterChain之前被执行,因为一旦匹配到用户的SecurityWebFilterChain,将不会继续匹配到管理员的配置,管理员的配置将不会生效。具体原因上一个节有分析。

package com.nineya.rkproblem.config;

import com.nineya.rkproblem.filter.ManageAuthenticationFilter;
import com.nineya.rkproblem.security.AuthenticationFailureHandler;
import com.nineya.rkproblem.security.AuthenticationSuccessHandler;
import com.nineya.rkproblem.security.ManageAuthenticationManager;
import com.nineya.rkproblem.security.UserAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;

import javax.annotation.Resource;
import java.util.LinkedList;

/**
 * @author 殇雪话诀别
 * 2020/11/7
 */
@Configuration
@EnableWebFluxSecurity
public class SecurityWebFluxConfiguration {
    /**
     * 登录成功的处理器
     */
    @Resource
    private AuthenticationSuccessHandler successHandler;
    /**
     * 登录失败的处理器
     */
    @Resource
    private AuthenticationFailureHandler failureHandler;
    @Resource
    private UserAuthenticationManager userAuthenticationManager;
    @Resource
    private ManageAuthenticationManager manageAuthenticationManager;
    @Resource
    private ManageAuthenticationFilter manageAuthenticationFilter;
    /**
     * 白名单
     */
    private static final String[] AUTH_WHITELIST = new String[]{"/manage/login", "/user/login"};

    @Bean
    @Order(0)
    public SecurityWebFilterChain manageSecurityFilterChain(ServerHttpSecurity http){
        http
                .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/**"))
                .formLogin(n->n
                        .loginPage("/manage/login")
                        .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/auth/login"))
                        .authenticationManager(manageAuthenticationManager)
                        .authenticationSuccessHandler(successHandler)
                        .authenticationFailureHandler(failureHandler))
                .logout(n->n.logoutUrl("/manage/logout"))
                .csrf(n->n.disable())
                .httpBasic(n->n.disable())
                .authorizeExchange(n->n
                        .pathMatchers(AUTH_WHITELIST).permitAll()
                        .pathMatchers("/manage/api/**").hasRole("MANAGE")
                        .anyExchange().authenticated())
                .addFilterBefore(manageAuthenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
        return http.build();
    }

    @Bean
    @Order(1)
    public SecurityWebFilterChain userSecurityFilterChain(ServerHttpSecurity http){
        http
                .securityMatcher(ServerWebExchangeMatchers.anyExchange())
                .formLogin(n->n
                        .loginPage("/user/login")
                        .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/user/auth/login"))
                        .authenticationManager(userAuthenticationManager)
                        .authenticationSuccessHandler(successHandler)
                        .authenticationFailureHandler(failureHandler))
                .logout(n->n.logoutUrl("/user/logout"))
                .csrf(n->n.disable())
                .httpBasic(n->n.disable())
                .authorizeExchange(n->n
                        .pathMatchers("/manage/**", "/user/login").permitAll()
                        .pathMatchers("/user/api/**").hasRole("USER"));
        return http.build();
    }

    @Bean
    public PasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

2. AuthenticationManager配置

两个登录入口需要创建两个AuthenticationManager,本文只是简单示例,能正常跑起来,逻辑不一定通的哦。

package com.nineya.rkproblem.security;

import com.nineya.rkproblem.service.ManageDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author 殇雪话诀别
 * 2020/11/15
 */
@Component
@Primary
public class ManageAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
    private Scheduler scheduler = Schedulers.boundedElastic();
    @Autowired
    private ManageDetailsServiceImpl manageDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());
        return retrieveUser(username)
                .publishOn(scheduler)
                .filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword())))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("账号或密码错误!"))))
                .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return manageDetailsService.findByUsername(username);
    }
}

package com.nineya.rkproblem.security;

import com.nineya.rkproblem.service.ManageDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * @author 殇雪话诀别
 * 2020/11/8
 */
@Component
public class UserAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {

    private Scheduler scheduler = Schedulers.boundedElastic();
    @Autowired
    private ManageDetailsServiceImpl manageDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());
        return retrieveUser(username)
                .publishOn(scheduler)
                .filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword())))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));
    }

    @Override
    protected Mono<UserDetails> retrieveUser(String username) {
        return manageDetailsService.findByUsername(username);
    }
}

3. 用户信息服务

AuthenticationManager中需要指定一个ReactiveUserDetailsService用于查询用户信息,从而比对用户的登录数据是否正确,实现如下:

package com.nineya.rkproblem.service;

import com.nineya.rkproblem.client.ManageClient;
import com.nineya.rkproblem.entity.Manage;
import com.nineya.tool.text.CheckText;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @author 殇雪话诀别
 * 2020/11/8
 */
@Component
public class ManageDetailsServiceImpl implements ReactiveUserDetailsService {
    @Autowired
    private ManageClient manageClient;

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        if (!CheckText.checkMail(username)){
            return Mono.error(new UsernameNotFoundException(String.format("邮箱 %s 格式错误!", username)));
        }
        Manage manage = manageClient.login(username);
        if (manage == null){
            return Mono.error(new UsernameNotFoundException(String.format("账号 %s 不存在!", username)));
        }
        UserDetails user = User.withUsername(username)
                .password(manage.getPassword())
                .roles("MANAGE")
                .build();
        return Mono.just(user);
    }
}

4. 登录结果处理器

在本文场景中,登录结果处理和业务无关,所以管理员和用户共用一个处理器。

登录失败处理器:

package com.nineya.rkproblem.security;

import com.alibaba.fastjson.JSONObject;
import com.nineya.rkproblem.entity.ResponseResult;
import com.nineya.rkproblem.entity.ResultCode;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @author 殇雪话诀别
 * 2020/11/12
 */
@Component
public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        ResponseResult result = new ResponseResult().setError(true);
        if (exception instanceof UsernameNotFoundException) {
            // 用户不存在
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else if (exception instanceof BadCredentialsException) {
            // 密码错误
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else if (exception instanceof LockedException) {
            // 用户被锁
            result.setCode(ResultCode.SUCCESS.getCode());
            result.setMessage(exception.getMessage());
        } else {
            // 系统错误
            result.setCode(ResultCode.SERVER_ERROR.getCode());
            result.setMessage(exception.getMessage());
        }
        String body = JSONObject.toJSONString(result);
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

登录成功处理器,在此处可实现通过Authentication中的信息生成token响应给客户端,实现自定义token,可以关闭Security默认的Session方案。但是如果此处自定义token,需要添加一个token验证的过滤器,在每次请求时解析自定义的token创建Authentication

package com.nineya.rkproblem.security;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.nineya.rkproblem.entity.ResponseResult;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.stream.Collectors;

/**
 * @author 殇雪话诀别
 * 2020/11/12
 */
@Component
public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.setStatusCode(HttpStatus.OK);
        Algorithm algorithm = Algorithm.HMAC256("secret");
        String token = JWT.create()
                .withClaim("uid", "123456")
                .withIssuedAt(new Date())
                .withIssuer("rkproblem")
                .sign(algorithm);
        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        DecodedJWT jwt = verifier.verify(token);
        System.out.println(jwt.getClaims().entrySet().stream()
                .map(n->n.getKey()+" = " + n.getValue().asString()).collect(Collectors.joining(", ")));
        ResponseResult<String> result = ResponseResult.success(token);
        String body = JSONObject.toJSONString(result);

        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }
}

5. 权限认证过滤器

示例中,简单的打印了一下token,并未将token转换为AuthenticationToken对象。

package com.nineya.rkproblem.filter;

import org.springframework.http.HttpHeaders;
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;

/**
 * 将表单里的参数转为AuthenticationToken对象
 *
 * @author 殇雪话诀别
 * 2020/11/20
 */
@Component
public class ManageAuthenticationFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
        HttpHeaders headers = serverWebExchange.getRequest().getHeaders();
        String manageToken = headers.getFirst("MANAGE_TOKEN");
        System.out.println(manageToken);
        return webFilterChain.filter(serverWebExchange);
    }
}