前言
shiro整合JWT系列,主要记录核心思路–如何在shiro+redis整合JWTToken。
该篇主要讲述整合JWT需要创建那些类,如下:
- JwtToken (JWT实体类)
- JwtUtil (JWT工具类)
- JwtFilter (JWT拦截器)
所使用的依赖是:
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1、JwtToken (JWT实体类)
该类实现了AuthenticationToken中的2个接口,getPrincipal()
和getCredentials()
,主要用来获取token。
这两个方法之前是由UsernamePasswordToken实现,用于获取用户名和密码。
- JwtToken代码如下:
public class JwtToken implements AuthenticationToken {
/**
* 用于确保在对象序列化过程中,版本号一致。
* 当一个对象被序列化后,它的字节流可以被存储在文件系统中或通过网络传输到另一个计算机。
* 当接收方收到字节流并反序列化它时,它需要确保序列化和反序列化的版本号一致,否则就会抛出版本不一致的异常。
* 因此,在序列化类中,需要为每个类提供一个唯一的 serialVersionUID,确保在版本升级时,反序列化仍然可以正确地工作。
*/
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
疑问:这个类具体是怎么使用到的?
回答:当获取到前端传来的token字符串后,将该字符串token存入JwtToken 对象中(如:new JwtToken(token)
),当使用的时候,只需要调用上面任意方法就可以取出前端传来的token。
2、JwtUtil (JWT工具类)
JWT工具类最主要的应该就是以下3个方法:
-
verify:验证
目的:验证token是否正确,判断前端传入的token
与后端生成的token
是否一样
账号和密钥按指定Algorithm 创建出来的JWT对象,可以理解就是我们的token。 -
getUsername:获取token中的账号(用户名)
目的:用于查询数据库中该用户的信息(得到存于数据库中的加密密码)
Jwt中主要包含header和payload两个部分。
-
sign:生成签名
目的:创建该用户的token
密钥按指定Algorithm生成的实例 + payload信息 + 过期时间,生成token(还可以加更多内容,有兴趣再去研究)
- JwtUtil代码如下:
public class JwtUtil {
// 过期时间30分钟
public static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 1、校验token是否正确
*
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
// 根据密钥(这里是密码)生成一个算法实例
Algorithm algorithm = Algorithm.HMAC256(secret);
// 生成JWT效验器
JWTVerifier verifier = JWT.require(algorithm) // 设置一个以该算法为基础的校验器
.withClaim("username", username) // payload 存储非敏感的信息 例如用户账号,不能存密码,防止被人解
.build(); // 创建校验器
// 效验TOKEN,如果出现不匹配,就会出现异常
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 2、获得token中的信息无需secret解密也能获得
*
* JWT.decode解码:可以查看token中的head,payload信息
* 如果想要校验token,则需要知道sign的算法和附带的payload信息
* sign的算法:Algorithm.HMAC256(secret)
* 附带的payload信息:JWT.withClaim
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
// 对token进行解码【可以查看token中的head,payload信息】
DecodedJWT jwt = JWT.decode(token);
// 获取解码信息中的username
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 3、生成签名,5min后过期
* 生成token签名EXPIRE_TIME 分钟后过期
* @param username 用户名
* @param secret 用户的密码【secret应该是定义的密钥,这里是用当前用户密码当作密钥】
* @return 加密的token
*/
public static String sign(String username, String secret) {
try{
//现在系统的时间 + 过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 根据密钥(这里是密码)生成一个算法实例
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username) // payload 存储非敏感的信息 例如用户账号,不能存密码,防止被人解析
.withExpiresAt(date) // 指定令牌的过期时间
.sign(algorithm); // 签名 保密复杂
}catch (UnsupportedEncodingException e){
return null;
}
}
3、JwtFilter (JWT拦截器)
注意:引入JWT后,核心登录subject.login()将在JwtFilter类的executeLogin方法里。
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效请重新登录");
}
}
/**
* 执行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("ACCESS_TOKEN");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// 这里真正实现shiro的登录,getSubject(request, response)直接SecurityUtils.getSubject()也一样
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 如果没有登录,直接返回401未授权提示
*
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
httpResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
// 返回到前端信息
httpResponse.getWriter().write("未登录或登录失效,请重新登录!");
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
疑惑:我知道了token的值,在模拟请求中,这个token应该放在header里的哪个字段?
回答:这个是我们代码中定义的,String token = httpServletRequest.getHeader("ACCESS_TOKEN");
这段就是获取请求header中名为ACCESS_TOKEN
的值;也就是我们说token存放的地方。(注意:ACCESS_TOKEN
这个名称可以自己定义)
ps:辣鸡的我就这个地方找了好久😢