一、jwt使用实例
- 安装依赖包
$ yarn add passport @nestjs/jwt passport-jwt @types/passport-jwt @nestjs/passport
2.保持干净地模块化,新建auth模块
$ nest g mo auth
$ nest g service auth
$ nest g co auth
相关文件:
auth.service.ts
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 10:29:28
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 13:09:27
* @Description:
*/
import { Injectable, Logger } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { IResponse } from "src/interfaces/response.interface";
import { User } from "src/interfaces/user.interface";
import { encrypt } from "src/utils/encryption";
import { UserService } from "../user/user.service";
const logger = new Logger("auth.service");
@Injectable()
export class AuthService {
private response: IResponse;
constructor(
private readonly userService: UserService,
private jwtService: JwtService
) {}
/**
* @description: 用户登录
* @param {User} user
* @return {*}
*/
public async login(user: User) {
try {
const username: string = user.username;
const password: string = user.password;
const userInfo = await this.userService.findOneByUsername(username);
if (!userInfo) {
this.response = { code: -1, msg: "用户不存在" };
return;
}
const pass = encrypt(password, userInfo.salt);
if (pass === userInfo.password) {
this.response = {
code: 1,
msg: "登录成功",
data: {
userId: `${userInfo._id}`,
token: await this.createToken(userInfo),
},
};
} else {
this.response = { code: -1, msg: "用户名密码错误" };
}
} catch (error) {
this.response = { code: -1, msg: "登录失败" };
logger.log(error);
} finally {
return this.response;
}
}
/**
* @description:创建token
* @param {User} user
* @return {*}
*/
private async createToken(user: User) {
const payload = { username: user.username, userId: `${user._id}` };
return this.jwtService.sign(payload);
}
}
auth.controller.ts
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 10:29:42
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 12:50:39
* @Description:
*/
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { User } from "src/interfaces/user.interface";
import { AuthService } from "./auth.service";
import { SkipAuth } from "./jwt-auth.guard";
@Controller("auth")
@ApiTags("用户验证")
export class AuthController {
constructor(private authService: AuthService) {}
// 登录
@SkipAuth()
@Post("login")
@ApiOperation({
summary: "用户登录",
})
async loginUser(@Body() userDto: User) {
return await this.authService.login(userDto);
}
}
auth.module.ts
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 10:27:57
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 12:42:52
* @Description:
*/
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { JwtModule } from "@nestjs/jwt";
import { UserService } from "../user/user.service";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
import { JWT_CONSTANT } from "./jwt.constant";
import { JwtStrategy } from "./jwt.strategy";
@Module({
imports: [
JwtModule.register({
secret: JWT_CONSTANT.secret,
}),
],
providers: [
AuthService,
UserService,
JwtStrategy,
{
provide: APP_GUARD,
useClass: JwtAuthGuard, //挂载全局接口
},
],
controllers: [AuthController],
})
export class AuthModule {}
jwt-auth.guard.ts:JWT全局守卫。@SkipAuth 跳过JWT验证
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 11:45:36
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 12:51:33
* @Description:
*/
import {
ExecutionContext,
Injectable,
UnauthorizedException,
SetMetadata,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
// 生成跳过检测装饰器
export const IS_PUBLIC_KEY = "isPublic";
export const SkipAuth = () => SetMetadata(IS_PUBLIC_KEY, true);
jwt.constant.ts : JWT配置文件secret为加密的key //
export const JWT_CONSTANT = {
secret: "NVGJb1$Z^xytmAvf",
};
jwt.strategy.ts
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 11:13:01
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 11:58:32
* @Description:
*/
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { JWT_CONSTANT } from "./jwt.constant";
import { User } from "src/interfaces/user.interface";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: JWT_CONSTANT.secret,
});
}
async validate(payload: User) {
return { userId: `${payload._id}`, username: payload.username };
}
}
==swager修改 ==
// main.ts
new DocumentBuilder()
.addBearerAuth()
.build();
需要验证token的controller中增加装饰器@ApiBearerAuth,增加后swager请求header会携带Authorization参数。
@ApiBearerAuth() // Swagger 的 JWT 验证
效果图:
无token
有token
跳过验证的接口:login
二、使用redis,增加token过期和单点登录
1.安装redis依赖
$ yarn add ioredis
2.增加redis.ts工具类
/*
* @Author: FORMAT-qi
* @Date: 2022-05-02 14:44:41
* @LastEditors: FORMAT-qi
* @LastEditTime: 2022-05-02 15:16:03
* @Description:
*/
import { Logger } from "@nestjs/common";
import Redis from "ioredis";
const logger = new Logger("auth.service");
const redisIndex = []; // 用于记录 redis 实例索引
const redisList = []; // 用于存储 redis 实例
const redisOption = {
host: "xxx.xxx.xx.x",
port: xxxx,
password: "xxxxxxxx",
};
export class RedisInstance {
static async initRedis(method: string, db = 0) {
const isExist = redisIndex.some((x) => x === db);
if (!isExist) {
Logger.debug(
`[Redis ${db}]来自 ${method} 方法调用 `
);
redisList[db] = new Redis({ ...redisOption, db });
redisIndex.push(db);
} else {
Logger.debug(`[Redis ${db}]来自 ${method} 方法调用`);
}
return redisList[db];
}
static async setRedis(
method: string,
db = 0,
key: string,
val: string,
timeout = 60 * 60
) {
if (typeof val == "object") {
val = JSON.stringify(val);
}
const redis = await RedisInstance.initRedis(method, db);
redis.set(`${key}`, val);
redis.expire(`${key}`, timeout);
}
static async getRedis(method: string, db = 0, key: string) {
return new Promise(async (resolve, reject) => {
const redis = await RedisInstance.initRedis(method, db);
redis.get(`${key}`, (err, val) => {
if (err) {
reject(err);
return;
}
resolve(val);
});
});
}
}
auth.service.ts //增加生成token后存入redis,用户ID和当key,保证用户只有一个token
//login方法中 ....
if (pass === userInfo.password) {
const token = await this.createToken(userInfo);
//存储token到redis
const redis = await RedisInstance.initRedis("auth.login", 0);
const key = `${userInfo._id}-${user.username}`;
await RedisInstance.setRedis("auth.login", 0, key, `${token}`);
this.response = {
code: 1,
msg: "登录成功",
data: {
userId: `${userInfo._id}`,
token,
},
};
} else {
this.response = { code: -1, msg: "用户名密码错误" };
}
修改 jwt-auth.guard.ts 中 canActivate方法
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
// token对比
const request = context.switchToHttp().getRequest();
const authorization = request["headers"].authorization || void 0;
let tokenNotTimeOut = true;
if (authorization) {
const token = authorization.split(" ")[1]; // authorization: Bearer xxx
try {
let payload: any = this.jwtService.decode(token);
const key = `${payload.userId}-${payload.username}`;
const redis_token = await RedisInstance.getRedis(
"jwt-auth.guard.canActivate",
0,
key
);
if (!redis_token || redis_token !== token) {
throw new UnauthorizedException("请重新登录");
}
} catch (err) {
tokenNotTimeOut = false;
throw new UnauthorizedException("请重新登录");
}
}
return tokenNotTimeOut && (super.canActivate(context) as boolean);
}
效果登陆后:redis中
修改token后(删除redis中token,修改值的等操作)请求结果
@干饭,明天继续