上节我们基于双 token 实现了登录状态的无感刷新。
这当然是能实现功能的,很多公司也这样用。
但是双 token 实现起来还是挺麻烦的。
所以实际上单 token 自动续期的方式用的也非常多。
单 token 的原理也很简单,就是登录后返回 jwt,每次请求接口带上这个 jwt,然后每次访问接口返回新的 jwt,然后前端更新下本地的 jwt token。
比如这个 token 是 7 天过期,那只要 7 天内访问一次系统,就会刷新 token。
7 天内不访问系统,token 过期,就需要重新登录了。
这种方案也能实现无感刷新,而且代码简单的多。
我们来写一下:
nest new single-token-refresh
进入项目,添加一个 user 模块:
nest g resource user --no-spec
加一个 login 的路由:
import { Body, Controller, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { LoginUserDto } from './dto/login-user.dto';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
async login(@Body() loginDto: LoginUserDto) {
console.log(loginDto)
}
}对应的 user/dto/login-user.dto.ts
export class LoginUserDto {
username: string;
password: string;
}把服务跑起来:
npm run start:dev
postman 访问下:


登录成功返回 jwt,安装下用到的包:
npm install --save @nestjs/jwt在 AppModule 引入:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [UserModule,
JwtModule.register({
global: true,
secret: 'guang'
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}然后 login 的时候返回 jwt
import { BadRequestException, Body, Controller, Inject, Post } from '@nestjs/common';
import { UserService } from './user.service';
import { LoginUserDto } from './dto/login-user.dto';
import { JwtService } from '@nestjs/jwt';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Inject(JwtService)
jwtService: JwtService;
@Post('login')
async login(@Body() loginDto: LoginUserDto) {
if(loginDto.username !== 'guang' || loginDto.password !== '123456') {
throw new BadRequestException('用户名或密码错误');
}
const jwt = this.jwtService.sign({
username: loginDto.username
}, {
secret: 'guang',
expiresIn: '7d'
});
return jwt;
}
}登录后返回 jwt,过期时间是 7 天。
访问下:


可以看到,登录后返回了 jwt。
然后加一个 Guard 来解析 jwt:
nest g guard login --flat --no-spec
登录鉴权逻辑和之前一样:
import { JwtService } from '@nestjs/jwt';
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const authorization = request.headers.authorization;
if(!authorization) {
throw new UnauthorizedException('用户未登录');
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
return true;
} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}
}取出 authorization header 中的 jwt token
jwt 有效就可以继续访问,否则返回 token 失效,请重新登录。
然后在 AppController 添加个接口加上登录鉴权:
@Get('aaa')
aaa() {
return 'aaa';
}
@Get('bbb')
@UseGuards(LoginGuard)
bbb() {
return 'bbb';
}aaa 接口可以直接访问,bbb 接口需要登录后才能访问。
访问 aaa
访问 bbb

登录拿到 token:

带上 token 访问 bbb:

带上 token 就可以访问需要登录的接口了。
这样就完成了登录和鉴权。
但这个 token 是有过期时间的,过期了就要重新登录了,所以要刷新 token。
上节实现了双 token 的无感刷新,今天实现单 token 刷新。
方式很简单,就是访问接口后返回新 token:

import { JwtService } from '@nestjs/jwt';
import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class LoginGuard implements CanActivate {
@Inject(JwtService)
private jwtService: JwtService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
const response: Response = context.switchToHttp().getResponse();
const authorization = request.headers.authorization;
if(!authorization) {
throw new UnauthorizedException('用户未登录');
}
try{
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
response.setHeader('token', this.jwtService.sign({
username: data.username
}, {
expiresIn: '7d'
}));
return true;
} catch(e) {
throw new UnauthorizedException('token 失效,请重新登录');
}
}
}试一下:

这样每次返回新 token,不就永不过期了?
而且实现还特别简单。
所以单 token 自动续期的方案用的挺多的。
我们写下前端代码:
npx create-vite single-token-refresh-frontend
进入项目,把服务跑起来:
npm install
npm run dev
浏览器访问下:

项目跑起来后,我们调用下后端接口。
首先后端要开启跨域:

然后前端页面调用下:
改下 App.tsx
import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';
function App() {
const [content, setContent] = useState('')
async function query() {
try {
const res = await axios.get('http://localhost:3000/bbb');
setContent(res.data);
} catch(e: any) {
console.log(e.response.data.message);
}
}
useEffect(() => {
query();
}, []);
return (
<div style={{fontSize: '100px'}}>{content}</div>
)
}
export default App在页面调用 bbb 接口,把结果显示到页面。
安装 axios:
npm install --save axios
提示未登录(打印两次是 main.tsx 里的 StrictMode 导致的,去掉就好了)
我们登录下:

import { useEffect, useState } from 'react'
import './App.css'
import axios from 'axios';
function App() {
const [content, setContent] = useState('')
async function query() {
try {
const res = await axios.post('http://localhost:3000/user/login', {
username: 'guang',
password: '123456'
});
console.log(res.data);
const res2 = await axios.get('http://localhost:3000/bbb', {
headers: {
Authorization: `Bearer ${res.data}`
}
});
setContent(res2.data);
} catch(e: any) {
console.log(e.response.data.message);
}
}
useEffect(() => {
query();
}, []);
return (
<div style={{fontSize: '100px'}}>{content}</div>
)
}
export default App现在接口就请求成功了:

这个 token 我们一般都放到 localstorage 里,每次请求都带上。
这段逻辑我们上节写过:
axios.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');
if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})那单 token 如何刷新呢?
很简单,拦截器里把 header 里的新 token 更新到 localStorage 就好了:
axios.interceptors.response.use(
(response) => {
const newToken = response.headers['token'];
if(newToken) {
localStorage.setItem('token ', newToken);
}
return response;
}
)但这样有个问题:
打印下 header

没有 token
但我们明明返回了啊:

这也是跨域的问题,默认你能访问的 header 是有限的。
如果想在代码访问别的 header,需要在后端支持下,在 Access-Controll-Expose-Headers 里加上这个 header

现在就可以访问这个 header 了:

这样更新完 localStorage 里的 token,不就实现无感刷新了么?
案例代码在小册仓库:
总结
这节我们实现了单 token 的无感刷新,它也是在公司里用的非常多的一种方案。
好处就是简单,只要每次请求接口的时候返回新的 token,然后刷新下本地 token 就可以了。
我们在 axios 的 response 拦截器里可以轻松做到这个,比双 token 的无感刷新可简单太多了。
要注意的是在代码里访问其他 header,需要后端配置下 expose headers 才可以。
你们公司里是用双 token 还是单 token 实现登录状态无感刷新呢?
(这节写的有点问题,单 token 应该在快过期的时候返回新 token,后面优化下)