springboot+vue3无感知刷新token实战

目录

一、java后端

1、token构造实现类

①验证码方式实现类

②刷新token方式实现类

 2、token相关操作:setCookie

①createToken

②refreshToken

二、前端(vue3+axios)


        web网站中,前后端交互时,通常使用token机制来做认证,token一般会设置有效期,当token过了有效期后,用户需要重新登录授权获取新的token,但是某些业务场景下,用户不希望频繁的进行登录授权,但是安全考虑,token的有效期不能设置太长时间,所以有了刷新token的设计,无感知刷新token的机制更进一步优化了用户体验,本文是博主实际业务项目中基于springboot和vue3无感知刷新token的代码实战。

首先介绍无感知刷新token的实现思路:

①首次授权颁发token时,我们通过后端给前端请求response中写入两种cookie

        - access_token

        - refresh_token(超时时间比access_token长一些)

需要注意:

        -后端setCookie时httpOnly=true(限制cookie只能被http请求携带使用,不能被js操作)

        -前端axios请求参数withCredentials=true(http请求时,自动携带token)

②access_token失效时,抛出特殊异常,前后端约定http响应码(401),此时触发刷新token逻辑

③前段http请求钩子中,如果出现http响应码为401时,立即触发刷新token逻辑,同时缓存后续请求,刷新token结束后,依次续发缓存中的请求

一、java后端

后端java框架使用springboot,spring-security

登录接口: 

/*** @author lichenhao* @date 2023/2/8 17:41*/
@RestController
public class AuthController {/*** 登录方法** @param loginBody 登录信息* @return 结果*/@PostMapping("/oauth")public AjaxResult login(@RequestBody LoginBody loginBody) {ITokenGranter granter = TokenGranterBuilder.getGranter(loginBody.getGrantType());return granter.grant(loginBody);}
}import lombok.Data;/*** 用户登录对象** @author lichenhao*/
@Data
public class LoginBody {/*** 用户名*/private String username;/*** 用户密码*/private String password;/*** 验证码*/private String code;/*** 唯一标识*/private String uuid;/** grantType 授权类型* */private String grantType;/** 是否直接强退该账号登陆的其他客户端* */private Boolean forceLogoutFlag;
}

token构造接口类和token实现类构造器如下:

/*** @author lichenhao* @date 2023/2/8 17:29* <p>* 获取token*/
public interface ITokenGranter {AjaxResult grant(LoginBody loginBody);
}/*** @author lichenhao* @date 2023/2/8 17:29*/
@AllArgsConstructor
public class TokenGranterBuilder {/*** TokenGranter缓存池*/private static final Map<String, ITokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();static {GRANTER_POOL.put(CaptchaTokenGranter.GRANT_TYPE, SpringUtils.getBean(CaptchaTokenGranter.class));GRANTER_POOL.put(RefreshTokenGranter.GRANT_TYPE, SpringUtils.getBean(RefreshTokenGranter.class));}/*** 获取TokenGranter** @param grantType 授权类型* @return ITokenGranter*/public static ITokenGranter getGranter(String grantType) {ITokenGranter tokenGranter = GRANTER_POOL.get(StringUtils.toStr(grantType, PasswordTokenGranter.GRANT_TYPE));if (tokenGranter == null) {throw new ServiceException("no grantType was found");} else {return tokenGranter;}}}

这里通过LoginBody的grantType属性,指定实际的token构造实现类;同时,需要有token

本文我们用到了验证码方式和刷新token方式,如下

1、token构造实现类

①验证码方式实现类

