这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战
1. 使用JWT
目前后台对于用户权限登录状态都是采用的jwt技术,他的作用我就不说了,基本上都知道他的用处(小白应该不会这么快接触node)
1.1 JWT生成的TOKEN
TOKEN,其实就是三块数据编码拼接而来的(header.payload.signature
),先来一段JWT生成的TOKEN
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QiLCJuaWNrbmFtZSI6IiIsInNhbHQiOiI5aVZUIiwicm9sZSI6MCwiZW1haWwiOiIiLCJwaG9uZSI6IiIsImlhdCI6MTY0MjUwODE5OSwiZXhwIjoxNjQyNTM2OTk5fQ.FhGAIRqqtDL__PBjrnseZ3H8YOgBQmmqZWKxjDATzK8
用.
对上述进行分段,然后对1,2,3段分别进行base64解码
atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')// '{"alg":"HS256","typ":"JWT"}'
// {"username":"root","nickname":"","salt":"9iVT","role":0,"email":"","phone":"","iat":1642508199,"exp":1642536999}
atob('eyJ1c2VybmFtZSI6InJvb3QiLCJuaWNrbmFtZSI6IiIsInNhbHQiOiI5aVZUIiwicm9sZSI6MCwiZW1haWwiOiIiLCJwaG9uZSI6IiIsImlhdCI6MTY0MjUwODE5OSwiZXhwIjoxNjQyNTM2OTk5fQ')
atob('FhGAIRqqtDL__PBjrnseZ3H8YOgBQmmqZWKxjDATzK8') // 报错, The string to be decoded is not correctly encoded
这样一下子就很清楚明了了,header,其实就是声明jwt用的何种哈希算法;payload载荷就是携带一些数据,其实如果用token,前后端可以协商payload携带用户的那些信息,一般来说是携带用户的非敏感信息,因为base64解码太容易,敏感信息放在这里非常的不安全
token 的重点就是signature
,因为base64编码基本等于公开,也很容易被篡改,如何保证可信。那就在签名上面。签名是头部(header)、荷载(payload)与一个密钥产生的哈希值。纵然前面的数据很容易被篡改,可是如果不知道签名的密钥则很难保证签名正确。
1.2 使用token的注意点
- 不要加入敏感信息在荷载数据中,因为base64可以无障碍解码
- 密钥一定保管好,只能存在服务器。因为安全的核心在签名,签名的核心在密钥
- 荷载中要加入过期时间或生成时间,不然无法判断他的有效期
- 若JWT是存Cookie里面的,最好为cookie加HttpOnly属性,防止XSS攻击
2. Nestjs权限实践
其实关于权限,在nestjs的官网也是有提到的,它的概述部分,讲述的守卫,授权守卫就是nestjs权限的实现思路
然后在nestjs官网的安全一节,也有详细的讲述如何做好权限验证,我这边就是综合文档简单的总结Nestjs实现jwt权限token认证的功能
2.1 实现Nestjs权限的几个工具包
@nestjs/jwt
,nestjs中jwt的核心包@nestjs/passport
, 这个包是用于nestjs中使用passport策略实现身份验证passport-jwt
,这个包就是passport验证的jwt实现策略
2.2 构建权限文件,配置相关用户文件
第一个就是建立auth模块的module,controller,service;(建议使用内置命令行创建,不知道的可以运行 nest -help 查看相关命令)
然后创建一个jwt.strategy.ts
文件,处理jwt的策略的逻辑
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { JwtSecret } from './auth.module';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
// 前端请求头header 就要有这个字段Authorization:`Bearer ${token}`
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: JwtSecret,
});
}
async validate(payload) {
// 处理ok返回username
return { username: payload.username };
}
}
- 首先对module进行处理,
auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '@/apis/user/user.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
export const JwtSecret = 'test/nest';
@Module({
imports: [
// 将PassportModule 加载到auth
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: JwtSecret,
signOptions: { expiresIn: '1h' },// 失效时间
}),
// 因为要用到User的一些功能,加载UserModule到auth
UserModule,
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
- 然后处理
auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from '@/apis/user/user.service';
import { encryptPassword } from '@/utils';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService, private usersService: UserService) {}
getToken(payload) {
const { username, nickname, role, email, phone } = payload;
return this.jwtService.sign({
username,
nickname,
role,
email,
phone,
});
}
async validateUser(username: string, psd: string): Promise<any> {
const user = await this.usersService.find({ username });
const pass = encryptPassword(psd, user.salt);
if (user && user.password === pass.password) {
return true;
}
return false;
}
}
- 在
auth.controller.ts
实现登录
import { Controller,Post, Body, UseGuards} from '@nestjs/common';
import { UserLoginDot } from '@/apis/user/user.dot';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from '@/apis/user/user.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService
) {}
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Body() params:UserLoginDot) {
const user = await this.userService.find({username:params.username})
return [
{
token: this.authService.getToken(user),
},
"登录成功"
]
}
}
- 设计
jwt.gurad.ts
,来确保需要的api路由来进行权限校验,因为一些api是不需要权限的,而且如视图路由也是不需要进行权限校验,路由拦截的,但是如果按文档对每个控制器方法使用@UseGuards
来进行路由守卫又显得过于麻烦,所以直接在全局设置守卫,也就是jwt.gurad.ts
,在其中设置路由拦截
import { ExecutionContext, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private readonly options = {
match: /^\/api/,
getMatch: /^\/api(.*)(\/isauth)(\/){0,1}(.*)/,
ignoreApi: [],
},
) {
super();
}
canActivate(context: ExecutionContext) {
const ctx = context.switchToHttp();
const request = ctx.getRequest();
const requestUrl = request.url;
const requestMethod = request.method;
if (this.options.ignoreApi.includes(requestUrl)) {
return true;
}
if (requestMethod === 'GET') {
if (this.options.getMatch.test(requestUrl)) {
return super.canActivate(context);
} else {
return true;
}
}
if (this.options.match.test(requestUrl)) {
return super.canActivate(context);
} else {
return true;
}
}
handleRequest(err, user) {
console.log(err, user, 'addaaaa');
if (err || !user) {
// jwt 只解决登录态问题 409
throw new HttpException('登录态已失效', HttpStatus.CONFLICT);
}
return user;
}
}
这样对只对/api
下的post方法和/api
下get方法路径有/isauth
字段的路由进行登录态权限校验,而且还是可以配置规则的
全局配置在main.ts
下设置app.useGlobalGuards(new JwtAuthGuard());
3. 更进一步,如何设计refresh_token?
背景,为什么需要refresh_token?当然是为了安全
作用:refresh_token
, 它是一个用来获取access_token
的凭证
不过重复刷新token最简单的设计就是,登录的时候,设置用户登录时间,在auth模块保留一个多长时间不在线就失效的时长,在token
失效的时候,请求刷新token
的接口,这时候就后端检查当前时间是否已经失效了,如果没有失效就返回一个新的token,并重置用户登录时间。这是一个比较简单的设计。也避免了长时间使用一个token
的弊端
然后一个设计就是,使用client_id
,client_secret
加密,存储在用户表中或者redis里面,这个refresh_token,验证的是auth/refresh
这个接口,如果这个接口失败就需要重新登录,否则就返回一个新的access_token
总结
- 实现权限,在nest里面的重点就是使用守卫,其实在中间件这部分实现也是可以的(例如,eggjs,koa等都是在中间件部分实现该逻辑的)