最近在公司中尝试搭建微服务网关,选型为Spring Cloud GateWay,GateWay是响应式编程(WebFlux)范式的,所以与以往项目有一点不同。
网关嘛,这里不细说。本篇记录WebFlux 如何与 Spring Security 、JWT 结合。
思路梳理
经过前面 MVC 与 Spring Security 集成的学习,我们可以归纳出一下要点:
- 认证的入口。在此处,我们需要把 Request 中 Body,转化为实体类,并封装到自定义 Security 令牌中。
- 认证逻辑处理。
- 鉴权入口、鉴权逻辑处理。
- 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);
}
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END