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:
- 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.
- Authentication logic processing.
- Authentication entry and logic processing.
- 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