什么是JWT?
JWT全称 - JSON WEB TOKEN
Link
什么是Redis?
Redis - 内存缓存服务器
Link
Windows下Redis的安装
NestJS 与 Authentication
注意:Authentication(鉴权) 与 Authorization(授权)的区别
Authentication发展至今,常用的方式有三种:
@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信息。
如果我们将@UseGuards(AuthGuard('local'))装饰器放到getHello方法上,再访问此方法时,我们会看到
至此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
使用获得的token作为bearer的值,访问受保护的api
上述示例验证了在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代码示例