Skip to content

Nest 最完美的登录方案 ​

  • 一般使用 JWT 和自定义居多

注意

  1. 有的时候我们登录方式不仅仅一种,而是多种,比如手机号登录,邮箱登录,微信登录,QQ 登录,微博登录等等,这个时候我们就需要使用单独来处理了

  2. 策略就是多种登录模式

  3. 最完美的登录方案 就是 passport,它支持多种登录方式,而且使用起来非常简单,只需要引入对应的策略就可以了

流程 ​

注意

  1. passport 会调用不同的策略来给控制器返回 user(名字别改),这个 user 就是告诉控制器这个人是谁,然后控制器就可以根据这个 user 来进行各种操作了

  2. 创建 auth 模块,名字不能改只能是 auth,在这个模块下面有多种策略

  3. 把一种策略当成全局守卫,这个全局守卫里面 把各个策略放进白名单里面

  4. 当各个策略通过 的时候,就会返回一个 user,控制器就会依据这个 user 来生成 token(JWT)或者其他的(你选择的主策略)

  5. 重点就是两个文件 一个是 xxx.strategy.ts 一个是 xxx.module.ts

结构如下 ​

ts

├── auth
│   ├── wechat.strategy.ts
│   ├── jwt.strategy.ts
│   ├── github.strategy.ts
│   ├── custom.strategy.ts
│   ├── local.strategy.ts
│   ├── auth.module.ts
│   ├── auth.service.ts

我这里就写了 5 个策略 你也可以按照你的需求来写

安装通用的 passport ​

ts
npm install --save @nestjs/passport passport

生成 auth 模块 ​

ts
nest g module auth

第一种策略(用户名和密码) ​

安装 ​

ts
npm install --save passport-local
npm install --save-dev @types/passport-local

local.strategy.ts ​

  • auth/local.strategy.ts
ts
import { Strategy } from "passport-local";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable, UnauthorizedException } from "@nestjs/common";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      usernameField: "username", // 明确字段名
      passwordField: "password",
    });
  }

  // 希望执行什么验证方法
  async validate(username: string, password: string) {
    if (!password.trim()) {
      throw new UnauthorizedException("密码不能为空");
    }
    if (!username.trim()) {
      throw new UnauthorizedException("用户名不能为空");
    }
    const users = [
      {
        userId: 1,
        username: "神说要有光",
        password: "guang",
      },
      {
        userId: 2,
        username: "东东东",
        password: "dong",
      },
    ];
    const user = users.find((user) => {
      if (user.username === username && user.password === password) {
        return user;
      }
    });
    if (!user) {
      throw new UnauthorizedException("用户名或者密码不正确");
    }
    console.log(user);
    return user;
  }
}

挂载 ​

  • auth/auth.module.ts 挂载这个策略
ts
import { Module } from "@nestjs/common";
import { LocalStrategy } from "./local.strategy";
@Module({
  imports: [],
  providers: [LocalStrategy],
})
export class AuthModule {}

使用 ​

  • 控制器 验证通过生成 token 返回
ts
  // 测试用户名和密码登录
  @UseGuards(AuthGuard('local'))
  @Post('testlogin/username')
  async testlogin(@Req() req: any) {
    //获取到user信息知道是谁了
    console.log(req.user);
    // 这里就该是生成jwt策略了,一会介绍
    return {
      token: '123456',
    };
  }

第二种策略(JWT) ​

安装 ​

ts

npm install --save passport-jwt

npm install @nestjs/jwt -S

jwt.strategy.ts ​

  • auth/jwt.strategy.ts
ts
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      // yml 形式
      // secretOrKey: configService.get("jwt").secretKey,
      secretOrKey: configService.getOrThrow<string>("JWT_SECRETKEY"),
    });
  }

  async validate(payload: any) {
    // 这里换成你自己生成jwt那个 或者全局搜索userId
    return { userId: payload.userId, userName: payload.userName };
  }
}

挂载 ​

  • auth/auth.module.ts 挂载这个策略
ts
import { Module } from "@nestjs/common";
import { JwtStrategy } from "./jwt.strategy";
@Module({
  imports: [],
  providers: [JwtStrategy],
})
export class AuthModule {}

使用 ​

  • 生成一个 jwt.service.ts 和 jwt.module.ts
ts
nest g service jwt
nest g module jwt

jwt.module.ts

