token版本管理实现多端登陆踢人下线及无限延期
实现单token无限刷新延期,二端登陆踢人方案
Token 无限刷新 • 用户登录 时,服务器返回一个 JWT Token,并在 payload 中存入 token_version(默认为 0)。 • 每次请求,后端生成 新 Token 并返回给前端,前端替换旧 Token 继续使用,实现无限刷新。
多端登录管理(踢人下线) • 用户登录时,token_version 在数据库存储,每次新登录时,token_version +1,使旧 Token 失效。 • 请求接口时,验证 Token 内的 token_version 是否与数据库一致: • 一致:正常返回数据。 • 不一致:说明用户在其他设备上重新登录,强制当前设备登出。
实现
auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { UserService } from 'src/user/user.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const response: Response = context.switchToHttp().getResponse();
const token = request.headers.authorization;
if (!token) {
throw new UnauthorizedException();
}
try {
const { userName, token_version } = await this.authService.verify(token);
const repeatLoginVersion =
await this.userService.checkRepeatLogin(userName);
if (token_version !== repeatLoginVersion) {
throw new Error('你的账号已在其他地方登录');
}
const newToken = await this.authService.signIn(userName, token_version);
response.setHeader('Authorization', newToken.token);
} catch (error) {
throw new UnauthorizedException(error.message);
}
return true;
}
}
auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async signIn(userName: string, token_version: number) {
const payload = { userName, token_version };
return {
token: await this.jwtService.signAsync(payload),
};
}
async verify(token: string) {
return this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
}
}
user.controller.ts
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from '../auth/auth.guard';
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('login')
async login(@Body() body: Record<string, any>) {
return await this.userService.authUser(body.username, body.password);
}
@UseGuards(AuthGuard)
@Get('profile')
async getProfile() {
return {
data: '用户信息你获取到了',
};
}
}
user.service.ts
import {
Injectable,
UnauthorizedException,
NotFoundException,
} from '@nestjs/common';
import { AuthService } from '../auth/auth.service';
@Injectable()
export class UserService {
constructor(private readonly authService: AuthService) {}
private readonly users = [
{
userId: 1,
username: 'john',
password: 'changeme',
token_version: 0,
},
{
userId: 2,
username: '1',
password: '1',
token_version: 0,
},
];
async authUser(userName: string, password: string) {
const user = await this.users.find((user) => user.username === userName);
if (!user) {
throw new NotFoundException('User not found');
}
if (user.password !== password) {
throw new UnauthorizedException('Incorrect password');
}
user.token_version += 1;
return await this.authService.signIn(user.username, user.token_version);
}
async checkRepeatLogin(username: string) {
const { token_version } = this.users.find(
(user) => user.username === username,
);
return token_version;
}
}