使用JWT双令牌机制进行接口请求鉴权

在前后端分离的开发过程中,前端发起请求,调用后端接口,后端在接收请求时,首先需要对收到的请求鉴权,在这种情况先我们可以采用JWT机制来鉴权。

JWT有两种机制,单令牌机制和双令牌机制。

单令牌机制服务端只生成一个 token,一般过期时间比较长,因此安全性稍差。

双令牌机制服务端生成两个token,一个access_token用来鉴权,过期时间一般较短(5分钟,15分钟等),另一个 refresh_token,只用来获取新的 access_token,过期时间较长(可设置为24小时或者更长)

单令牌机制一般步骤:

1.前端发起登录请求

2.服务端验证用户名密码,验证通过则下发token返回给前端。

3.前端收到返回的token进行保存。

4.前端后续请求头将携带 token 供服务端验证。

5.服务端收到请求首先验证 token,验证通过则正常提供服务,不通过则返回相应提示信息。

6.token 过期后重新登录。

双令牌机制与单令牌机制不同的是服务端生成两个token,一个access_token用来鉴权,一个 refresh_token 用来刷新 access_token

双令牌机制一般步骤:

1.前端发起登录请求

2.服务端验证用户名密码,验证通过则下发access_token和refresh_token 返回给前端。

3.前端收到返回的两个 token进行保存。

4.前端后续请求头将携带 access_token供服务端验证。

5.服务端收到请求首先验证 token,验证通过则正常提供服务,不通过则返回相应提示信息。

6.access_token过期后,前端请求头 将 access_token替换为  refresh_token,调用刷新 access_token的接口。获取到新的  access_token并保存,重新发起请求。

1.pom.xml添加 java-jwt

<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.1</version>
</dependency>

2.编写JwtUtil.java

import java.util.Calendar;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JwtUtil {private final static String SIGNATURE = "sdf46fsdf"; //密钥private final static String ACCESS_TYPE = "access";private final static String REFRESH_TYPE = "refresh";private final static String SCOPE = "cms";private static int accessExpire = 15*60; //15分钟private static int refreshExpire = 60*60*24; //24小时/*** 生成  访问 token* @return 返回token*/public static String getAccessToken( String identity){JWTCreator.Builder builder = JWT.create();builder.withClaim("type", ACCESS_TYPE);builder.withClaim("identity", identity);builder.withClaim("scope", SCOPE);Calendar instance = Calendar.getInstance();instance.add(Calendar.SECOND, accessExpire);builder.withExpiresAt(instance.getTime());return builder.sign(Algorithm.HMAC256(SIGNATURE)).toString();}/*** 生成 刷新 token* @return 返回token*/public static String getRefreshToken(String identity){JWTCreator.Builder builder = JWT.create();builder.withClaim("type", REFRESH_TYPE);builder.withClaim("identity", identity);builder.withClaim("scope", SCOPE);Calendar instance = Calendar.getInstance();instance.add(Calendar.SECOND, refreshExpire);builder.withExpiresAt(instance.getTime());return builder.sign(Algorithm.HMAC256(SIGNATURE)).toString();}/*** 验证token* @param token*/public static void verify(String token){JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);}/*** 获取token中payload* @param token* @return*/public static DecodedJWT getToken(String token){return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);}
}

3.过滤器校验token

import org.hibernate.annotations.common.util.StringHelper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qyp.hpa.util.JwtUtil;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.*;
@Component
public class FilterConfig implements HandlerInterceptor{public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)throws Exception {}public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2)throws Exception {}public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));response.setHeader("Access-Control-Allow-Methods", "*");response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Allow-Headers", "Authorization,Origin, X-Requested-With, Content-Type, Accept,Access-Token,Content-length,Content-Range,Keep-Alive,Accept-Ranges,Connection");//Origin, X-Requested-With, Content-Type, Accept,Access-TokenString requestURL = request.getRequestURL().toString();if(requestURL.indexOf("login")!=-1 || requestURL.indexOf("refresh")!=-1){return true;}Map<String,Object> map = new HashMap<>();//令牌建议是放在请求头中,获取请求头中令牌AuthorizationString token = request.getHeader("Authorization");try{if(StringHelper.isEmpty(token)){map.put("msg","token不能为空");return false;}JwtUtil.verify(token);//验证令牌return true;//放行请求} catch (SignatureVerificationException e) {e.printStackTrace();map.put("msg","无效签名");} catch (TokenExpiredException e) {e.printStackTrace();map.put("code","10051");map.put("msg","token过期");} catch (AlgorithmMismatchException | JWTDecodeException | InvalidClaimException e) {e.printStackTrace();map.put("code","10041");map.put("msg","token算法不一致");} catch (Exception e) {e.printStackTrace();map.put("msg","token失效");}map.put("state",false);//设置状态//将map转化成json,response使用的是JacksonString json = new ObjectMapper().writeValueAsString(map);response.setContentType("application/json;charset=UTF-8");response.getWriter().print(json);return true;}
}

