Recently, I tried to build a microservice GateWay in the company and chose Spring Cloud GateWay as the model. GateWay is a responsive programming (WebFlux) paradigm, so it is a little different from previous projects.

Gateway, I won’t go into details here. This article notes how WebFlux works with Spring Security and JWT.

Thinking to comb

After the previous study of MVC and Spring Security integration, we can summarize the following points:

  1. Authenticated entry. At this point, we need to convert the Body of the Request into an entity class and wrap it in a custom Security token.
  2. Authentication logic processing.
  3. Authentication entry and logic processing.
  4. Token processing.

To summarize briefly, it is mainly about authentication and authentication. Let’s get started.

Custom Security authentication token

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(a) {
        return this.credentials;
    }
 
    @Override
    public Object getPrincipal(a) {
        return this.principal;
    }
 
    public LoginData getLoginData(a) {
        return this.loginData;
    }
 
    public void setLoginData(LoginData loginData) {
        this.loginData = loginData;
    }
 
    @Override
    public boolean implies(Subject subject) {
        return false; }}Copy the code

Change the request mode to 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 -> {
                    // Read the request 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());
 
                    // Encapsulate a custom token for 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);
                    returnMono.just(myAuthToken); }); }}Copy the code

Authentication processing logic

/** * Extract user credentials from 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());
 
        / / access 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());
 
        // Build the user token
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setId(userId);
        myUserDetails.setUsername(username);
        myUserDetails.setRoleList(list);
 
        // Verify the validity of the token
        checkToken(token, userId);
 
        // Build the authentication credentials for Security
        MyAuthenticationToken authToken = new MyAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());
        log.debug("User information parsed from token: {}", myUserDetails);
 
        // Remove the token from the request header and add the parsed information
        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);
    }
Copy the code
@Component
@Primary
@Slf4j
public class MyAuthenticationManager implements ReactiveAuthenticationManager {
 
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private MyUserDetailsServiceImpl userDetailsService;
 
    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
 
        // Return the value
        if (authentication.isAuthenticated()) {
            return Mono.just(authentication);
        }
 
        // Convert to a custom security token
        MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
        log.debug("{}", myAuthenticationToken.toString());
 
        // Get the login parameters
        LoginData loginData = myAuthenticationToken.getLoginData();
        if (loginData == null) {
            throw new AuthenticationServiceException("Login parameters not obtained");
        }
        String loginType = loginData.getLoginType();
        if (StringUtils.isBlank(loginType)) {
            throw new AuthenticationServiceException("Login mode cannot be empty.");
        }
 
        // Get the user entity. Here is the logon logical implementation.
        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("User does not exist or password is incorrect")); }}else if (LoginType.PHONE_CODE.equals(loginType)) {
 
            this.checkPhoneVerifyCode(loginData.getPhone(), loginData.getPhoneVerifyCode());
            userDetails = userDetailsService.loadUserByPhone(loginData.getPhone());
 
        } else {
            throw new AuthenticationServiceException("Unsupported login mode");
        }
 
        MyAuthenticationToken authenticationToken = new MyAuthenticationToken(userDetails, myAuthenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
 
        return Mono.just(authenticationToken);
    }
Copy the code

Authentication processing logic

/** * Authorizes the logic processing center */
@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);
 
        // Get the list of roles accessible to the current path from 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());
 
        // The user who passes the authentication and matches the role can access the current path
        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());
    }
Copy the code

Uncertified processor

/** * Not authenticated processing processor */
@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()));
                    returnresponse.writeWith(Mono.just(buffer)); }); }}Copy the code

Certified success processor

/** * Successfully authenticates processor *@DescAfter a successful login, operations such as token generation are performed. *@Author DaMai
* @Date2021/3/23 15:26 * But do good, don't ask the future. * /
@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();
 
                    // Generate 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());
 
                    // Assemble the return parameters
                    UserLoginVO result = new UserLoginVO();
                    UserInfoVO userInfo = new UserInfoVO();
                    BeanUtils.copyProperties(userDetails, userInfo);
                    result.setUserInfo(userInfo);
                    result.setToken(token);
 
                    / / to 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);
 
                    returnresponse.writeWith(Mono.just(dataBuffer)); })); }}Copy the code

Authentication failure handler

@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);
 
            // The account does not exist
            if (exception instanceof UsernameNotFoundException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_NOT_EXIST);
                // The user name or password is incorrect
            } else if (exception instanceof BadCredentialsException) {
                result = ResultVO.error(ResultEnum.LOGIN_PASSWORD_ERROR);
                // The account has expired
            } else if (exception instanceof AccountExpiredException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_EXPIRED);
                // The account has been locked
            } else if (exception instanceof LockedException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_LOCKED);
                // User credentials are invalid
            } else if (exception instanceof CredentialsExpiredException) {
                result = ResultVO.error(ResultEnum.ACCOUNT_CREDENTIAL_EXPIRED);
                // The account has been disabled
            } 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);
            returnresponse.writeWith(Mono.just(dataBuffer)); })); }}Copy the code

Description Failed authentication processor

/** * Authentication error handler */
@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()));
                    returnresponse.writeWith(Mono.just(buffer)); }); }}Copy the code

Logout processor

@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();
        // Define the return value
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSONObject.toJSONString(ResultVO.success()).getBytes());
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 
        // Convert to a custom security token
        MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
        MyUserDetails userDetails = (MyUserDetails) myAuthenticationToken.getPrincipal();
 
        / / delete the token
        redisTemplate.opsForHash().delete(AuthConstant.TOKEN_REDIS_KEY, userDetails.getId());
        log.info("Successful logout: {}", myAuthenticationToken.toString());
 
        returnresponse.writeWith(Mono.just(dataBuffer)); }}Copy the code

The total configuration

/** * Security core configuration class *@DescConfigure security * in detail here@Author DaMai
* @Date2021/3/23 15:26 * But do good, don't ask the future. * /
@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));
                    // Filter urls that do not need to be blocked
                    exchange.pathMatchers(pattern).permitAll()
                            // Intercept authentication
                            .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(a) {
        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;
    }
 
    /** * User information verification manager, can be added according to the demand in order to execute */
    @Bean
    ReactiveAuthenticationManager reactiveAuthenticationManager(a) {
        LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
        managers.add(myAuthenticationManager);
        return new DelegatingReactiveAuthenticationManager(managers);
    }
Copy the code