NestJS - Session, JWT & Red

什么是JWT?
JWT全称 - JSON WEB TOKEN
Link

什么是Redis?
Redis - 内存缓存服务器
Link
Windows下Redis的安装

NestJS 与 Authentication
注意:Authentication(鉴权) 与 Authorization(授权)的区别

Authentication发展至今,常用的方式有三种:

  • Session
  • JWT
  • oAuth

@nestjs/passport 库支持上述三种认证方式。
以下步骤描述了如何使用@nestjs/passport实现authentication

/* 全局安装nestjs */
npm i -g @nestjs/cli

/* 新建项目 */
nest new nest-auth

/ * 安装passport依赖 * /
npm install --save @nestjs/passport passport passport-local
npm install --save-dev @types/passport-local

/ * 认证模块 * /
nest g module auth
nest g service auth

/ * 用户模块 * /

nest g module users
nest g service users

Guard & Strategy
为 users/user.service.ts 加入模拟数据

import { Injectable } from '@nestjs/common';

// This should be a real class/interface representing a user entity
export type User = any;

@Injectable()
export class UsersService {
  private readonly users = [
    {
      userId: 1,
      username: 'john',
      password: 'changeme',
    },
    {
      userId: 2,
      username: 'maria',
      password: 'guess',
    },
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }
}

users/user.module.ts → 增加exports

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}
在auth/auth.service.ts中增加validateUser方法


import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

AuthModule中引入UsersModule, auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';

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

添加策略的实现, auth/local.strategy.ts

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

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

Passport会调用每个PassportStrategy实现类中的validate方法。
修改auth/auth.module.ts,在imports、providers中加入Passport和LocalStrategy

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [UsersModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

使用NestJS的Guard机制实现鉴权(Authentication)。 如NestJS官网所述,Guard的作用是决定Http请求是否要通过路由被处理。
NestJS/AuthGuard

app.controller.ts

import { Controller, Request, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('/')
export class AppController {
  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
  
  @Get('hello')
  getHello(): string {
    return 'Hello World!';
  }
}

启动项目:npm run start:dev
测试访问hello接口:http://localhost:3000/hello
打开postman,输入以下参数,注意红框的部分。 可以看到返回user信息。
NestJS - Session, JWT & Red

如果我们将@UseGuards(AuthGuard('local'))装饰器放到getHello方法上,再访问此方法时,我们会看到
NestJS - Session, JWT & Red

至此AuthGuard可以work,接下去我们要实现基于Session、JWT的鉴权。

JWT
添加密钥常量, auth/constants.ts

export const jwtConstants = {
  secret: 'secretKey',
};

增加jwt策略 auth/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtConstants } from '../constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

修改auth/auth.module.ts

此处修改的目的是注册Jwt模块,设置密钥与过期时间


import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule, 
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: { expiresIn: '180s' },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

最后运行项目:npm run start:dev
打开postman,访问login接口,获得access_token
NestJS - Session, JWT & Red

使用获得的token作为bearer的值,访问受保护的api
NestJS - Session, JWT & Red

上述示例验证了在NestJS下JWT的工作方式

Session + Redis
NestJS默认使用express,因此为了支持Session需要安装以下依赖(包括Redis)

npm i --save express-session redis connect-redis passport-custom
npm i -D @types/express-session @types/redis @types/connect-redis
在先前示例的基础上使之支持session,并能够保存至redis。打开main.ts,更新如下:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session'; 
import * as connectRedis from 'connect-redis'; 
import * as redis from 'redis'; 
import * as passport from 'passport'; 

const RedisStore = connectRedis(session); 

// 设置redis链接参数,具体参考 https://www.npmjs.com/package/redis 
const redisClient = redis.createClient(6379, '127.0.0.1'); 

// 设置passport序列化和反序列化user的方法,在将用户信息存储到session时使用 
passport.serializeUser(function(user, done) { 
  done(null, user); 
}); 
// 反序列化 
passport.deserializeUser(function(user, done) { 
  done(null, user); 
}); 

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use( 
    session({ 
      secret: 'demo-session-secret', //加密session时所使用的密钥
      resave: false, 
      saveUninitialized: false, 
      // 使用redis存储session 
      store: new RedisStore({ client: redisClient }),  
    }), 
  ); 
  // 设置passport,并启用session 
  app.use(passport.initialize()); 
  app.use(passport.session()); 


  await app.listen(3000);
}
bootstrap();

新建策略:useSession.strategy.ts,该策略用来进行身份验证,并将结果保存至session(由于设过使用redis,所以会将session存储在redis中),sessionid会加入response的cookie中。

import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { promisify } from 'util'; 

@Injectable()
export class UseSessionStrategy extends PassportStrategy(Strategy, 'useSession') {
  constructor(private authService: AuthService) {
    super({ 
        passReqToCallback: true, 
      });
  }

  async validate(@Request() req, username: string, password: string): Promise<any> {
    console.log('from useSession Strategy');

    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    // 用户名密码匹配,设置session 
    // promisify,统一代码风格,将node式callback转化为promise 
    await promisify(req.login.bind(req))(user); 
    return user; 
  }
}

新建策略:applySession.strategy.ts, 该策略比较简单,判断请求的session中是否带有user信息

import { Injectable, Request, UnauthorizedException } from "@nestjs/common"; 
import { PassportStrategy } from "@nestjs/passport"; 
import { Strategy } from "passport-custom"; 

@Injectable() 
export class ApplySessionStrategy extends PassportStrategy(Strategy, 'applySession') { 
  async validate(@Request() req): Promise<any> { 

    // 注意,passport的session数据结构,使用req.session.passport.user来访问 user session
    const { passport: { user } } = req.session; 

    if (!user) { 
      throw new UnauthorizedException(); 
    } 
    // 这里的userId和username是上面local.strategy在调用login()函数的时候,passport添加到session中的。 
    // 数据结构保持一致即可 
    const { userId, username } = user; 
    return { 
      userId, 
      username, 
    }; 
  } 
} 

最后在auth.module.ts中导出这两个策略,并在app.controller中使用

import { Controller, Request, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';

@Controller('/')
export class AppController {
  constructor(private readonly authService: AuthService) {}

  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    console.log(JSON.stringify(req.user));
    return req.user;
  }

  @UseGuards(AuthGuard('useSession'))
  @Post('auth/login2')
  async login2(@Request() req) {
    return true;
  }

  @UseGuards(AuthGuard('applySession'))
  @Get('profile2')
  getProfile2(@Request() req) {
    console.log(JSON.stringify(req.user));
    return req.user;
  }
}

最后,使用postman做测试

发起login请求,会看到response带有名为connect.sid的cookie

使用该cookie访问受保护的接口(postman会自动把cookie带上)

总结
NestJS的AuthGuard帮助我们实现路由请求拦截,Passport及其Strategy帮助我们将不同的鉴权策略(Auth Strategy)应用到不同的路由处理方法上。express-session则是处理session以及与redis的同步。

参考:

NestJS官网

JWT代码示例

Session+Redis代码示例

上一篇:MySQL | MySQL 数据库系统(二)- SQL语句的基本操作


下一篇:Nginx系列(四)-安全认证Basic Auth