4.登录接口、刷新令牌接口

服务端登录接口

@RequestMapping(value="sys/login", method = RequestMethod.POST)public Map<String,Object> logIn(@RequestBody Map<String,Object> param) {Map<String,Object> map = new HashMap<String,Object>();String username = param.get("username")==null?"":param.get("username").toString();String password = param.get("password")==null?"":param.get("password").toString();try{//为了方便演示,直接写了固定的用户名和密码if("admin".equals(username) && "123456".equals(password)){String userId = "sadfsdfsdfsdfs";//生成JWT令牌String token = JwtUtil.getAccessToken(userId);String refresh_token = JwtUtil.getRefreshToken(userId);map.put(NS.MSG, "登录成功");map.put("access_token", token);map.put("refresh_token", refresh_token);map.put(NS.SUCC, NS.OK);}else {map.put(NS.MSG, NS.NO_USER);map.put(NS.SUCC, NS.ERROR);}} catch(Exception e){map.put(NS.SUCC, NS.ERROR);e.printStackTrace();}return map;}

服务端刷新令牌接口 

/*** 刷新令牌*/@GetMapping("sys/refresh")public Map<String,Object> getRefreshToken(HttpServletRequest request) {Map<String,Object> map = new HashMap<String,Object>();String tokenStr = request.getHeader("Authorization");DecodedJWT token = JwtUtil.getToken(tokenStr);String id = token.getClaim("identity").asString();//生成JWT令牌String access_token = JwtUtil.getAccessToken(id);map.put("access_token", access_token);return map;}

5. Vue前端调用

前端这里使用的是 vue3,以下是调用服务端登录接口,登录成功保存  access_token和 refresh_token

import { saveTokens
} from '@/util/token'
import { login} from '@/util/user'const _login= async () =>{const param = {username: 'admin',password: '123456',}const res:any = await login(param);console.log('login res', res);if(res.succ == '1'){saveTokens(res.access_token,res.refresh_token)}}

axios.js 封装处理  access_token过期,无感刷新 token

axios.js 封装处理当前端调用刷新 token的接口时,将请求头 headers.Authorization 字段替换为 refresh_token

T

util / token.js 工具类 

/*** 存储tokens* @param {string} accessToken* @param {string} refreshToken*/
export function saveTokens(accessToken, refreshToken) {localStorage.setItem('access_token', accessToken)localStorage.setItem('refresh_token', refreshToken)
}
/*** 存储access_token* @param {string} accessToken*/
export function saveAccessToken(accessToken) {localStorage.setItem('access_token', accessToken)
}
/*** 获得某个token* @param {string} tokenKey*/
export function getToken(tokenKey) {return localStorage.getItem(tokenKey)
}
/*** 移除token*/
export function removeToken() {localStorage.removeItem('access_token')localStorage.removeItem('refresh_token')
}

util / user.js 配置请求服务端登录接口

import {post,get} from '@/util/axios'
export async function login(data) {return await post('sys/login', data)
}

axios.js 完整代码

/*** 封装 axios*/
import axios from 'axios'
import {Message} from 'view-ui-plus'
import { getToken, saveAccessToken } from '@/util/token'
const baseUrl = "http://localhost:8089/"
const ErrorCode = {777: '前端错误码未定义',999: '服务器未知错误',10000: '未携带令牌',10020: '资源不存在',10030: '参数错误',10041: 'assessToken损坏',10042: 'refreshToken损坏',10051: 'assessToken过期',10052: 'refreshToken过期',10060: '字段重复',10070: '不可操作',
}
const config = {baseURL: baseUrl || '',timeout: 5 * 1000, // 请求超时时间设置crossDomain: true,// withCredentials: true, // Check cross-site Access-Control// 定义可获得的http响应状态码// return true、设置为null或者undefined,promise将resolved,否则将rejectedvalidateStatus(status) {return status >= 200 && status < 510},
}
/*** 错误码是否是refresh相关* @param { number } code 错误码*/
function refreshTokenException(code) {const codes = [10000, 10042, 10050, 10052, 10012]return codes.includes(code)
}
// 创建请求实例
const _axios = axios.create(config)
_axios.interceptors.request.use(originConfig => {// 有 API 请求重新计时///autoJump(router)const reqConfig = { ...originConfig }// step1: 容错处理if (!reqConfig.url) {console.error('request need url')}reqConfig.method = reqConfig.method.toLowerCase() // 大小写容错// 参数容错if (reqConfig.method === 'get') {if (!reqConfig.params) {reqConfig.params = reqConfig.data || {}}} else if (reqConfig.method === 'post') {if (!reqConfig.data) {reqConfig.data = reqConfig.params || {}}// 检测是否包含文件类型, 若包含则进行 formData 封装let hasFile = falseObject.keys(reqConfig.data).forEach(key => {if (typeof reqConfig.data[key] === 'object') {const item = reqConfig.data[key]if (item instanceof FileList || item instanceof File || item instanceof Blob) {hasFile = true}}})// 检测到存在文件使用 FormData 提交数据if (hasFile) {const formData = new FormData()Object.keys(reqConfig.data).forEach(key => {formData.append(key, reqConfig.data[key])})reqConfig.data = formData}}// step2: permission 处理if (reqConfig.url === 'sys/refresh') {const refreshToken = getToken('refresh_token')if (refreshToken) {reqConfig.headers.Authorization = refreshToken}} else {const accessToken = getToken('access_token')if (accessToken) {reqConfig.headers.Authorization = accessToken}}return reqConfig},error => Promise.reject(error),
)
// Add a response interceptor
_axios.interceptors.response.use(async res => {if (res.status.toString().charAt(0) === '2') {return res.data}const { code, msg } = res.datareturn new Promise(async (resolve, reject) => {let tipMessage = ''const { url } = res.config// refresh_token 异常,直接登出if (refreshTokenException(code)) {/** setTimeout(() => {store.dispatch('loginOut')const { origin } = window.locationwindow.location.href = origin}, 1500)return resolve(null)*/}// assessToken相关,刷新令牌console.log('msg',msg,'code',code)if (code === "10041" || code === "10051") {console.log('--msg',msg,'code',code)const cache = {}if (cache.url !== url) {cache.url = urlconst refreshResult = await _axios('sys/refresh')saveAccessToken(refreshResult.access_token)// 将上次失败请求重发const result = await _axios(res.config)return resolve(result)}}// 弹出信息提示的第一种情况:直接提示后端返回的异常信息(框架默认为此配置);// 特殊情况:如果本次请求添加了 handleError: true,用户自行通过 try catch 处理,框架不做额外处理if (res.config.handleError) {return reject(res)}if (typeof msg === 'string') {tipMessage = msg}if (Object.prototype.toString.call(msg) === '[object Object]') {;[tipMessage] = Object.values(msg).flat()}if (Object.prototype.toString.call(msg) === '[object Array]') {;[tipMessage] = msg}//ElMessage.error(tipMessage)Message.error(tipMessage)reject(res)})},error => {if (!error.response) {//ElMessage.error('请检查 API 是否异常')console.log('error', error)}// 判断请求超时if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {//ElMessage.warning('请求超时')}return Promise.reject(error)},
)
// 导出常用函数
/*** @param {string} url* @param {object} data* @param {object} params*/
export function post(url, data = {}, params = {}) {return _axios({method: 'post',url,data,params,})
}
/*** @param {string} url* @param {object} params*/
export function get(url, params = {}) {return _axios({method: 'get',url,params,})
}
/*** @param {string} url* @param {object} data* @param {object} params*/
export function put(url, data = {}, params = {}) {return _axios({method: 'put',url,params,data,})
}
/*** @param {string} url* @param {object} params*/
export function _delete(url, params = {}) {return _axios({method: 'delete',url,params,})
}
export default _axios

6.效果展示

调用登录接口

请求

响应

调用 list 接口,请求时将 token 放在  headers的  Authorization字段即可

在 access_token过期而  refresh_token未过期时,调用任意接口会刷新  access_token 

由上图可以看到,第一次调用 list 接口时,报 token 已过期,此时会自动调用 刷新 access_token的 refresh接口,成功刷新 access_token后,再次自动调用 list  接口,成功返回,达到了无感刷新token的效果。

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

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

相关文章

环信IM x 亚马逊云科技,助力出海企业实现可靠通讯服务

随着全球化进程的加速&#xff0c;越来越多的企业选择出海&#xff0c;拓展国际市场。然而&#xff0c;面对不同国家和地区的用户&#xff0c;企业在即时通讯方面遇到了诸多挑战。为了帮助企业克服这些困难&#xff0c;环信IM与亚马逊云科技强强联手&#xff0c;共同推出了一套…

LDR6020:重塑iPad一体式有线键盘体验的创新力量

在移动办公与娱乐日益融合的时代&#xff0c;iPad凭借其强大的性能和便携性&#xff0c;成为了众多用户不可或缺的生产力工具。然而&#xff0c;为了进一步提升iPad的使用体验&#xff0c;一款高效、便捷的键盘成为了不可或缺的配件。今天&#xff0c;我们要介绍的&#xff0c;…

关于黑马商城微服务拆分

1.拆分流程 大差不差分为 创建module-依赖-启动类-配置yml文件-抽取代码-数据库-配置启动项-测试 2.微服务的好处 在测试的时候明显感觉到微服务的好处 不用启动所有的项目 只是单纯一个模块比如支付就可以自己调试 非常便捷而且易开发 抽取的公共模块api也不用启动就能测试 …

免费缺陷管理工具深度评测与使用心得

国内外主流的10款缺陷跟踪工具对比&#xff1a;PingCode、Worktile、滴答清单、CalendarTask、专注清单、Todo清单、Jira、Bugzilla、MantisBT、Redmine。 在寻找合适的缺陷管理工具时&#xff0c;很多团队面临一个共同的挑战&#xff1a;如何在有限的预算内找到既高效又易于使…

深入浅出WebRTC—ALR

ALR&#xff08;Application Limited Region&#xff09;指的是网络传输过程中&#xff0c;由于应用层的限制&#xff08;而非网络拥塞&#xff09;导致带宽未被充分利用的情况。在这种情况下&#xff0c;应用层可能因为处理能力、手动配置或其他因素无法充分利用可用带宽&…

Spring Authorization Server 自定义 OAuth2 密码模式返回数据结构优化

前言 对接了自定义密码模式&#xff0c;但是返回的数据结构不符合要求 我们需要改成下面格式 开始 我假设你已经对接好了自定义密码功能&#xff0c;不会的话看下面文章 Spring Authorization Server 1.1 扩展实现 OAuth2 密码模式与 Spring Cloud 的整合实战&#xff08;上…

像 MvvmLight 一样使用 CommunityToolkit.Mvvm 工具包

文章目录 简介一、安装工具包二、实现步骤1.按照MvvmLight 的结构创建对应文件夹和文件2.编辑 ViewModelLocator3.引用全局资源二、使用详情1.属性2.命令3. 消息通知4. 完整程序代码展示运行结果简介 CommunityToolkit.Mvvm 包(又名 MVVM 工具包,以前称为 Microsoft.Toolkit…

[数据集][目标检测]拐杖检测数据集VOC+YOLO格式2778张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2778 标注数量(xml文件个数)&#xff1a;2778 标注数量(txt文件个数)&#xff1a;2778 标注…

【iOS】—— isMemberOfClass isKindOfClass以及源码

【iOS】—— isMemberOfClass & isKindOfClass以及源码 isa指针示例源码解析&#xff1a;isKindOfClass&#xff1a;源码解析&#xff08;实例方法和类方法&#xff09;isMemberOfClass&#xff1a;源码解析&#xff08;实例方法和类方法&#xff09;源码分析总结&#xff…

【中项】系统集成项目管理工程师-第2章 信息技术发展-2.2新一代信息技术及应用-2.2.3大数据与2.2.4区块链

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

服务器证书基于 OpenSSL一键颁发脚本

文章目录 一、场景说明二、脚本职责三、参数说明四、操作示例五、注意事项 一、场景说明 本自动化脚本旨在为提高研发、测试、运维快速部署应用环境而编写。 脚本遵循拿来即用的原则快速完成 CentOS 系统各应用环境部署工作。 统一研发、测试、生产环境的部署模式、部署结构、…

vue使用了代理跨域,部署上线,使用Nginx配置出现问题,访问不到后端接口

1、如果路由的mode是history模式的要加上框框里的哪句&#xff0c;然后配置下面的location router location / {root /usr/local/app/dist/; #vue文件dist的完整路径try_files $uri $uri/ router;index index.html index.htm;}#error_page 500 502 503 504 /50x.html;lo…

数据分析入门指南:数据库入门(五)

本文将总结CDA认证考试中数据库中部分知识点&#xff0c;内容来源于《CDA模拟题库与备考资料PPT》 。 CDA认证&#xff0c;作为源自中国、面向全球的专业技能认证&#xff0c;覆盖金融、电信、零售、制造、能源、医疗医药、旅游、咨询等多个行业&#xff0c;旨在培养能够胜任数…

CVE-2023-33440(任意文件上传)

简介 Faculty Evaluation System v1.0 存在未授权任意文件上传漏洞漏洞 过程 打开靶场 进行目录扫描 发现后台login.php&#xff0c;进入查看 弱口令进行测试&#xff0c;无效&#xff0c;无法进入 根据提示是未授权访问文件上传 &#xff0c;应该是不需要登录就能触发漏洞…

openstack设置IP直接登录,不需要加dashboard后缀

openstack 实验环境&#xff0c;openstack-t版&#xff0c;centos2009 修改配置文件 [rootcontroller ~]# vim /WEBROOT /etc/openstack-dashboard/local_settings #将dashboard去掉 WEBROOT /dashboard/ #改为 WEBROOT /[rootcontroller ~]# vim /etc/httpd/conf.d/openst…

深度学习每周学习总结N4:中文文本分类-Pytorch实现(基本分类(熟悉流程)、textCNN分类(通用模型)、Bert分类(模型进阶))

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 目录 0. 总结&#xff1a;1. 基础模型a. 数据加载b. 数据预处理c. 模型搭建与初始化d. 训练函数e. 评估函数f.拆分数据集运行模型g. 结果可…

C++STL初阶(7):list的运用与初步了解

在了解了vector之后&#xff0c;我们只需要简单学习List与vector不一样的接口即可 1.list的基本接口 1.1 iterator list中&#xff0c;与vector最大的区别就是迭代器由随机迭代器变成双向迭代器 string和vector中的迭代器都是随机迭代器&#xff0c;支持-等&#xff0c;而LIS…

达梦数据库 MPP集群搭建(带主备)

MPP集群搭建&#xff08;带主备&#xff09; 1.背景2.操作内容和要求3. 具体步骤3.1 搭建过程3.1.1 集群搭建3.1.2 准备工作3.1.2.1 初始化3.1.2.2 备份数据库 3.1.3 配置主库EP013.1.3.1 配置dm.ini3.1.3.2 配置dmmal.ini3.1.3.3 配置dmarch.ini3.1.3.4 配置dmmpp.ctl3.1.3.5 …

Golang | Leetcode Golang题解之第263题丑数

题目&#xff1a; 题解&#xff1a; var factors []int{2, 3, 5}func isUgly(n int) bool {if n < 0 {return false}for _, f : range factors {for n%f 0 {n / f}}return n 1 }

Linux云计算 |【第一阶段】SERVICES-DAY4

主要内容&#xff1a; DHCP概述、PXE批量装机、配置PXE引导、Kickstart自动应答、Cobbler装机平台 一、DHCP服务概述及原理 DHCP动态主机配置协议&#xff08;Dynamic Host Configuration Protocol&#xff09;&#xff0c;由IETF&#xff08;Internet网络工程师任务小组&…