vue中使用雙token機制
一、什么是雙token機制?
雙 Token 機制是一種增強身份驗證安全性(主)和提升用戶體驗(次)的技術(shù)方案,常用于處理用戶登錄和會話管理,主要包含訪問令牌(Access Token)和刷新令牌(Refresh Token)
二、基本概念
- 訪問令牌(Access Token):短token
- 這是用于訪問受保護資源的短期令牌,通常有效期較短,比如 15 分鐘到 1 小時不等。
- 它包含了用戶的身份信息和權(quán)限聲明,服務器通過驗證該令牌來確認用戶是否有權(quán)限訪問特定資源。
- 由于有效期短,即使被竊取,攻擊者利用它進行惡意操作的時間窗口也有限。
- 刷新令牌(Refresh Token):長token
- 刷新令牌的有效期較長,可能是幾天甚至幾周。
- 其作用是在訪問令牌過期后,用于獲取新的訪問令牌,而無需用戶重新登錄。
- 刷新令牌通常存儲在更安全的位置,如 HTTP - Only Cookie 中,以降低被竊取的風險。
三、工作流程
- 用戶登錄:用戶在客戶端輸入用戶名和密碼等憑據(jù)進行登錄。服務器驗證這些憑據(jù),若驗證通過,會生成一個訪問令牌和一個刷新令牌,并將它們返回給客戶端。
- 訪問資源:客戶端在后續(xù)請求中攜帶訪問令牌,服務器驗證該令牌的有效性。如果令牌有效,服務器處理請求并返回響應。
- 訪問令牌過期:當訪問令牌過期后,客戶端在下次請求時會收到服務器返回的 “令牌過期” 錯誤。
- 刷新令牌:客戶端使用刷新令牌向服務器發(fā)起刷新請求。服務器驗證刷新令牌的有效性,如果有效,會生成一個新的訪問令牌,并返回給客戶端。
- 新的訪問資源:客戶端使用新的訪問令牌繼續(xù)訪問受保護資源。
- 刷新令牌過期:當刷新令牌也過期時,用戶需要重新登錄以獲取新的訪問令牌和刷新令牌。
因為 access_token 如果有效期太短,用戶就需要頻繁地進行身份驗證,用戶體驗差。設置得太長呢,一旦 access token 被獲取之后被冒用的可能性大。所以使用 refresh token 就可以把access token 的有效期縮短,在提高安全性的同時還保證了用戶體驗。
四、具體實現(xiàn)
這里前端采用 vue3 + axios,后端采用 nest.js 實現(xiàn)
1. 創(chuàng)建后端登錄接口
要求:登錄成功返回兩個 token,一個用于刷新 token(refresh token),一個用于訪問 token(access token)。
1.1 創(chuàng)建 access、refresh token 模塊
Config 模塊用來讀取
.env文件配置
// src/module/jwt-access.module.ts
import { Module } from '@nestjs/common'
import { JwtModule, JwtService } from '@nestjs/jwt'
import { ConfigModule, ConfigService } from '@nestjs/config'
import {
JWT_ACCESS_EXPIRES_IN,
JWT_ACCESS_SECRET
} from '../common/constant/env'
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>(JWT_ACCESS_SECRET),
signOptions: {
expiresIn: configService.get<string>(JWT_ACCESS_EXPIRES_IN)
}
})
})
],
providers: [
{
// 創(chuàng)建一個別名,方便在 auth.service.ts 中注入
provide: 'JWT_ACCESS',
useExisting: JwtService
}
],
exports: [JwtModule, 'JWT_ACCESS']
})
export class JwtAccessModule {}
// src/module/jwt-refresh.module.ts
import { Module } from '@nestjs/common'
import { JwtModule, JwtService } from '@nestjs/jwt'
import { ConfigModule, ConfigService } from '@nestjs/config'
import {
JWT_REFRESH_EXPIRES_IN,
JWT_REFRESH_SECRET
} from '../common/constant/env'
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>(JWT_REFRESH_SECRET),
signOptions: {
expiresIn: configService.get<string>(JWT_REFRESH_EXPIRES_IN)
}
})
})
],
providers: [
{
provide: 'JWT_REFRESH',
useExisting: JwtService
}
],
exports: [JwtModule, 'JWT_REFRESH']
})
export class JwtRefreshModule {}
# .env DB_TYPE=mysql DB_DATABASE=code-blocks-DB JWT_ACCESS_SECRET=code-blocks-server-access-secret JWT_ACCESS_EXPIRES_IN=30m JWT_REFRESH_SECRET=code-blocks-server-refresh-secret JWT_REFRESH_EXPIRES_IN=7d
1.2 在 auth.module.ts 中注入兩個模塊
// src/module/auth.module.ts
import { Module } from '@nestjs/common'
import { AuthController } from '../controllers/auth.controller'
import { AuthService } from '../services/auth.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../entities/user.entity'
import { JwtAccessModule } from './jwt-access.module'
import { JwtRefreshModule } from './jwt-refresh.module'
@Module({
imports: [
TypeOrmModule.forFeature([User]),
JwtAccessModule,
JwtRefreshModule
],
controllers: [AuthController],
providers: [AuthService]
})
export class AuthModule {}
1.3 創(chuàng)建兩個 jwt 的校驗策略
這一步是結(jié)合后續(xù)創(chuàng)建的管道,來對接口的請求進行校驗,例如:@UseGuards(JwtRefreshGuard)
在 PassportStrategy 第二個參數(shù)傳入 name,為了后續(xù) guard 中進行區(qū)分
jwtFromRequest 來判斷 jwt 從什么地方獲取:
- 從請求頭中
Authorization獲取 :jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - 自定義:從請求頭中獲取自定義的 Refresh-Token 字段
// src/strategy/jwt-access.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { JWT_ACCESS_SECRET } from '../common/constant/env'
import { InjectRepository } from '@nestjs/typeorm'
import { User } from '../entities/user.entity'
import { Repository } from 'typeorm'
@Injectable()
export class JwtAccessStrategy extends PassportStrategy(
Strategy,
'jwt-access'
) {
constructor(
protected configService: ConfigService,
@InjectRepository(User)
private usersRepository: Repository<User>
) {
super({
// 從請求頭中獲取token
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>(JWT_ACCESS_SECRET)
})
}
// 對token進行校驗,會在req.user上添加信息
async validate(payload: { userId: string; phone: string }) {
const user_info = await this.usersRepository.findOne({
where: { id: payload.userId },
select: ['id', 'phone', 'is_status']
})
if (!user_info) return false
if (!user_info.is_status) return false
if (user_info.phone !== payload.phone && user_info.id !== payload.userId)
return false
return { userId: payload.userId, phone: payload.phone }
}
}
// src/strategy/jwt-refresh.strategy.ts
import { Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { JWT_REFRESH_SECRET } from '../common/constant/env'
import { InjectRepository } from '@nestjs/typeorm'
import { User } from '../entities/user.entity'
import { Repository } from 'typeorm'
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
Strategy,
'jwt-refresh'
) {
constructor(
protected configService: ConfigService,
@InjectRepository(User)
private usersRepository: Repository<User>
) {
super({
// 從請求頭中獲取自定義的 Refresh-Token 字段
jwtFromRequest: (req: Request) => {
const refreshToken = req.headers['refresh-token']
if (!refreshToken) {
throw new UnauthorizedException('Refresh token is required')
}
return refreshToken
},
ignoreExpiration: false,
secretOrKey: configService.get<string>(JWT_REFRESH_SECRET)
})
}
// 對token進行校驗,會在req.user上添加信息
async validate(payload: { userId: string; phone: string }) {
const user_info = await this.usersRepository.findOne({
where: { id: payload.userId },
select: ['id', 'phone', 'is_status']
})
if (!user_info) return false
if (!user_info.is_status) return false
if (user_info.id !== payload.userId) return false
return { userId: payload.userId, phone: user_info.phone }
}
}
1.4 全局注冊策略(Strategy)
nest 通過依賴注入的方式來實現(xiàn)模塊之間的使用,也可以局部注冊。只有注冊后,全局的守衛(wèi)才會生效。
// src/app.module.ts
import { Global, Logger, Module } from '@nestjs/common'
// 不同模塊
import { AuthModule } from './modules/auth.module'
import { EditModule } from './modules/edit.module'
import { UserModule } from './modules/user.module'
import { TypeOrmConfigModule } from './config/typeorm.module'
import { LogConfigModule } from './config/log.module'
import { ENV_Config_Module } from './config/config.module'
import { JwtAccessStrategy } from './strategy/jwt-access.strategy'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from './entities/user.entity'
import { JwtRefreshStrategy } from './strategy/jwt-refresh.strategy'
@Global()
@Module({
imports: [
ENV_Config_Module,
TypeOrmConfigModule,
TypeOrmModule.forFeature([User]),
LogConfigModule,
AuthModule,
EditModule,
UserModule
],
controllers: [],
// 全局提供logger,從@nestjs/common進行導入。因為在main.ts中重構(gòu)官方的logger實例
// JwtAccessStrategy、JwtRefreshStrategy 策略使其在全局都能使用
providers: [Logger, JwtAccessStrategy, JwtRefreshStrategy],
exports: [Logger]
})
export class AppModule {}
1.5 創(chuàng)建兩個 jwt 的守衛(wèi)
實現(xiàn)接口注解,@UseGuards(JwtAccessGuard) 和 @UseGuards(JwtRefreshGuard) 來實現(xiàn)統(tǒng)一校驗
例如:
// xxx.controller.ts
// 全局接口
@Controller('edit')
@UseGuards(JwtAccessGuard)
export class EditController {}
// 單獨接口
@Get('refresh-token')
@UseGuards(JwtRefreshGuard)
async refreshToken(@Headers('Refresh-Token') refreshToken: string) {}
實現(xiàn):
// src/guard/jwt-access.guard.ts
import { AuthGuard } from '@nestjs/passport'
/**
* AuthGuard 默認為 jwt,也可以在 strategy/jwt.strategy.ts 中修改為其他策略
* JwtAccessStrategy 繼承的 PassportStrategy,在 PassportStrategy 第二個參數(shù)就是 name 值
* */
export class JwtAccessGuard extends AuthGuard('jwt-access') {
constructor() {
super()
}
}
// src/guard/jwt-refresh.guard.ts
import { AuthGuard } from '@nestjs/passport'
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {
constructor() {
super()
}
}
1.6 實現(xiàn)刷新 token 接口
刷新 token 接口有兩種思路:
- 接口返回兩個 token,這樣后續(xù)就可以保證這個 長 token(refresh token)永遠不會過期。
- 接口只返回短 token,長 token 會過期,例如 7 天后過期用戶也會重新登錄。(這里采用這種方式)
token 存儲方式也有幾種:自行選擇
- cookie(refresh token) + localStorage(access token)
- localStorage(refresh token + access token)
// src/controller/auth.controller.ts
/**
* 刷新token接口
* @headers headers['Refresh-Token'] 刷新token
*/
@Get('refresh-token')
@UseGuards(JwtRefreshGuard)
async refreshToken(@Headers('Refresh-Token') refreshToken: string) {
const data = await this.authService.refreshToken(refreshToken);
return {
code: 200,
message: '刷新token成功',
data,
};
}
// src/service/auth.service.ts
import {
HttpException,
HttpStatus,
Inject,
Injectable,
NotFoundException
} from '@nestjs/common'
import { Repository } from 'typeorm'
import { InjectRepository } from '@nestjs/typeorm'
import { RegisterDto } from '../dto/user/register.dto'
import { JwtService } from '@nestjs/jwt'
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
// 使用 @Inject 手動注入依賴,通過在 1.1 中注入的內(nèi)容實現(xiàn)
@Inject('JWT_ACCESS') private readonly jwtAccess: JwtService,
@Inject('JWT_REFRESH') private readonly jwtRefresh: JwtService
) {}
/**
* 刷新token
*/
async refreshToken(refreshToken: string): Promise<{ accessToken: string }> {
const payload = this.jwtRefresh.decode(refreshToken)
const user = await this.usersRepository.findOne({
where: { id: payload.userId },
select: ['id', 'phone']
})
if (!user) {
throw new NotFoundException('用戶不存在')
}
const accessToken = await this.jwtAccess.signAsync({
userId: user.id,
phone: user.phone
})
return { accessToken }
}
}
2. 前端登錄
2.1 新增刷新 token 接口
export const reqRefreshToken = () =>
request<any, RefreshTokenResponse>({
url: API.refreshToken,
method: 'get',
headers: { 'Refresh-Token': getRefreshToken() }
})
2.2 配置請求響應攔截器
需要注意的點:
- 在攜帶 access token 的接口,返回 401 時,就需要發(fā)送 reqRefreshToken 來刷新 token
- 在頁面多個并發(fā)請求時,需要創(chuàng)建一個請求隊列,當 token 刷新后重新發(fā)送請求
import axios, { type AxiosRequestConfig } from 'axios'
import router from '@/router/index'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import { baseUrl } from '@/common/baseUrl'
import { API as AuthAPI, reqRefreshToken } from './auth'
const user = useUserStore()
const request = axios.create({
baseURL: baseUrl,
timeout: 300000
})
request.interceptors.request.use(
(config) => {
const user = useUserStore()
if (user.token) config.headers['Authorization'] = 'Bearer ' + user.token
return config
},
(error) => {
return Promise.reject(error)
}
)
// 已經(jīng)處理過的錯誤碼
const hasErrorCode = [401, 403]
// 在刷新token時,如果頁面上有多個請求,當token過期后,那這幾個請求都會觸發(fā) reqRefreshToken 來刷新token
/**
* 1. 需要定義一個變量來標記當前是否刷新中,避免重復刷新token
* 2. 創(chuàng)建一個請求隊列,當刷新token成功后,需要把隊列中的請求重新發(fā)送
*/
let isRefreshing = false
interface PendingTask {
config: AxiosRequestConfig
resolve: (value?: any) => void
}
const requestQueue: PendingTask[] = []
request.interceptors.response.use(
async (response) => {
if (
response.data?.code === 401 &&
!response.config.url?.includes(AuthAPI.refreshToken)
) {
if (!isRefreshing) {
// 第一個觸發(fā) 401 的請求,刷新 token 并重新發(fā)送隊列中的請求
isRefreshing = true
const res = await reqRefreshToken()
isRefreshing = false
if (res.code === 200) {
const accessToken = res.data.accessToken
// 更新accessToken
user.refresh(accessToken)
// 重新請求
requestQueue.forEach(({ config, resolve }) => {
config.headers!['Authorization'] = 'Bearer ' + accessToken
resolve(request(config))
})
requestQueue.length = 0
/**
* 如果同時多個請求,在其他幾個請求中,有一個先返回響應,先響應的回執(zhí)行 requestQueue.forEach,
* 此時這個 requestQueue 沒有當前請求,則需要返回當前這個請求,重新去執(zhí)行
* (返回一個Promise會直接去執(zhí)行)
*/
return request(response.config)
} else {
// refreshToken過期
user.clearInfo()
router.replace('/login')
ElMessage.error('登錄已過期,請重新登錄')
}
} else {
// 當前請求不是第一個觸發(fā) 401 的請求,則將當前為401(token過期的請求)添加到請求隊列中
return new Promise((resolve) => {
requestQueue.push({
config: response.config,
resolve
})
})
}
return
}
if (response.data?.code === 403) {
user.clearInfo()
router.replace('/login')
ElMessage.error('無權(quán)限')
return
}
if (response.data?.code === 200) {
response.data['status'] = true
}
if (
!response.data['status'] &&
!hasErrorCode.includes(response.data?.code)
) {
ElMessage.error(response.data.message)
}
return response.data
},
(error) => {
return Promise.reject(error)
}
)
export default request到此這篇關(guān)于vue中使用雙token機制的文章就介紹到這了,更多相關(guān)vue 雙token機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Vue3+TypeScript的全局對象的注入和使用詳解
這篇文章主要介紹了基于Vue3+TypeScript的全局對象的注入和使用,本篇隨筆主要介紹一下基于Vue3+TypeScript的全局對象的注入和使用,需要的朋友可以參考下2022-09-09
el-form-item?prop屬性動態(tài)綁定不生效問題及解決
這篇文章主要介紹了el-form-item?prop屬性動態(tài)綁定不生效問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07
Vue3 響應式數(shù)據(jù) reactive使用方法
這篇文章主要介紹了Vue3 響應式數(shù)據(jù) reactive使用方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-11-11

