一、SpringSecurity - WebFlux
上篇文章我们讲解了SpringSecurity
在WebFlux
环境下的动态角色权限的控制,本篇文章我们一起讲解下SpringSecurity
在WebFlux
环境下整合JWT使用 Token 认证授权。
上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122511037
二、整合JWT使用 Token 认证授权
在关于SpringSecurity
在WebFlux
环境下的使用,在前面的几篇文章中已经讲解了。在看本篇文章之前,最好已经看过本专栏的前面几篇关于SpringSecurity
的文章了,一些重复性的代码就不再写出来了。下面直接进入主题。
在开始之前我们先清楚一个问题,对于登录认证SpringSecurity
已经帮我们实现了,我们可以指定登录的路径,默认是x-www-form-urlencoded
方式。所以我们不用编写登录的逻辑,但是有些情况下可能SpringSecurity
所提供的不能满足我们的需求,比如我们是自定义的加密数据传输的情况,此时我们可以自己写一个登录接口,在该接口中颁发Token
令牌出来,并设置ServerHttpSecurity
对象的formLogin().disable()
,下面的演示中是采用SpringSecurity
所提供的认证来进行演示。
编写JWT工具类
这里我将权限也放在了JWT中,如果需要动态变更用户权限的可以考虑放在Redis或其他NoSql数据库中,本文主要演示Jwt的使用:
@Data
@Component
public class JwtTool {
private String key = "com.bxc";
private long overtime = 1000 * 60 * 60;
public String CreateToken(String userid, String username, List<String> roles) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setId(userid)
.setSubject(username)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key)
.claim("roles", roles);
if (overtime > 0) {
builder.setExpiration(new Date(nowMillis + overtime));
}
return builder.compact();
}
public boolean VerityToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if (claims != null) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public String getUserid(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if (claims != null) {
return claims.getId();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String getUserName(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if (claims != null) {
return claims.getSubject();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public List<String> getUserRoles(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if (claims != null) {
return (List<String>) claims.get("roles");
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String getClaims(String token, String param) {
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if (claims != null) {
return claims.get(param).toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
编写登录成功的Handler
我们可以在这里做办法Token令牌的逻辑:
@Component
public class LoginSuccessHandler implements ServerAuthenticationSuccessHandler {
@Autowired
JwtTool jwtTool;
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
UserEntity user = (UserEntity) authentication.getPrincipal();
String username = user.getUsername();
List<GrantedAuthority> authorities = (List<GrantedAuthority>) user.getAuthorities();
List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String token = jwtTool.CreateToken(String.valueOf(user.getId()), username, roles);
JSONObject params = new JSONObject();
params.put("code", 200);
params.put("msg", "登陆成功!");
params.put("username", username);
params.put("role", roles);
params.put("token", token);
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Mono<Void> ret = null;
try {
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return ret;
}
}
编写登录失败的Handler
返回客户端一个友好的提示,这里我直接返回了登录失败
,大家可以根据AuthenticationException
这个类进行具体判断,返回具体的错误信息:
@Component
public class LoginFailedHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
JSONObject params = new JSONObject();
params.put("code", 400);
params.put("msg", "登录失败!");
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Mono<Void> ret = null;
try {
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
} catch (UnsupportedEncodingException e0) {
e0.printStackTrace();
}
return ret;
}
}
编写登录失效的Hander
@Component
public class LoginLoseHandler extends HttpBasicServerAuthenticationEntryPoint {
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
JSONObject params = new JSONObject();
params.put("code", 401);
params.put("msg", "登录失效!");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
Mono<Void> ret = null;
try {
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
} catch (UnsupportedEncodingException e0) {
e0.printStackTrace();
}
return ret;
}
}
编写JWT的过滤器
既然上面已经颁发了JWT
的Token
,那么请求来的第一步就要进行JWT
的过滤和校验,如果OK在交给SpringSecurity
将JWT的内容解析出来,所以这个过滤器只是一个教研JWT
是否有效的作用,并没有对当前请求授权:
@Slf4j
@Component
public class JwtWebFilter implements WebFilter {
@Autowired
JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders header = response.getHeaders();
header.add("Content-Type", "application/json; charset=UTF-8");
String path = request.getPath().value();
if (path.contains("/auth/login")){
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
return response.writeWith(Mono.just(buffer));
}
boolean isold = jwtTool.VerityToken(token);
if (!isold) {
JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
return response.writeWith(Mono.just(buffer));
}
String username = jwtTool.getUserName(token);
if (com.alibaba.druid.util.StringUtils.isEmpty(username)) {
JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
return response.writeWith(Mono.just(buffer));
}
return chain.filter(exchange);
}
private JSONObject setResultErrorMsg(Integer code,String msg) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", code);
jsonObject.put("message", msg);
return jsonObject;
}
}
解析JWT中用户信息,并授予角色权限信息
上面只是做了JWT
的一个初步过滤,到这就要解析JWT
中的信息,组建一个UsernamePasswordAuthenticationToken
进行用户的授权,这里我又做了一遍JWT的校验,其实这里可以不做JWT的校验了,前面的过滤器已经校验过了,直接取内容即可,
@Slf4j
@Component
public class JwtSecurityContextRepository implements ServerSecurityContextRepository {
@Autowired
JwtTool jwtTool;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
String path = exchange.getRequest().getPath().toString();
// 过滤路径
if ("/auth/login".equals(path)) {
return Mono.empty();
}
String token = exchange.getRequest().getHeaders().getFirst("token");
if (StringUtils.isBlank(token)) {
throw new DisabledException("登录失效!");
}
boolean isold = jwtTool.VerityToken(token);
if (!isold) {
throw new AccessDeniedException("登录失效!");
}
String username = jwtTool.getUserName(token);
if (com.alibaba.druid.util.StringUtils.isEmpty(username)) {
throw new AccessDeniedException("登录失效!");
}
Authentication newAuthentication = new UsernamePasswordAuthenticationToken(username, username);
return new ReactiveAuthenticationManager() {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.fromCallable(() -> {
List<String> roles = jwtTool.getUserRoles(token);
List<GrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
UserEntity principal = new UserEntity();
principal.setUsername(username);
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
});
}
}.authenticate(newAuthentication).map(SecurityContextImpl::new);
}
}
判断用户是否有权访问该接口
上面只是获取到了用户所以拥有的角色权限信息,下面还要判断访问的该接口所需的角色用户是否拥有,这个地方的逻辑在上篇文章中进行了讲解,可以参考下上篇文章:
@Component
public class AuthManagerHandler implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
MeunMapper meunMapper;
@Autowired
RoleMapper roleMapper;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
ServerHttpRequest request = object.getExchange().getRequest();
String requestUrl = request.getPath().pathWithinApplication().value();
List<MeunEntity> list = meunMapper.selectList(null);
List<String> roles = new ArrayList<>();
list.forEach(m -> {
if (antPathMatcher.match(m.getPattern(), requestUrl)) {
List<String> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId())
.stream()
.map(r -> r.getRole())
.collect(Collectors.toList());
roles.addAll(allRoleByMenuId);
}
});
if (roles.isEmpty()) {
return Mono.just(new AuthorizationDecision(false));
}
return authentication
.filter(a -> a.isAuthenticated())
.flatMapIterable(a -> a.getAuthorities())
.map(g -> g.getAuthority())
.any(c -> {
if (roles.contains(String.valueOf(c))) {
return true;
}
return false;
})
.map(hasAuthority -> new AuthorizationDecision(hasAuthority))
.defaultIfEmpty(new AuthorizationDecision(false));
}
@Override
public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
return null;
}
}
编写无权访问的提示Handler
@Component
public class AccessDeniedHandler implements ServerAccessDeniedHandler {
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) {
JSONObject params = new JSONObject();
params.put("code", 403);
params.put("msg", "权限不足!");
ServerHttpResponse response = serverWebExchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Mono<Void> ret = null;
try {
ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
} catch (UnsupportedEncodingException e0) {
e0.printStackTrace();
}
return ret;
}
}
修改SecurityConfig配制
将上面所写的配制到SpringSecurity
中:
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Autowired
UserDetailService userDetailService;
@Autowired
AuthManagerHandler authManagerHandler;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
LoginFailedHandler loginFailedHandler;
@Autowired
LoginLoseHandler loginLoseHandler;
@Autowired
JwtSecurityContextRepository jwtSecurityContextRepository;
@Autowired
JwtWebFilter jwtWebFilter;
//security的鉴权排除列表
private static final String[] excludedAuthPages = {
"/auth/login",
"/auth/logout"
};
@Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //无需进行权限过滤的请求路径
.pathMatchers(HttpMethod.OPTIONS).permitAll() //o
.pathMatchers("/**").access(authManagerHandler)
.anyExchange().authenticated()
.and()
.addFilterAfter(jwtWebFilter, SecurityWebFiltersOrder.FIRST)
.securityContextRepository(jwtSecurityContextRepository)
.formLogin()
.loginPage("/auth/login")
.authenticationSuccessHandler(loginSuccessHandler)
.authenticationFailureHandler(loginFailedHandler)
.and().exceptionHandling().authenticationEntryPoint(loginLoseHandler)
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().cors().disable().csrf().disable();
return http.build();
}
}
三、效果演示
不登录,直接访问http://localhost:8080/admin/test
,会提示登录失效。
登录http://localhost:8080/auth/login
,获取返回的Token
:
下面使用返回的Token
,再次测试上面的接口:
如果访问一个无权限的接口:http://localhost:8080/common/test