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

news2024/9/20 10:47:52

在前后端分离的开发过程中,前端发起请求,调用后端接口,后端在接收请求时,首先需要对收到的请求鉴权,在这种情况先我们可以采用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-Token
        String requestURL = request.getRequestURL().toString();
        if(requestURL.indexOf("login")!=-1 || requestURL.indexOf("refresh")!=-1){
        	return true;
        }
        Map<String,Object> map = new HashMap<>();
        //令牌建议是放在请求头中,获取请求头中令牌Authorization
        String 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使用的是Jackson
        String 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,否则将rejected
  validateStatus(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 = false
      Object.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.data
    return new Promise(async (resolve, reject) => {
      let tipMessage = ''
      const { url } = res.config
      // refresh_token 异常,直接登出
      if (refreshTokenException(code)) {
        /** 
        setTimeout(() => {
          store.dispatch('loginOut')
          const { origin } = window.location
          window.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 = url
          const 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.coloradmin.cn/o/1936541.html

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

相关文章

JAVA 异步编程(线程安全)二

1、线程安全 线程安全是指你的代码所在的进程中有多个线程同时运行&#xff0c;而这些线程可能会同时运行这段代码&#xff0c;如果每次运行的代码结果和单线程运行的结果是一样的&#xff0c;且其他变量的值和预期的也是一样的&#xff0c;那么就是线程安全的。 一个类或者程序…

Linux驱动开发-06蜂鸣器和多组GPIO控制

一、控制蜂鸣器 1.1 控制原理 我们可以看到SNVS_TAMPER1是这个端口在控制着蜂鸣器,同时这是一个PNP型的三极管,在端口输出为低电平时,蜂鸣器响,在高电平时,蜂鸣器不响 1.2 在Linux中端口号的控制 gpiochipX:当前SoC所包含的GPIO控制器,我们知道I.MX6UL/I.MX6ULL一共包…

整顿职场?安全体系建设

本文由 ChatMoney团队出品 00后整顿职场&#xff0c;职场到底怎么了&#xff1f;无压力、无忧虑的00后可以直接开整&#xff0c;那绝大部分打工人寒窗苦读、闯过高考&#xff0c;艰辛毕业&#xff0c;几轮面试杀入职场&#xff0c;结婚买房、上有老下有小&#xff0c;就活该再被…

怎么剪辑音频文件?4款适合新的音频剪辑软件

是谁还不会音频剪辑&#xff1f;无论是个人音乐爱好者&#xff0c;还是专业音频工作者&#xff0c;我们都希望能找到一款操作简便、功能强大且稳定可靠的音频剪辑工具。今天&#xff0c;我就要为大家带来四款热门音频剪辑软件的体验感分享。 一、福昕音频剪辑 福昕音频剪辑是…

JUnit 单元测试

JUnit 测试是程序员测试&#xff0c;就是白盒测试&#xff0c;可以让程序员知道被测试的软件如何 &#xff08;How&#xff09;完成功能和完成什么样&#xff08;What&#xff09;的功能。 下载junit-4.12和hamcrest-core-1.3依赖包 相关链接 junit-4.12&#xff1a;Central …

【JavaScript 算法】最长公共子序列:字符串问题的经典解法

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、算法原理状态转移方程初始条件 二、算法实现注释说明&#xff1a; 三、应用场景四、总结 最长公共子序列&#xff08;Longest Common Subsequence&#xff0c;LCS&#xff09;是字符串处理中的经典问题。给定两个字符串…

Go语言之参数传递

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 修改参数 假设你定义了一个函数&#xff0c;并在函数里对参数进行…

string相关

int main() {// world替换成 xxxxxxxxxxxxxxxxxxxxxxstring s1("hello world hello bit");s1.replace(6, 5, "xxxxxxxxxxxxxxxxxxxxxx");cout << s1 << endl;s1.replace(6, 23, "yyyyy");cout << s1 << endl;// 所有空格…

C++ | Leetcode C++题解之第260题只出现一次的数字III

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> singleNumber(vector<int>& nums) {int xorsum 0;for (int num: nums) {xorsum ^ num;}// 防止溢出int lsb (xorsum INT_MIN ? xorsum : xorsum & (-xorsum));int type1 0, …

类和对象:构造函数

构造函数是特殊的成员函数&#xff0c;需要注意的是&#xff0c;构造函数虽然名称叫构造&#xff0c;但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时&#xff0c;空间就开好了)&#xff0c;⽽是对象实例化时初始化对象。构造函数的本质是要替代…

通过splunk web服务将服务器上文件下载到本地

1. 需求说明 工作中经常遇到需要将服务器上的文件下载到本地&#xff0c;但是由于各种网络环境限制&#xff0c;没办法使用winscp或者xftp工具&#xff0c;那么如何将服务器上的文件下载下来呢&#xff1f; 这里提供一种思路: 如果服务器上安装有web服务&#xff0c;可将待下…

MongoDB教程(十四):MongoDB查询分析

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、查询分…

防火墙之内容安全过滤技术篇

深度行为检测技术&#xff1a;是一种基于应用层的流量检测和控制技术 DPI:针对完整的数据包&#xff0c;进行内容的识别和检测 基于应用网关的检测技术 --- 有些应用控制和数据是分离的&#xff0c;比如一些视频流。一开始会通过TCP协议链接之后&#xff0c;协商一些参数&#…

扩容升级丨极海正式推出G32A1465系列汽车通用MCU,驱动智驾再进阶

继2023年推出G32A系列汽车通用平台首发产品G32A1445系列后&#xff0c;极海宣布正式推出G32A1465系列全新汽车通用MCU&#xff0c;以满足日益增长的智能驾驶应用需求。作为升级迭代产品&#xff0c;G32A1465专为应用范围不断扩大的高运算要求而设计&#xff0c;集成丰富的通信接…

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

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

【Vue】深入了解 Axios 在 Vue 中的使用:从基本操作到高级用法的全面指南

文章目录 一、Axios 简介与安装1. 什么是 Axios&#xff1f;2. 安装 Axios 二、在 Vue 组件中使用 Axios1. 发送 GET 请求2. 发送 POST 请求 三、Axios 拦截器1. 请求拦截器2. 响应拦截器 四、错误处理五、与 Vuex 结合使用1. 在 Vuex 中定义 actions2. 在组件中调用 Vuex acti…

Elasticsearch:Retrievers 介绍 - Python Jupyter notebook

在今天的文章里&#xff0c;我是继上一篇文章 “Elasticsearch&#xff1a;介绍 retrievers - 搜索一切事物” 来使用一个可以在本地设置的 Elasticsearch 集群来展示 Retrievers 的使用。在本篇文章中&#xff0c;你将学到如下的内容&#xff1a; 从 Kaggle 下载 IMDB 数据集…

[米联客-安路飞龙DR1-FPSOC] FPGA基础篇连载-21 VTC视频时序控制器设计

软件版本&#xff1a;Anlogic -TD5.9.1-DR1_ES1.1 操作系统&#xff1a;WIN10 64bit 硬件平台&#xff1a;适用安路(Anlogic)FPGA 实验平台&#xff1a;米联客-MLK-L1-CZ06-DR1M90G开发板 板卡获取平台&#xff1a;https://milianke.tmall.com/ 登录“米联客”FPGA社区 ht…

SpringDoc2问题汇总

在项目中尝试使用SpringDoc进行文档生成&#xff0c;在使用过程中遇到一系列的问题加以记录. 1.引入依赖 只是单纯的使用SpringDoc的话不需要引入一些乱七八糟的依赖&#xff0c;如今各种增强和拓展依赖层出不穷&#xff0c;但是随着这些依赖的出现带来的不仅是增强&#xff0…

【BUG】已解决:ModuleNotFoundError: No module named ‘PIL‘

已解决&#xff1a;ModuleNotFoundError: No module named ‘PIL‘ 目录 已解决&#xff1a;ModuleNotFoundError: No module named ‘PIL‘ 【常见模块错误】 错误原因&#xff1a; 解决办法&#xff1a; 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我…