NestJs权限处理

924 阅读5分钟

这是我参与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权限的几个工具包

  1. @nestjs/jwt,nestjs中jwt的核心包
  2. @nestjs/passport, 这个包是用于nestjs中使用passport策略实现身份验证
  3. 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 };
  }
}

  1. 首先对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 {}
  1. 然后处理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;
  }
}
  1. 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),
            },
            "登录成功"
        ]
    }
}
  1. 设计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等都是在中间件部分实现该逻辑的)