1.为什么要做双token?
双token系统通常用于提高安全性和分离不同级别的权限。在这种系统中,通常会有两个token:
-
Access Token(业务token):这是用户直接使用来访问资源的token。它的有效期较短,一旦过期,用户需要重新认证来获取新的access token。这样做的好处是即使access token被泄露,由于其有效期短,攻击者利用它进行不当操作的时间窗口有限。
-
Refresh Token (刷新token):Refresh token是用来在access token过期后重新获取新的access token的。它的有效期通常较长,甚至可以说是永久的。但是,refresh token通常不会直接发送给客户端,而是保存在服务器端。当需要刷新access token时,客户端通过提供refresh token来请求新的access token。
此外,使用双token的原因还包括:
-
安全性:通过短期的access token和长期的refresh token,可以在不影响用户体验的情况下,减少安全风险。即使access token被盗,由于其有效期短,损害可以被控制在一定范围内。
-
权限管理:双token系统可以更好地管理用户的权限。例如,在开放平台中,第三方应用可以通过refresh token来获取access token,而不需要知道用户的用户名和密码,这样既保证了用户信息的安全,又赋予了第三方应用一定的权限。
-
用户体验:双token系统可以在不影响用户体验的前提下,实现后台的安全管理和策略调整。用户不需要频繁登录,同时也能保证系统的安全性。
-
灵活性:在需要进行更细粒度的权限控制或者策略调整时,双token系统提供了更多的灵活性。例如,可以在不改变refresh token的情况下,调整access token的有效期或权限范围。
总之,双token系统是一种常见的安全设计模式,通过分离短期和长期凭证,以及用户直接使用的token和用于刷新的token,来提高系统的安全性和灵活性。这种设计特别适用于需要与第三方应用共享权限而又不泄露用户敏感信息的场景。
2.前端代码实现 (不做要求,复制过去即可)
const refreshToken = async () => {
return await axios.get('/apis/bm-member-service-app/api/member/refresh_token', {
params: {
refreshToken: beimao_store.refreshToken
}
})
.then(function (response) {
const { code, msg, data } = response.data
if (code != 0) {
return Promise.reject(new Error(msg));
}
beimao_store.token = data.token;
beimao_store.refreshToken = data.refreshToken;
return Promise.resolve("成功");
})
.catch(function (error) {
return Promise.reject(new Error(error));
});
}
axios完整代码:
import axios from 'axios'
import { showNotify, closeNotify } from 'vant';
import { beimaoStore } from '@/store/pinia'
const beimao_store =beimaoStore()
interface IResponseData<T> {
code: number,
message: string,
data: T
}
const refreshToken = async () => {
return await axios.get('/apis/bm-member-service-app/api/member/refresh_token', {
params: {
refreshToken: beimao_store.refreshToken
}
})
.then(function (response) {
const { code, msg, data } = response.data
if (code != 0) {
return Promise.reject(new Error(msg));
}
beimao_store.token = data.token;
beimao_store.refreshToken = data.refreshToken;
return Promise.resolve("成功");
})
.catch(function (error) {
return Promise.reject(new Error(error));
});
}
// let loading:any;
class Http {
myAxios: any;
constructor(config: any) {
this.myAxios = axios.create(config);
// 添加请求拦截器
this.myAxios.interceptors.request.use(function (config:any) {
//显示loading层
showNotify({ type: 'warning', message: '加载中...', duration: 0 });
// to do
if (beimao_store.token) {
config.headers.token = beimao_store.token
}
if (beimao_store.traceId) {
config.headers.traceId = beimao_store.traceId
}
return config;
}, function (error:any) {
// 对请求错误做些什么
closeNotify();
return Promise.reject(error);
});
let that=this;
// 添加响应拦截器
this.myAxios.interceptors.response.use(async function (response:any) {
//关闭loading层
closeNotify();
const {code,msg,data} = response.data
if(code === 0){
return data;
} else if (code == undefined){
return response;
//457 token过期
} else if (code == 457){
try{
await refreshToken()
return that.myAxios(response.config)
}
catch(error:any){
showNotify({ type: 'danger', message: error.msg });
return Promise.reject(error);
}
// window.location.href = '/login';
}else if(code == 456){
showNotify({ type: 'danger', message: msg });
// window.location.href = '/login';
}
else if(code != 0){
showNotify({ type: 'danger', message: msg });
return Promise.reject(msg);
}
}, function (error:any) {
// 对响应错误做点什么
closeNotify();
return Promise.reject(error);
});
}
get<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.myAxios.get(url, { params, ...data });
}
post<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.myAxios.post(url, params, data);
}
put<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.myAxios.put(url, params, data);
}
delete<T>(url: string, params?: object, data = {}): Promise<IResponseData<T>> {
return this.myAxios.delete(url, { params, ...data });
}
}
const config = {
baseURL: '',
timeout: 30 * 1000,
withCredentials: true,
}
export default new Http(config);
3.后端代码
controller:
/*
* Copyright (c) 2020, 2024, All rights reserved.
*
*/
package com.beimao.controller;
import com.beimao.model.LoginInfo;
import com.beimao.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/member/login")
public class LoginController {
@Autowired
private LoginService loginService;
/**
* 登录
*
* @param loginInfo
* @return
*/
@PostMapping()
public Map<String, String> login(@RequestBody LoginInfo loginInfo) {
return loginService.login(loginInfo);
}
/**
* 刷新token
*
* @param refreshToken
* @return
*/
@GetMapping("/refresh_token")
public Map<String, String> refresh_token(String refreshToken) {
return loginService.refreshToken(refreshToken);
}
}
service:
package com.beimao.service;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSignerUtil;
import cn.smart.core.exception.BizException;
import cn.smart.core.model.LocalUser;
import com.beimao.dao.LoginDao;
import com.beimao.dao.MemberDao;
import com.beimao.model.LoginInfo;
import com.beimao.model.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class LoginService {
@Autowired
private LoginDao loginDao;
@Value("${cn.smart.tokenx.key}")
private String key;
/**
* 登录
*
* @param
* @return
*/
public Map<String,String> login(LoginInfo loginInfo) {
//1.手机号是否为空,格式是否正确
if (ObjUtil.isEmpty(loginInfo.getTel())) {
log.debug("手机号不能为空");
throw new BizException(461, "手机号不能为空");
}
if (!Validator.isMobile(loginInfo.getTel())) {
log.debug("手机号格式不正确");
throw new BizException(462, "手机号格式不正确");
}
//查询数据库进行登录
List<Member> memberList = loginDao.select(loginInfo);
if (ObjUtil.isEmpty(memberList)) {
log.debug("手机号或密码错误");
throw new BizException(463, "手机号或密码错误");
}
Member member = memberList.get(0);
return token(member);
}
/**
* 生成token(长期token与短期token)
* @param member
* @return
*/
public Map<String,String> token(Member member) {
Map<String ,String > tokenMap = new HashMap<>();
//进行jwt
//生成短期token
Map<String, Object> map = new HashMap<String, Object>() {
private static final long serialVersionUID = 1L;
{
put("id", member.getId());
put("nickName", member.getNickName());
put("exp", System.currentTimeMillis() / 1000 + 10); //10秒
}
};
String token = JWTUtil.createToken(map, key.getBytes());
tokenMap.put("token",token);
//生成长期token
Map<String, Object> map1 = new HashMap<String, Object>() {
private static final long serialVersionUID = 1L;
{
put("id", member.getId());
put("exp", System.currentTimeMillis() / 1000 + 60 * 60 * 24 *90 ); //90天
}
};
String refreshToken = JWTUtil.createToken(map1, key.getBytes());
tokenMap.put("refreshToken",refreshToken);
return tokenMap;
}
/**
* 刷新token
* @param refreshToken
* @return
*/
public Map<String,String> refreshToken(String refreshToken) {
//1.校验refreshToken是否为空
if (ObjUtil.isEmpty(refreshToken)) {
log.debug("refreshToken不能为空");
throw new BizException(462, "refreshToken不能为空");
}
boolean b = false;
// 验证算法,JWTValidator包含过期的验证,验证比较全面
try {
JWTValidator.of(refreshToken).validateAlgorithm(JWTSignerUtil.hs256(key.getBytes())).validateDate();
b = true;
}catch (Exception ex){
ex.printStackTrace();
}
if(!b){
throw new BizException(457,"token不正确");
}
//解析refreshToken获取id,查询数据将用户必要的信息放入token中
JSONObject jsonObject = JSONUtil.toBean(JWTUtil.parseToken(refreshToken).getPayload().toString(), JSONObject.class);
Integer id = jsonObject.getInt("id");
Member member = loginDao.selectById(id);
return token(member);
}
}
dao:
package com.beimao.dao;
import com.beimao.model.LoginInfo;
import com.beimao.model.Member;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface LoginDao {
List<Member> select(LoginInfo loginInfo);
Member selectById(Integer id);
}
mappper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.beimao.dao.LoginDao">
<select id="select" resultType="com.beimao.model.Member">
select * from member
<where>
<if test="tel != null">
tel = #{tel}
</if>
<if test="password != null">
and password = #{password}
</if>
</where>
</select>
<select id="selectById" resultType="com.beimao.model.Member">
select id,nickName from member where id = #{id} limit 1
</select>
</mapper>