ts
import { Global, Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import { JwtAllService } from "./jwt.service";

@Global()
@Module({
  imports: [
    JwtModule.registerAsync({
      global: true,
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        // secret: configService.get('jwt').secretKey,
        // signOptions: {
        //   expiresIn: configService.get('jwt').expiresin,
        // },
        secret: configService.get("JWT_SECRETKEY"),
        signOptions: {
          expiresIn: configService.get("JWT_EXPIRESIN"),
        },
      }),
    }),
  ],
  providers: [JwtAllService],
  exports: [JwtModule, JwtAllService],
})
export class JwtAllModule {}

jwt.service.ts

ts
import { Injectable, Inject } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class JwtAllService {
  // 注入配置文件
  @Inject()
  private readonly configService: ConfigService;
  constructor(private readonly jwtService: JwtService) {}

  async setToken(data: any) {
    // 这里不能写用户密码,怕破译
    const payload = {
      userId: data.userId, // userId 就是用户ID
      userName: data.userName, // userNamee 就是用户姓名
    };
    const result = await this.jwtService.sign(payload);
    console.log(result);
    // 生成token
    return result;
  }

  verifyToken(token: string) {
    return this.jwtService.verify(token, {
      // yml 格式
      // secret: this.configService.get('jwt').secretKey,
      secret: this.configService.get("JWT_SECRETKEY"),
    });
  }

  // refreshToken
  async setRefreshToken(data: any) {
    const payload = {
      userId: data.userId, // userId 就是用户ID
      userName: data.userName, // userNamee 就是用户姓名
    };
    const result = await this.jwtService.sign(payload, {
      // yml 格式
      // expiresIn: this.configService.get('jwt').refreshExpiresIn, // 设置长时间
      expiresIn: this.configService.get("JWT_REFRESH_EXPIRESIN"), // 设置长时间
    });
    return result;
  }
}
  • 在 app.module.ts 中挂载守卫和 jwt 模块
ts
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE, APP_GUARD } from "@nestjs/core";
import { GlobalExceptionsFilter } from "./common/global.filter";
import { GlobalExceptionsCheckFilter } from "./common/global_check.filter";
import { GlobalGuardCheckFilter } from "./common/global_guard_check.filter";
import { UnauthorizedExceptionFilter } from "./common/global_unauth.filter";
import { GlobalInterceptor } from "./common/global.interceptor";
import { GlobalPipe } from "./common/global.pipe";
import { GlobalGuard } from "./common/global.guard";
import { Module } from "@nestjs/common";
import configuration from "./config/index";
import { ConfigModule } from "@nestjs/config";
import { UsersModule } from "./modules/users/users.module";
import { AuthModule } from "./auth/auth.module";
import { JwtAllModule } from "./modules/jwt/jwt.module";
@Module({
  imports: [
    // 配置模块
    ConfigModule.forRoot({
      cache: true,
      load: [configuration],
      isGlobal: true,
    }),
    UsersModule,
    AuthModule,
    JwtAllModule,
  ],
  controllers: [],
  providers: [
    // 全局异常过滤器(他负责兜底 处理其他异常)
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsFilter,
    },
    // 检查管道过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionsCheckFilter,
    },
    // 守卫过滤器
    {
      provide: APP_FILTER,
      useClass: GlobalGuardCheckFilter,
    },
    // 策略过滤器
    {
      provide: APP_FILTER,
      useClass: UnauthorizedExceptionFilter,
    },
    // 全局统一格式拦截器
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalInterceptor,
    },
    // 全局管道
    {
      provide: APP_PIPE,
      useClass: GlobalPipe,
    },
    // 全局守卫
    {
      provide: APP_GUARD,
      useClass: GlobalGuard,
    },
  ],
})
export class AppModule {}
  • 控制器 使用
ts

  // 测试用户名和密码登录策略
  @UseGuards(AuthGuard('local'))
  @Post('testlogin/username')
  async testlogin(@Req() req: any) {
    //获取到user信息知道是谁了
    console.log(req.user);
    // 这里就该是生成jwt策略了,一会介绍
    return {
      token: '123456',
    };
  }

  // 注册用户生成jwt
  @Post('testregister/jwt')
  async testregister(@Body() body: any) {
    const result = await this.jwtAllService.setToken({
      userId: body.userId,
      userName: body.userName,
    });
    return {
      token: result,
    };
  }

  // 用户jwt验证策略
  @UseGuards(AuthGuard('jwt'))
  @Post('testlogin/jwt')
  async loginJwt(@Req() req: any) {
    console.log('通过jwt确实进来了');
    console.log(req.user);
    return {
      data: '通过jwt确实进来了',
    };
  }

  // 测试用户自定义登录策略
  // 我自己写的验证方法
  @UseGuards(AuthGuard('custom'))
  @Post('testlogin/custom')
  getCustom(@Req() req: any) {
    console.log('确实进来了');
    console.log(req.user);
    return ['haha', 'hehe'];
  }
  • 请求的时候类似头部参数 Bearer 后面换成你自己的
