1,前言
最近在做项目,做到权限模块,感觉登录功能也没这么简单。
查阅了一些文档加博客,大致明白了如何写这个模块。所以记录一下。
2,正文
一,POM
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
二,启动类
@SpringBootApplication
public class ShiroTest {
public static void main(String[] args) {
SpringApplication.run(ShiroTest.class);
}
}
三,JwtUtil
jwt工具类,方便对jwt加密,解密,获取数据
package com.cql.shiro.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.UnsupportedEncodingException;
import java.util.Date;
public class JwtUtil {
// 过期时间 24 小时
private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
// 密钥
private static final String SECRET = "shiro";
/**
* @param username 用户名
* @return 加密的token
*/
public static String createToken(String username) {
try {
// 过期时间为一天
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
// 附带username信息
return JWT.create()
.withClaim("userId", username)
//到期时间
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 校验 token 是否正确
*
* @param token 密钥
* @param username 用户名
* @return 是否正确
*/
public static boolean verify(String token, String username) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//在token中附带了username信息
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("userId", username)
.build();
//验证 token
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息,无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getClaim(String claim,String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
四,JwtToken
package com.cql.shiro.pojo;
import org.apache.shiro.authc.AuthenticationToken;
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
五,自定义Realm
自定义Realm
package com.cql.shiro.realm;
import com.cql.shiro.pojo.JWTToken;
import com.cql.shiro.pojo.User;
import com.cql.shiro.service.UserService;
import com.cql.shiro.util.JwtUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
UserService userService;
/**
* 必须重写此方法,不然会报错,因为shiro不支持我们自定义的token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证方法————");
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String userId = JwtUtil.getClaim("userId", token);
if (userId == null || !JwtUtil.verify(token, userId)) {
throw new AuthenticationException("token认证失败!");
}
User user = userService.getUserByUserId(userId);
if (user == null) {
throw new AuthenticationException("该用户不存在!");
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("————权限认证————");
String userId = JwtUtil.getClaim("userId", principals.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = userService.getUserRole(userId);
//每个角色拥有默认的权限
String permission = userService.getUserPermission(userId);
//设置该用户拥有的角色和权限
info.addRole(role);
info.addStringPermission(permission);
return info;
}
}
六,UserService
因为重点不是对数据库的增删改查,所以这部分直接写死了
package com.cql.shiro.service;
import com.cql.shiro.pojo.User;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 验证用户名
public User getUserByUserId(String userId) {
if ("123".equals(userId)) {
return null;
} else {
User user = new User();
user.setInfo("测试信息");
user.setUserId(userId);
return user;
}
}
// 获取角色
public String getUserRole(String userId) {
if ("123".equals(userId)) {
return "teacher";
} else {
return "student";
}
}
// 获取权限
public String getUserPermission(String userId) {
if ("123".equals(userId)) {
return "vip1";
} else {
return "vip10";
}
}
}
七,JwfFilter
过滤,验证。请求头里面的token。(记住是请求头携带token,而不是请求参数,就是因为)
package com.cql.shiro.filter;
import com.cql.shiro.pojo.JWTToken;
import com.cql.shiro.pojo.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Service
public class JwfFilter extends BasicHttpAuthenticationFilter {
//判断是否有token,如果有,就对token进行验证,否则直接通过(游客或匿名用户不需要登录,所以不需要token)。
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
e.printStackTrace();
response401(request,response,e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Token");
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("Token");
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
// 写回错误信息。因为是前后端分离,所以不是跳转页面,而是返回错误信息给前端
private void response401(ServletRequest req, ServletResponse resp, String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = httpServletResponse.getWriter();
String data = new ObjectMapper().writeValueAsString(R.error(msg));
out.append(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
八,ShiroConfig
package com.cql.shiro.config;
import com.cql.shiro.filter.JwfFilter;
import com.cql.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 先走 filter ,然后 filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
*/
@Bean
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new LinkedHashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JwfFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 设置无权限时跳转的 url;
//factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
Map<String, String> filterRuleMap = new HashMap<>();
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
// 访问 /unauthorized/** 不通过JWTFilter
//filterRuleMap.put("/unauthorized/**", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义 realm.
securityManager.setRealm(customRealm);
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 添加注解支持
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
九,controller
@RestController
public class LoginController {
@Autowired
private UserService userService;
@PostMapping("/login")
public R login(String userId){
User user = userService.getUserByUserId(userId);
if(user == null){
return R.error("用户不存在");
}else{
return R.ok().data("token", JwtUtil.createToken(userId));
}
}
@RequiresPermissions("vip10")
@GetMapping("/testPermission")
public R testPermission(){
return R.ok().data("message","收到消息了");
}
@RequiresRoles("teacher")
@GetMapping("/testRole")
public R testRole(){
return R.ok().data("message","收到消息了");
}
}
十,异常捕获
@RestController
public class LoginController {
@Autowired
private UserService userService;
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTI2NTY0NDZ9.tGfpqCVB8faAYSsmhVX5ZoZcAe0UaJ4aB9FI0KHSQV0
@PostMapping("/login")
public R login(String userId) {
User user = userService.getUserByUserId(userId);
if (user == null) {
return R.error("用户不存在");
} else {
return R.ok().data("token", JwtUtil.createToken(userId));
}
}
@RequiresPermissions("vip10")
@PostMapping("/testPermission")
public R testPermission() {
return R.ok().data("message", "testPermission");
}
@RequiresRoles(logical = Logical.OR, value = {
"teacher", "student"})
@RequestMapping(value = "/testRole", method = RequestMethod.GET)
public R testRole() {
return R.ok().data("message", "testRole");
}
}
十一,测试
获取token重新访问上面接口
三,关于token过期处理(思路)
1,前端
大家都知道在前后端是以token的形式交互,既然是token,那么肯定有它的过期时间,没有一个token是永久的,永久的token就相当于一串永久的密码,是不安全的,那么既然有刷新时间,问题就来了
- 前后端交互的过程中token如何存储?
- token过期时,前端该怎么处理
- 当用户正在操作时,遇到token过期该怎么办?直接跳回登陆页面?
token如何存储
cookie的大小约4k,兼容性在ie6及以上 都兼容,在浏览器和服务器间来回传递,因此它得在服务器的环境下运行,而且可以设定过期时间,默认的过期时间是session会话结束。
localStorage的大小约5M,兼容性在ie7及以上都兼容,有浏览器就可以,不需要在服务器的环境下运行, 会一直存在,除非手动清除 。
对于这个问题,答案大致分为2种
- 存在
cookie
中 - 存在
localStorage
中
token过期时,前端该怎么处理
思路:token过期处理方式大概就是:
- 第一种:跳回登陆页面重新登陆
- 第二种:catch 401 ,然后重新获取 token
对于第一种,很简单在vue中我们可以在 axios 拦截器中这样写:
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response.data
},
function (error) {
console.log(error)
if (error.response) {
if (error.response.status === 401) {
//401 token过期
Message.error('登陆过期请重新登陆!')
setToken('')
//跳转会登录页面
router.push({
name: 'login'
})
}
}
}
// 对响应错误做点什么
return Promise.reject(error.response)
}
)
对于第二种,如何重新获取 token,这就要涉及到后端的知识了
现代认证和/或授权解决方案已将令牌的概念引入其协议中。令牌是经过特殊处理的数据,它们可以提供足够的信息来授权用户执行操作,或者允许客户端获取有关授权过程的其他信息(然后完成它)。换句话说,令牌是允许执行授权过程的信息。客户端(或授权服务器以外的任何一方)是否可读取或解析此信息是由实现定义的。重要的是:客户端获取此信息,然后使用它来访问资源。JSON Web令牌(JWT)规范 定义了一种可以由实现表示公共令牌信息的方式。
JWT定义了一种方式,其中可以表示与认证/授权过程有关的某些公共信息。顾名思义,数据格式是JSON。JWT具有某些常见字段,例如主题,发行者,到期时间等。当与其他规范(如JSON Web签名(JWS)和JSON Web加密(JWE))结合使用时,JWT变得非常有用。这些规范不仅提供了授权令牌通常所需的所有信息,而且还提供了验证令牌内容的方法,使其不会被篡改(JWS)和加密信息以使其保持不透明的方式给客户(JWE)。数据格式的简单性(及其它优点)帮助JWT成为最常见的令牌类型之一。
我们将关注两种最常见的令牌类型:访问令牌和刷新令牌
。
- 访问令牌携带必要的信息以直接访问资源。换句话说,当客户端将访问令牌传递给管理资源的服务器时,该服务器可以使用令牌中包含的信息来决定客户端是否被授权。访问令牌通常具有到期日期并且是短暂的。
- 刷新令牌包含获取新访问令牌所需的信息。换句话说,每当访问令牌需要访问特定资源时,客户端可以使用刷新令牌来获得由认证服务器发布的新访问令牌。常见用例包括在旧的访问令牌过期后获取新访问令牌,或者首次访问新资源。刷新令牌也可以过期,但相当长寿。刷新令牌通常受到严格的存储要求,以确保它们不会泄露。它们也可以被授权服务器列入黑名单
服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token 过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401
所以要实现无痛刷新token,我们应该这样
- 在axios的拦截器中加入token刷新逻辑
- 当用户token过期时,去向服务器请求新的 token
- 把旧的token替换为新的token
- 然后继续用户当前的请求
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response.data
},
function (error) {
console.log(error)
if (error.response) {
if (error.response.status === 401) {
// 如果当前路由不是login,并且用户有 “记住密码” 的操作
// 那么去请求新 token
if (router.currentRoute.name !== 'login') {
if (getRemember() && getRefreshToken()) {
return doRequest(error)
} else {
Message.error('登陆过期请重新登陆!')
setToken('')
router.push({
name: 'login'
})
}
}
}
}
// 对响应错误做点什么
return Promise.reject(error.response)
}
)
/、刷新token,并且重新发送上一个请求
async function doRequest (error) {
const data = await store.dispatch('refreshToken')
let {
token_type: tokenType, access_token: accessToken } = data
let token = tokenType + accessToken
let config = error.response.config
config.headers.Authorization = token
const res = await axios.request(config)
return res
}
这里我们一定要用同步的方法来进行这一系列操作!!(比如 async / await)
2,后端
基本思路
-
单个token的做法
- token(A)过期设置为15分钟
- 前端发起请求,后端验证token(A)是否过期;如果过期,前端发起刷新token请求,后端设置已再次授权标记为true,请求成功
- 前端发起请求,后端验证再次授权标记,如果已经再次授权,则拒绝刷新token的请求,请求成功
- 如果前端每隔72小时,必须重新登录,后端检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败
-
授权token加上刷新token的做法:用户仅登录一次,用户改变密码,则废除token,重新登录
1.0实现
- 登录成功,返回
access_token
和refresh_token
,客户端缓存此两种token - 使用
access_token
请求接口资源,成功则调用成功;如果token超时(捕获401状态码),客户端
携带refresh_token
调用中间件接口获取新的access_token; - 中间件接受刷新token的请求后,检查refresh_token是否过期。
如过期,拒绝刷新,客户端收到该状态后,跳转到登录页;
如未过期,生成新的access_token和refresh_token并返回给客户端(如有可能,让旧的refresh_token失效),客户端携带新的access_token重新调用上面的资源接口。 - 客户端退出登录或修改密码后,调用中间件注销旧的token(使access_token和refresh_token失效),同时清空客户端的access_token和refresh_toke。
2.0实现
场景: access_token访问资源 refresh_token授权访问 设置固定时间X必须重新登录
-
登录成功,后台jwt生成
access_token(jwt有效期30分钟)
和refresh_token(jwt有效期15天)
,并缓存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以根据sub-key,废除部分设备的token。)
,设置过期时间为1个月,保证最终所有token都能删除),返回后,客户端缓存此两种token; -
使用
access_token
请求接口资源,校验成功且redis中存在该access_token(未废除)
则调用成功;如果token超时,中间件删除access_token(废除)
;客户端再次携带refresh_token
调用中间件接口获取新的access_token
; -
中间件接受刷新token的请求后,检查
refresh_token
是否过期。
如过期,拒绝刷新,删除refresh_token(废除)
; 客户端收到该状态后,跳转到登录页;
如未过期,检查缓存中是否有refresh_token(是否被废除)
,如果有,则生成新的access_token
并返回给客户端,客户端接着携带新的access_token
重新调用上面的资源接口。 -
客户端退出登录或修改密码后,调用中间件注销旧的
token(中间件删除access_token和refresh_token(废除))
,同时清空客户端侧的access_token和refresh_toke
。 -
如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
-
以上3刷新
access_token
可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:失效,长时间未登录,频繁刷新)
部分代码
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 判断用户是否想要登入
if (this.isLoginAttempt(request, response)) {
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
} catch (Exception e) {
// 认证出现异常,传递错误信息msg
String msg = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable != null && throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
} else if (throwable != null && throwable instanceof TokenExpiredException) {
// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
if (this.refreshToken(request, response)) {
return true;
} else {
msg = "token已过期(" + throwable.getMessage() + ")";
}
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
msg = throwable.getMessage();
}
}
/**
* 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2.
* 无需转发,直接返回Response信息 一般使用第二种(更方便)
*/
// 直接返回Response信息
this.response401(request, response, msg);
return false;
}
}
return true;
}
/**
* 此处为AccessToken刷新,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
// 获取当前Token的帐号信息
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
// 判断Redis中RefreshToken是否存在
if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 读取配置文件,获取refreshTokenExpireTime属性
// PropertiesUtil.readProperties("config.properties");
// String refreshTokenExpireTime =
// PropertiesUtil.getProperty("refreshTokenExpireTime");
// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
Integer.parseInt(refreshTokenExpireTime));
// 刷新AccessToken,设置时间戳为当前最新时间戳
token = JwtUtil.sign(account, currentTimeMillis);
// 将新刷新的AccessToken再次进行Shiro的登录
JwtToken jwtToken = new JwtToken(token);
// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
this.getSubject(request, response).login(jwtToken);
// 最后将刷新的AccessToken存放在Response的Header中的Token字段返回
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Token", token);
return true;
}
}
return false;
}