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();
  }

  // 希望执行什么验证方法
  async validate(username: string, password: string) {
    console.log("validate");
    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.get("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 } from "@nestjs/common";

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

@Catch(UnauthorizedException)
export class UnauthorizedExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = 200;

    // 错误内容
    const message =
      exception instanceof UnauthorizedException
        ? exception.message
        : exception.stack;

    response.status(status).json({
      code: 401,
      data: "token无效",
      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"
}