【SpringSecurity】十一、SpringSecurity集成JWT实现token的方法与校验
1、依赖与配置
添加JWT的maven依赖:
<!-- 添加jwt的依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
application.yaml中配置密钥的值,方便代码中引用和后续更改:
jwt:
secretKey: mykey
2、JWT工具类
这里的命名改为JWTService好点,Utils命名似乎偏静态方法一点。
@Component
@Slf4j
public class JwtUtils {
//算法密钥
@Value("${jwt.secretKey}")
private String jwtSecretKey;
/**
* 创建jwt
*
* @param userInfo 用户信息
* @param authList 用户权限列表
* 根据登录用户的数据库信息和权限信息,加上服务端密钥,创建token
* @return 返回jwt(JSON WEB TOKEN)
*/
public String createToken(String userInfo, List<String> authList) {
//创建时间
Date currentTime = new Date();
//过期时间,5分钟后过期
Date expireTime = new Date(currentTime.getTime() + (1000 * 60 * 5));
//jwt的header信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("type", "JWT");
headerClaims.put("alg", "HS256");
//创建jwt
return JWT.create()
.withHeader(headerClaims) // 头部信息
.withIssuedAt(currentTime) //已注册声明:签发日期,发行日期
.withExpiresAt(expireTime) //已注册声明 过期时间
.withIssuer("llg") //已注册声明,签发人
.withClaim("userInfo", userInfo) //私有声明,可以自己定义
.withClaim("authList", authList) //私有声明,可以自定义
.sign(Algorithm.HMAC256(jwtSecretKey)); // 签名,使用HS256算法签名,并使用密钥
//HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
}
/**
* 验证jwt的签名,简称验签
* @param token 需要验签的jwt
* @return 验签结果
*/
public boolean verifyToken(String token) {
//获取验签类对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签,如果不报错,则说明jwt是合法的,而且也没有过期
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
//如果报错说明jwt 为非法的,或者已过期(已过期也属于非法的)
log.error("验签失败:{}", token);
e.printStackTrace();
}
return false;
}
/**
* 从token中获取用户信息
* 这个userInfo是创建token时我自己塞进去的
* @param token jwt
* @return 用户信息
*/
public String getUserInfo(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签(主要为了同时的获取解析的结果)
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中userInfo的值,并返回
return decodedJWT.getClaim("userInfo").asString();
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
/**
* 获取用户权限
*
* @param token
* @return
*/
public List<String> getUserAuth(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签(主要为了同时的获取解析的结果)
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中的自定义数据authList(权限列表),并返回
return decodedJWT.getClaim("authList").asList(String.class);
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
}
再贴一下下统一结果类的定义:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult implements Serializable {
private Integer code; //响应码
private String msg; //响应消息
private Object data; //响应对象
}
下面是安全用户类,用于在数据库的用户对象类SysUser和返给框架的官方对象类UserDetails之间做过渡转换。UserDetails <====> SecurityUser <====> SysUser
@Setter
public class SecurityUser implements UserDetails {
private final SysUser sysUser;
private List<SimpleGrantedAuthority> simpleGrantedAuthorities;
public SecurityUser(SysUser sysUser) {
this.sysUser=sysUser;
}
public SysUser getSysUser() {
return sysUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthorities;
}
@Override
public String getPassword() {
String userPassword=this.sysUser.getPassword();
//注意清除密码
this.sysUser.setPassword(null);
return userPassword;
}
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
3、认证成功处理器
自定义处理器,实现AuthenticationSuccessHandler
,当用户登录认证成功后,会执行这个处理器,即认证成功处理器
。
/**
* 认证成功处理器,当用户登录成功后,会执行此处理器
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//使用此工具类进行序列化
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//从认证对象中获取认证用户信息
//查看前面第六章的UserDetailsService接口的loadUserByUsername方法,
//返回给框架的是一个自定义的SecurityUser对象(Security实现了UserDetails)
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
//从SecurityUser中拿出和底层MYSQL挂钩的SysUser类信息
String userInfo=objectMapper.writeValueAsString(securityUser.getSysUser());
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) securityUser.getAuthorities();
//List<SimpleGrantedAuthority>转List<String>
List<String> authList=new ArrayList<>();
for (SimpleGrantedAuthority authority : authorities) {
authList.add(authority.getAuthority());
}
//也可使用stream流代替上面的for循环
List<String> authList = authorities.stream().map(
a -> {
return a.getAuthority();
}
).collect(Collectors.toList());
//也可使用stream流+Lambda表达式
List<String> authList = authorities.stream()
.map(SimpleGrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 调用前面的JWT工具类方法创建jwt
String token = jwtUtils.createToken(userInfo,authList);
//返回给前端token@Builder模式创建对象
HttpResult httpResult = HttpResult.builder().code(200).msg("OK").data(token).build();
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
}
4、创建JWT过滤器
定义JWT过滤器,用来检验一个个对接口的请求中token是否合法,注意放行登录接口:
/**
* 定义一次性请求过滤器
*/
@Component
@Slf4j
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求uri
String requestURI = request.getRequestURI();
// 如果是登录页面,放行
if (requestURI.equals("/login")) {
filterChain.doFilter(request, response);
return;
}
//获取请求头中的Authorization,前端一般这么传,key为Authorization
String authorization = request.getHeader("Authorization");
//如果Authorization为空,那么不允许用户访问,直接返回
if (!StringUtils.hasText(authorization)) {
printFront(response, "没有登录!");
return;
}
//Authorization 去掉头部的Bearer 信息,获取token值
String jwtToken = authorization.replace("Bearer ", "");
//验签
boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);
//验签不成功
if (!verifyTokenResult) {
printFront(response, "jwtToken 已过期");
return;
}
//到这儿算是验证通过,但还没结束,还要将信息填充后返给SpringSecurity框架
//从payload中获取userInfo
String userInfo = jwtUtils.getUserInfo(jwtToken);
//从payload中获取授权列表
List<String> userAuth = jwtUtils.getUserAuth(jwtToken);
//在认证成功处理器中,创建token时,userInfo里放的是SysUser对象的序列化字符串,这里反序列化
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUser securityUser = new SecurityUser(sysUser);
//设置权限
List<SimpleGrantedAuthority> authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
securityUser.setAuthorityList(authList);
//填充信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser
, null, authList);
//通过安全上下文设置认证信息
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke);
//继续访问相应的rul等
filterChain.doFilter(request, response);
}
/**
* 定义一个通过response向前端返回数据的方法
* 这里不是controller层,不是你直接返回个结果类就行的,注意区别
*/
private void printFront(HttpServletResponse response, String message) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
HttpResult httpResult = new HttpResult();
httpResult.setCode(401);
httpResult.setMsg(message);
writer.print(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
}
上面的过滤器中,除了正常的验签,最后的消息填充与保存在安全上下文,就是下图中的第十步:
5、安全配置类
修改下安全配置类,把上面的处理器和过滤器加进来。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private JwtCheckFilter jwtCheckFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//先看token是否合法,再走框架的用户名密码校验过滤器
http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
//要是有之间的验证码校验,则它应该在token校验之前
//认证通过后,走认证成功处理器,颁发token
http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
//简单按接口加个权限要求
http.authorizeRequests()
.mvcMatchers("/student/**")
.hasAnyAuthority("student:query","student:update")
.anyRequest()
.authenticated(); //任何请求均需要认证(登录成功)才能访问
http.csrf().disable(); //跨域
//禁用session方式
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
效果:
登录认证后返给前端token: