什么是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信息。
如果我们将@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的同步。
参考: