"Spring Security" (9) Integration of Spring Security and WebFlux

"Spring Security" (9) Integration of Spring Security and WebFlux

Recently, I tried to build a microservice gateway in the company, and the selection was Spring Cloud GateWay. 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 records how WebFlux is combined with Spring Security and JWT.

Ideas combing

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

  1. Certified entrance. Here, we need to convert the Body in the Request into an entity class and encapsulate it in a custom Security token.
  2. Authentication logic processing.
  3. Authentication entrance, authentication logic processing.
  4. Token processing.

To summarize briefly, the main aspects are authentication and authentication. The following is officially 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 () { 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 ; } } Copy code

Modified to Post, application/json request method

@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()); //Custom token that encapsulates 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); }); } } Copy 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()); //Get token HttpHeaders httpHeaders = exchange.getRequest().getHeaders(); String authorization = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION); if (StringUtils.isBlank(authorization)) { return Mono.empty(); } //parse 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()); //Construct a user token MyUserDetails myUserDetails = new MyUserDetails(); myUserDetails.setId(userId); myUserDetails.setUsername(username); myUserDetails.setRoleList(list); //Confirm the validity of the token checkToken(token, userId); //Construct Security authentication credentials 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 code
@Component @Primary @ SLF4J public class MyAuthenticationManager the implements ReactiveAuthenticationManager { @Autowired private PasswordEncoder passwordEncoder; @Autowired private MyUserDetailsServiceImpl userDetailsService; @Override public Mono<Authentication> authenticate (Authentication authentication) { //Has passed the verification, return directly if (authentication.isAuthenticated()) { return Mono.just(authentication); } //Convert to a custom security token MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication; log.debug( "{}" , myAuthenticationToken.toString()); //Get login parameters LoginData loginData = myAuthenticationToken.getLoginData(); if (loginData == null ) { throw new AuthenticationServiceException( "Login parameters were not obtained" ); } String loginType = loginData.getLoginType(); if (StringUtils.isBlank(loginType)) { throw new AuthenticationServiceException( "The login method cannot be empty" ); } //Get user entity. Here is the logical realization of the login method. 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 wrong" )); } } else if (LoginType.PHONE_CODE.equals(loginType)) { this .checkPhoneVerifyCode(loginData.getPhone(), loginData.getPhoneVerifyCode()); userDetails = userDetailsService.loadUserByPhone(loginData.getPhone()); } else { throw new AuthenticationServiceException( "Unsupported login method" ); } MyAuthenticationToken authenticationToken = new MyAuthenticationToken(userDetails, myAuthenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticationToken); return Mono.just(authenticationToken); } Copy code

Authentication processing logic

/** * Authorized 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 by 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()); //Users who have passed authentication and match roles 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 code

Uncertified processor

/** * Uncertified 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())); return response.writeWith(Mono.just(buffer)); }); } } Copy code

Certified success processor

/** * Certified successful processor * @Desc will generate token and other operations after successful login here. * @Author DaMai * @Date 2021/3/23 15:26 * Doing good deeds without asking for reward. */ @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()); //Assembly return parameter UserLoginVO result = new UserLoginVO(); UserInfoVO userInfo = new UserInfoVO(); BeanUtils.copyProperties(userDetails, userInfo); result.setUserInfo(userInfo); result.setToken(token); //save 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); return response.writeWith(Mono.just(dataBuffer)); })); } } Copy 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); //Username or password is wrong } else if (exception instanceof BadCredentialsException) { result = ResultVO.error(ResultEnum.LOGIN_PASSWORD_ERROR); //Account has expired } else if (exception instanceof AccountExpiredException) { result = ResultVO.error(ResultEnum.ACCOUNT_EXPIRED); //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); //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); return response.writeWith(Mono.just(dataBuffer)); })); } } Copy code

Authentication failure handler

/** * 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())); return response.writeWith(Mono.just(buffer)); }); } } Copy code

Log out of the 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 token redisTemplate.opsForHash().delete(AuthConstant.TOKEN_REDIS_KEY, userDetails.getId()); log.info( "Logout successful: {}" , myAuthenticationToken.toString()); return response.writeWith(Mono.just(dataBuffer)); } } Copy code

Total configuration

/** * Security core configuration class * @Desc configures security in detail here * @Author DaMai * @Date 2021/3/23 15:26 * Doing good deeds without asking for reward. */ @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); }