SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权
Posted 小毕超
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权相关的知识,希望对你有一定的参考价值。
一、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;
编写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
喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!
以上是关于SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权的主要内容,如果未能解决你的问题,请参考以下文章
SpringSecurity - WebFlux环境下实现用户动态认证
SpringSecurity系列4基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析
WebFlux 和 Spring Security 会碰出哪些火花?
WebFlux 和 Spring Security 会碰出哪些火花?