ts
POST http://localhost:5000/user/testtoken

Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInVzZXJuYW1lIjoi5byg5LiJIiwiaWF0IjoxNzYwMDk1NDQwLCJleHAiOjE3NjAxMzE0NDB9.1OQvsKMjmcc3ml_AeyiFr2uRF6pipBMZgaw-ePdCMQE

第三种策略(Github) ​

安装 ​

ts

npm install --save passport-github2
npm install --save-dev @types/passport-github2

github.strategy.ts ​

  • auth/github.strategy.ts
ts
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Profile, Strategy } from "passport-github2";

@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, "github") {
  constructor() {
    super({
      clientID: "你自己要登录的github账号的clientID",
      clientSecret: "你自己要登陆的github账号的clientSecret",
      callbackURL: "你自己要登陆的github账号的回调地址",
      scope: ["public_profile"],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: Profile) {
    return profile;
  }
}

挂载 ​

ts
import { Module } from "@nestjs/common";

import { GithubStrategy } from "./github.strategy";

@Module({
  imports: [],
  providers: [GithubStrategy],
})
export class AuthModule {}

使用 ​

控制器里面 出两个接口

  • 登录
ts
  // github登录
  @UseGuards(AuthGuard('github'))
  @Get('github/login')
  async githubLogin(@Req() req: any) {
    console.log('github登录成功');
  }
  • 回调
ts
  // github登录成功回调
  @UseGuards(AuthGuard('github'))
  @Get('githubcallback')
  async githubCallback(@Req() req: any) {
    console.log('github登录成功回调');
    console.log(req.user);
    // 下面就是存入数据库然后 返回token
    /*
    {
  id: '19398663',
  nodeId: 'MDQ6VXNlcjE5Mzk4NjYz',
  displayName: 'YoungJudge',
  username: 'YoungJudge',
  profileUrl: 'https://github.com/YoungJudge',
  emails: [ { value: 'yinjiephp@Gmail.com' } ],
  photos: [ { value: 'https://avatars.githubusercontent.com/u/19398663?v=4' } ],
  provider: 'github'
  */
    return req.user;
  }

第四种策略(自定义) ​

安装 ​

ts
npm install --save passport-custom

custom.strategy.ts ​

  • auth/custom.strategy.ts
ts
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-custom";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";

@Injectable()
export class CustomStrategy extends PassportStrategy(Strategy, "custom") {
  constructor(private authService: AuthService) {
    super();
  }

  // 希望执行什么验证方法
  async validate(req: any) {
    console.log(req.body); // 这里就会获取到请求所有的数据 你自己定义即可
    const user = req.body.id;
    // 挂载只能是user
    return user;
  }
}

挂载 ​

ts
import { Module } from "@nestjs/common";

import { CustomStrategy } from "./custom.strategy";

@Module({
  imports: [],
  providers: [CustomStrategy],
})
export class AuthModule {}

使用 ​

ts
  // 我自己写的验证方法
  @UseGuards(AuthGuard('custom'))
  @Get('profile2')
  getProfile2(@Req() req: any) {
    console.log('确实进来了');
    console.log(req.user);
    return ['haha', 'hehe'];
  }

第五种策略(微信) ​

安装 ​

同 custom

wechat.strategy.ts ​

ts
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-custom";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";

@Injectable()
export class WeChatStrategy extends PassportStrategy(Strategy, "wechat") {
  constructor(private authService: AuthService) {
    super();
  }

  // 希望执行什么验证方法
  async validate(req: any) {
    console.log(req.query.code);
    const user = req.query.code;
    return user;
  }
}

挂载 ​

ts
import { Module } from "@nestjs/common";
import { WeChatStrategy } from "./wechat.strategy";
@Module({
  imports: [],
  providers: [WeChatStrategy],
})
export class AuthModule {}

使用 ​

ts
  // 我自己写的验证方法wechat
  @UseGuards(AuthGuard('wechat'))
  @Get('profile3')
  getProfile3(@Req() req: any) {
    console.log('微信');
    console.log(req.user);
    return ['微信', 'hehe'];
  }

捕获 ​

  • 使用 passport 的时候,验证失败会自动抛出一个 UnauthorizedException 异常

  • 我们可以捕获这个异常,然后返回我们自定义的响应

Unauth.filter.ts ​

ts
import { ExceptionFilter, Catch, ArgumentsHost, Inject } from "@nestjs/common";

import { Injectable, UnauthorizedException } from "@nestjs/common";

// 增加日志模块
import { Logger } from "winston";
import { getReqMainInfo } from "../commonModules/utils/tools";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";

@Catch(UnauthorizedException)
export class UnauthorizedExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
  ) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = 200;

    // 依据你自己的路由 来写错误信息
    let message =
      exception instanceof UnauthorizedException
        ? exception.message
        : exception.stack;
    if (message == "Unauthorized") {
      message = "用户名或者密码不能为空";
    }
    // 记录日志(错误消息,错误码,请求信息等)
    this.logger.error(message, {
      status: 403,
      req: getReqMainInfo(request),
      // stack: exception.stack,
    });
    response.status(status).json({
      code: 403,
      data: message,
      message: "操作失败",
    });
  }
}