/*** @author lichenhao* @date 2023/2/8 17:32*/
@Component
public class CaptchaTokenGranter implements ITokenGranter {public static final String GRANT_TYPE = "captcha";@Autowiredprivate SysLoginService loginService;@Overridepublic AjaxResult grant(LoginBody loginBody) {String username = loginBody.getUsername();String code = loginBody.getCode();String password = loginBody.getPassword();String uuid = loginBody.getUuid();Boolean forceLogoutFlag = loginBody.getForceLogoutFlag();AjaxResult ajaxResult = validateLoginBody(username, password, code, uuid);// 验证码loginService.validateCaptcha(username, code, uuid);// 登录loginService.login(username, password, uuid, forceLogoutFlag);// 删除验证码loginService.deleteCaptcha(uuid);return ajaxResult;}private AjaxResult validateLoginBody(String username, String password, String code, String uuid) {if (StringUtils.isBlank(username)) {return AjaxResult.error("用户名必填");}if (StringUtils.isBlank(password)) {return AjaxResult.error("密码必填");}if (StringUtils.isBlank(code)) {return AjaxResult.error("验证码必填");}if (StringUtils.isBlank(uuid)) {return AjaxResult.error("uuid必填");}return AjaxResult.success();}
}/*** 登录验证** @param username 用户名* @param password 密码* @return 结果*/public void login(String username, String password, String uuid, Boolean forceLogoutFlag) {// 校验basic authIClientDetails iClientDetails = tokenService.validBasicAuth();// 用户验证Authentication authentication = null;try {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);} catch (Exception e) {if (e instanceof BadCredentialsException) {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();} else {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}} finally {AuthenticationContextHolder.clearContext();}LoginUser loginUser = (LoginUser) authentication.getPrincipal();tokenService.setUserAgent(loginUser);Long customerId = loginUser.getUser().getCustomerId();Boolean singleClientFlag = SystemConfig.isSingleClientFlag();if(customerId != null){Customer customer = customerService.selectCustomerById(customerId);singleClientFlag = customer.getSingleClientFlag();log.info(String.format("客户【%s】单账号登录限制开关:%s", customer.getCode(), singleClientFlag));}if(singleClientFlag){List<SysUserOnline> userOnlineList = userOnlineService.getUserOnlineList(null, username);if(CollectionUtils.isNotEmpty(userOnlineList)){if(forceLogoutFlag != null && forceLogoutFlag){// 踢掉其他使用该账号登陆的客户端userOnlineService.forceLogoutBySysUserOnlineList(userOnlineList);}else{throw new ServiceException("【" + username + "】已登录,是否仍然登陆", 400);}}}// 生成tokentokenService.createToken(iClientDetails, loginUser, uuid);AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));recordLoginInfo(loginUser.getUserId());}

②刷新token方式实现类

/*** @author lichenhao* @date 2023/2/8 17:35*/
@Component
public class RefreshTokenGranter implements ITokenGranter {public static final String GRANT_TYPE = "refresh_token";@Autowiredprivate TokenService tokenService;@Overridepublic AjaxResult grant(LoginBody loginBody) {tokenService.refreshToken();return AjaxResult.success();}
}

 2、token相关操作:setCookie

①createToken

    /*** 创建令牌* 注意:access_token和refresh_token 使用同一个tokenId*/public void createToken(IClientDetails clientDetails, LoginUser loginUser, String tokenId) {if(loginUser == null){throw new ForbiddenException("用户信息无效,请重新登陆!");}loginUser.setTokenId(tokenId);String username = loginUser.getUsername();String clientId = clientDetails.getClientId();// 设置jwt要携带的用户信息Map<String, Object> claimsMap = new HashMap<>();initClaimsMap(claimsMap, loginUser);long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);int accessTokenValidity = clientDetails.getAccessTokenValidity();long accessTokenExpMillis = nowMillis + accessTokenValidity * MILLIS_SECOND;Date accessTokenExpDate = new Date(accessTokenExpMillis);String accessToken = createJwtToken(SecureConstant.ACCESS_TOKEN, accessTokenExpDate, now, JWT_TOKEN_SECRET, claimsMap, clientId, tokenId, username);int refreshTokenValidity = clientDetails.getRefreshTokenValidity();long refreshTokenExpMillis = nowMillis + refreshTokenValidity * MILLIS_SECOND;Date refreshTokenExpDate = new Date(refreshTokenExpMillis);String refreshToken = createJwtToken(SecureConstant.REFRESH_TOKEN, refreshTokenExpDate, now, JWT_REFRESH_TOKEN_SECRET, claimsMap, clientId, tokenId, username);// 写入cookie中HttpServletResponse response = ServletUtils.getResponse();WebUtil.setCookie(response, SecureConstant.ACCESS_TOKEN, accessToken, accessTokenValidity);WebUtil.setCookie(response, SecureConstant.REFRESH_TOKEN, refreshToken, refreshTokenValidity);//插入缓存(过期时间为最长过期时间=refresh_token的过期时间 理论上,保持操作的情况下,一直会被刷新)loginUser.setLoginTime(nowMillis);loginUser.setExpireTime(refreshTokenExpMillis);updateUserCache(loginUser);}private void initClaimsMap(Map<String, Object> claims, LoginUser loginUser) {// 添加jwt自定义参数}/*** 生成jwt token** @param jwtTokenType token类型:access_token、refresh_token* @param expDate      token过期日期* @param now          当前日期* @param signKey      签名key* @param claimsMap    jwt自定义信息(可携带额外的用户信息)* @param clientId     应用id* @param tokenId      token的唯一标识(建议同一组 access_token、refresh_token 使用一个)* @param subject      jwt下发的用户标识* @return token字符串*/private String createJwtToken(String jwtTokenType, Date expDate, Date now, String signKey, Map<String, Object> claimsMap, String clientId, String tokenId, String subject) {JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("typ", "JWT").setId(tokenId).setSubject(subject).signWith(SignatureAlgorithm.HS512, signKey);//设置JWT参数(user维度)claimsMap.forEach(jwtBuilder::claim);//设置应用idjwtBuilder.claim(SecureConstant.CLAIMS_CLIENT_ID, clientId);//设置token typejwtBuilder.claim(SecureConstant.CLAIMS_TOKEN_TYPE, jwtTokenType);//添加Token过期时间jwtBuilder.setExpiration(expDate).setNotBefore(now);return jwtBuilder.compact();}/** 更新缓存中的用户信息* */public void updateUserCache(LoginUser loginUser) {// 根据tokenId将loginUser缓存String userKey = getTokenKey(loginUser.getTokenId());redisService.setCacheObject(userKey, loginUser, parseIntByLong(loginUser.getExpireTime() - loginUser.getLoginTime()), TimeUnit.MILLISECONDS);}private String getTokenKey(String uuid) {return "login_tokens:" + uuid;}

②refreshToken

    /*** 刷新令牌有效期*/public void refreshToken() {// 从cookie中拿到refreshTokenString refreshToken = WebUtil.getCookieVal(ServletUtils.getRequest(), SecureConstant.REFRESH_TOKEN);if (StringUtils.isBlank(refreshToken)) {throw new ForbiddenException("认证失败!");}// 验证 refreshToken 是否有效Claims claims = parseToken(refreshToken, JWT_REFRESH_TOKEN_SECRET);if (claims == null) {throw new ForbiddenException("认证失败!");}String clientId = StringUtils.toStr(claims.get(SecureConstant.CLAIMS_CLIENT_ID));String tokenId = claims.getId();LoginUser loginUser = getLoginUserByTokenId(tokenId);if(loginUser == null){throw new ForbiddenException("用户信息无效,请重新登陆!");}IClientDetails clientDetails = getClientDetailsService().loadClientByClientId(clientId);// 删除原token缓存delLoginUserCache(tokenId);// 重新生成tokencreateToken(clientDetails, loginUser, IdUtils.simpleUUID());}/*** 根据tokenId获取用户信息** @return 用户信息*/public LoginUser getLoginUserByTokenId(String tokenId) {String userKey = getTokenKey(tokenId);LoginUser user = redisService.getCacheObject(userKey);return user;}/*** 删除用户缓存*/public void delLoginUserCache(String tokenId) {if (StringUtils.isNotEmpty(tokenId)) {String userKey = getTokenKey(tokenId);redisService.deleteObject(userKey);}}

③异常码

 401:access_token无效,开始刷新token逻辑

403:refresh_token无效,或者其他需要跳转登录页面的场景

二、前端(vue3+axios)

// 创建axios实例
const service = axios.create({// axios中请求配置有baseURL选项,表示请求URL公共部分baseURL: import.meta.env.VITE_APP_BASE_API,// 超时timeout: 120000,withCredentials: true
})// request拦截器
service.interceptors.request.use(config => {// do somethingreturn config
}, error => {})// 响应拦截器
service.interceptors.response.use(res => {loadingInstance?.close()loadingInstance = null// 未设置状态码则默认成功状态const code = res.data.code || 200;// 获取错误信息const msg = errorCode[code] || res.data.msg || errorCode['default']if (code === 500) {ElMessage({message: msg, type: 'error'})return Promise.reject(new Error(msg))} else if (code === 401) {return refreshFun(res.config);} else if (code === 601) {ElMessage({message: msg, type: 'warning'})return Promise.reject(new Error(msg))} else if (code == 400) {// 需要用户confirm是否强制登陆return Promise.resolve(res.data)} else if (code !== 200) {ElNotification.error({title: msg})return Promise.reject('error')} else {return Promise.resolve(res.request.responseType === 'blob' ? res : res.data)}},error => {loadingInstance?.close()loadingInstance = nullif (error.response.status == 401) {return refreshFun(error.config);}let {message} = error;if (message == "Network Error") {message = "后端接口连接异常";} else if (message.includes("timeout")) {message = "系统接口请求超时";} else {message = error.response.data ? error.response.data.msg : 'message'}ElMessage({message: message, type: 'error', duration: 5 * 1000})return Promise.reject(error)}
)// 正在刷新标识,避免重复刷新
let refreshing = false;
// 请求等待队列
let waitQueue = [];function refreshFun(config) {if (refreshing == false) {refreshing = true;return useUserStore().refreshToken().then(() => {waitQueue.forEach(callback => callback()); // 已成功刷新token,队列中的所有请求重试waitQueue = [];refreshing = false;return service(config)}).catch((err) => {waitQueue = [];refreshing = false;if (err.response) {if (err.response.status === 403) {ElMessageBox.confirm('登录状态已过期(认证失败),您可以继续留在该页面,或者重新登录', '系统提示', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning'}).then(() => {useUserStore().logoutClear();router.push(`/login`);}).catch(() => {});return Promise.reject()} else {console.log('err:' + (err.response && err.response.data.msg) ? err.response.data.msg : err)}} else {ElMessage({message: err.message,type: 'error',duration: 5 * 1000})}})} else {// 正在刷新token,返回未执行resolve的Promise,刷新token执行回调return new Promise((resolve => {waitQueue.push(() => {resolve(service(config))})}))}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/35531.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全球最快的 JSON 文件查询工具

本文字数&#xff1a;1684&#xff1b;估计阅读时间&#xff1a;5分钟 审校&#xff1a;庄晓东&#xff08;魏庄&#xff09; 本文在公众号【ClickHouseInc】首发 介绍 在 ClickHouse&#xff0c;我们热衷于基准测试和性能优化。所以当我第一次看到 Hacker News 上那篇“查询大…

代码随想录算法训练营day31|134.加油站、135. 分发糖果、406.根据身高重建队列

134.加油站 如下图所示&#xff1a; 当索引一道2的时候&#xff0c;剩余油量的总量13-6 < 0&#xff0c;这个时候说明以索引0为起点不合适&#xff0c;将起点更新为索引3. 两点证明&#xff1a; 1.如果我们从蓝色段中间选一个点开始&#xff0c;是不是最后sumGas就不小于0…

从灵感到成品:使用AI生成博客文章的完整指南

在信息爆炸的时代&#xff0c;每个人都有讲述自己故事的权利和需求。博客作为一种表达方式&#xff0c;不仅能记录个人经历&#xff0c;还能分享知识和观点。然而&#xff0c;许多人在写博客文章时&#xff0c;常常会遇到灵感枯竭、时间不够用或者不知道如何开始等问题。幸运的…

光伏储能系统/安科瑞DTSD1352-CF双向计量表-安科瑞 蒋静

1 长期以来&#xff0c;我国施行居民用电低价政策&#xff0c;居民电价大幅低于供电成本&#xff0c;虽然实施了全天分三时段的阶梯电价政策&#xff0c;但过去近10年中高峰节电的效果却不够明显。从分时用电运作机制来看&#xff0c;居民用电价格的波动幅度不大&#xff0c;但…

华为云鲲鹏架构docker部署2048小游戏

华为云鲲鹏架构docker部署2048小游戏 1. 鲲鹏架构ESC2. 配置docker3. 上传2048镜像4. 删除容器,镜像 1. 鲲鹏架构ESC 2. 配置docker 安装dockeryum -y install docker开机启动 systemctl enable docker启动docker服务 systemctl start docker查询docker的运行版本 docker -v3…

时序分析基本概念介绍——min pulse width 最小脉冲宽度

文章目录 前言一、什么是 min pulse width&#xff1f;二、为什么检查 min pulse width&#xff1f;三、如何设置 min pulse width约束&#xff1f;1. 在sdc里面定义2. library里面定义 四、如何检查 min pulse width&#xff1f;五、如何修复 min pulse width&#xff1f;总结…

docker启动ws-scrcpy和redroid记录

git克隆最新的ws-scrcpy代码 git clone gitgithub.com:NetrisTV/ws-scrcpy.git进入ws-scrcpy目录新建Dockerfile文件&#xff0c;内容如下 FROM node:16-alpine WORKDIR /appRUN npm config set registry http://mirrors.cloud.tencent.com/npm/ RUN npm install -g node-gyp…

攻防世界-Web题目1

目录 cookie 1、题目 2、知识点 3、思路 get_post 1、题目 2、知识点 3、思路 disabled_button 1、题目 2、知识点 3、思路 backup 1、题目 2、知识点 3、思路 cookie 1、题目 2、知识点 cookie&#xff0c;数据包 3、思路 题目提示我们cookie&#xff0c;抓…

Markdown中如何插入空行和空格

Markdown 是一种轻量级的标记语言&#xff0c;它的主要目标是以易读易写为优先&#xff0c;并兼容 HTML。虽然 Markdown 本身对于排版的要求比较宽松&#xff0c;但在某些情况下&#xff0c;我们可能需要在文档中插入空行或空格来达到特定的排版效果。 插入空行 在Markdown中…

【ai】trition:tritonclient.utils.shared_memory 仅支持linux

Can’t find tritonclient.utils.shared_memory on WIN10 #4149yolov4的python客户端 导入以后,windows 的pycharm 就是看不到折腾了很久:SaviorEnv 环境下安装tritonclient[all]也会失败 (base) C:\Users\zhangbin>conda create -n SaviorEnv python=3.8 Collecting pack…

ubuntu 18 虚拟机安装(1)

ubuntu 18 虚拟机安装 ubuntu 18.04.6 Ubuntu 18.04.6 LTS (Bionic Beaver) https://releases.ubuntu.com/bionic/ 参考&#xff1a; 设置固定IP地址 https://blog.csdn.net/wowocpp/article/details/126160428 https://www.jianshu.com/p/1d133c0dec9d ubuntu-18.04.6-l…

元数据管理的发展历程你了解吗?元数据管理要克服哪些挑战?

在当今的信息化时代&#xff0c;数据的价值已被广泛认可&#xff0c;而元数据作为描述数据的数据&#xff0c;其作用日益凸显。元数据管理&#xff0c;作为确保数据质量、促进数据共享和提高数据透明度的关键环节&#xff0c;对企业的数据战略至关重要。随着技术的发展&#xf…

程序设计中对内存分配管理的思考,进程内存、线程内存、共享池、栈、堆

设计一个程序&#xff0c;要考虑如何分配和管理内存&#xff0c;以下是对所有内存分配和管理类型的总结。 第一、进程级的内存资源&#xff0c;也叫全局静态内存&#xff0c;其生命周期是伴随整个进程的运行期间&#xff0c;可以用作在进程范围内共享数据的方法。对应于C语言的…

C#1.0-11.0所有历史版本主要特性总结

文章目录 前言名词解释主要版本一览表各版本主要特性一句话总结 C# 1.0 (Visual Studio 2002, .Net Framework 1.0)C# 2.0 (Visual Studio 2005, .Net Framework 2.0)C# 3.0 (Visual Studio 2008, .Net Framework 3.0)C# 4.0 (Visual Studio 2010, .Net Framework 4)C# 5.0 (V…

Bigram 分词学习

Bigram 分词&#xff1a;概念、应用与中文实践 Bigram 分词是一种基础而有效的文本处理技术&#xff0c;特别是在自然语言处理中有着广泛的应用。本文将详细解释 Bigram 分词的概念、它在各个领域的应用&#xff0c;以及在中文处理中的独特优势和实践。 什么是 Bigram 分词&a…

DataWhale - 吃瓜教程学习笔记(三)

学习视频&#xff1a;第3章-对数几率回归_哔哩哔哩_bilibili 西瓜书对应章节&#xff1a; 3.3 对数几率回归 sigmoid函数 极大似然估计建模 信息论 以概率论、随机过程为基本研究工具&#xff0c;研究广义通信系统的整个过程 - 信息熵 &#xff08;信息期望&#xff09; 度…

Windows bat 提取多个目录下的文件,到一个目录

批处理命令 echo off setlocalrem 设置源目录和目标目录 set "sourceDirE:\motrix" set "targetDirE:\新建文件夹"rem 创建目标目录&#xff0c;如果不存在 if not exist "%targetDir%" mkdir "%targetDir%"rem 循环遍历源目录中的所…

TCP:TCP连接的建立与终止

TCP连接的建立与终止 建立连接第一次握手第二次握手第三次握手 终止连接第一次挥手第二次挥手第三次挥手第四次挥手 T C P是一个面向连接的协议。无论哪一方向另一方发送数据之前&#xff0c;都必须先在双方之间建立一条连接。本文将详细讨论一个T C P连接是如何建立的以及通信…

2024年能源电力行业CRM研究报告

中国能源电力行业属于大制造业的重要组成部分&#xff0c;在国民经济中的地位举足轻重。据统计&#xff0c;近十年来能源电力行业的整体投资呈现出增长趋势&#xff0c;尤其是“十四五”期间增长显著&#xff0c;2022年全国主要电力企业共完成投资12470亿元&#xff0c;同比增长…

STM32中挂在APB1(低速)和APB2(高速)上的外设

在STM32中&#xff0c; 连接在APB1(低速外设)上的设备有&#xff1a; 电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3、SPI2、窗口看门狗、Timer2、Timer3、Timer4 。 连接在APB2(高速外设)上的设备有&#xff1a; GPIO_A-E、USART1、ADC1、ADC2、ADC3、TIM1、TIM…