最后汇总 ​

  • 控制器
ts
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  Req,
  UseGuards,
} from "@nestjs/common";
import { UsersService } from "./users.service";
import { AuthGuard } from "@nestjs/passport";
import { JwtServiceAll } from "../jwt/jwt.service";
@Controller("users")
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
    private readonly JwtServiceAll: JwtServiceAll,
  ) {}

  @Get("config")
  async getconfig() {
    return this.usersService.getConfig();
  }

  //  用户jwt注册
  @Post("register/jwt")
  async createJwt(@Body() body: any) {
    const token = await this.JwtServiceAll.setToken({
      userId: body.userId,
      userName: body.userName,
    });
    return {
      data: token,
    };
  }

  // 用户jwt验证
  @UseGuards(AuthGuard("jwt"))
  @Post("login/jwt")
  async loginJwt(@Req() req: any) {
    console.log("通过jwt确实进来了");
    console.log(req.user);
    return {
      data: "通过jwt确实进来了",
    };
  }

  // 测试全局传递过来没有userId
  @Post("check/jwt")
  async checkjwt(@Req() req: any) {
    console.log("通过全局验证确实进来了");
    console.log(req.userId);
    return {
      data: "通过全局验证确实进来了",
    };
  }

  // 用户自定义登录
  @UseGuards(AuthGuard("custom"))
  @Post("login/custom")
  async loginCustom(@Req() req: any) {
    console.log("通过自定义确实进来了");
    console.log(req.user);
    return {
      data: "通过自定义确实进来了",
    };
  }

  @UseGuards(AuthGuard("local"))
  @Post("login/basic")
  async loginbasic(@Req() req: any) {
    console.log("通过用户名和密码进来了");
    console.log(req.user);
    return {
      data: "用过用户名和密码进来了",
    };
  }
}
  • 调用接口
bash
### 测试环境
GET http://localhost:5000/users/config

### JWT 注册 不做验证
POST http://localhost:5000/users/register/jwt
Content-Type: application/json

{
    "userId": "100",
    "userName": "test"
}


### JWT 登录
POST http://localhost:5000/users/login/jwt
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMDAiLCJ1c2VyTmFtZSI6InRlc3QiLCJpYXQiOjE3NjI5NTU5MjEsImV4cCI6MTc2Mjk1OTUyMX0.Evtwijj9-pdmRMWN3DBEjuNLRt8QhcdWF10x8lNnc7M


### JWT 测试token登录 绑定userId
POST http://localhost:5000/users/check/jwt
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMDAiLCJ1c2VyTmFtZSI6InRlc3QiLCJpYXQiOjE3NjI5NTU5MjEsImV4cCI6MTc2Mjk1OTUyMX0.Evtwijj9-pdmRMWN3DBEjuNLRt8QhcdWF10x8lNnc7M



### 自定义登录
POST http://localhost:5000/users/login/custom
Content-Type: application/json

{
    "id": "100"
}


### 用户名和密码登录
POST http://localhost:5000/users/login/basic
Content-Type: application/json

{
    "username" : "神说要有光",
    "password": "guang"
}