D9 的任务是实现一种验证用户身份的方法。用户使用客户端请求我们的服务端应用,在服务端这里有时候需要知道请求是谁发出的,这就需要验证请求用户的身份。方法有很多,这次任务会介绍一种基于 JWT 的身份验证方法。
JWT 身份验证的流程
登录
在我们的服务端应用里需要提供一个登录接口,负责处理登录请求,检查用户名与密码以后响应签发的 JWT 或异常。
首先用户要先请求登录,一般就是使用他们在应用里的用户名与密码请求登录接口。这个登录接口会检查用户提供的用户名与密码,比如先看一下用户名在应用里是否存在。如果存在,再检查这个用户提供的密码跟我们存储在应用数据库里的密码是否匹配,如果匹配,登录接口就会给这个用户签发一个 JWT,并且把这个 JWT 响应给用户的客户端。
验证
用户的请求登录成功以后,服务端的登录接口给用户客户端响应一个签发的 JWT,客户端收到这个 JWT 以后要把它存储在某个地方(比如 LocalStorage)。用户使用客户端再次请求服务端应用的其它资源的时候,每一次请求都要在请求里带着这个 JWT,服务端收到请求会检查请求里是否包含了 JWT ,如果包含,服务端会验证 JWT 并解析里面的内容,这样服务端就会知道这个请求到底是由哪个用户发出的。因为服务端签发 JWT 的时候,在它里面会包含用户的 ID 或者用户名之类的信息。
身份验证模块
在应用里我们可以单独创建一个模块去处理用户的身份验证,比如创建一个叫 Auth(authentication) 的模块。在这个模块的控制器里添加一个登录接口,处理用户的登录请求。
接口
src/modules/auth/auth.module.ts
@Controller('auth') export class AuthController { constructor( private readonly authService: AuthService ) { } @Post('login') async login(@Body() data: LoginDto) { return await this.authService.login(data); } }
接口地址是 auth/login,用户请求这个接口地址,接口的处理器会把请求里的数据提取出来交给 authService 上的 login 方法。
签发
src/modules/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { LoginDto } from './auth.dto'; import { JwtModule, JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor( private readonly userService: UserService, private readonly jwtService: JwtService ) { } async login(data: LoginDto) { const { name, password } = data; const entity = await this.userService.findByName(name); if (!entity) { throw new UnauthorizedException('用户名不存在。'); } if (!(await entity.comparePassword(password))) { throw new UnauthorizedException('密码不匹配。'); } const { id } = entity; const payload = { id, name }; const token = this.signToken(payload); return { ...payload, token }; } signToken(data: JwtModule) { return this.jwtService.sign(data); } }
在 AuthService 里面添加一个需要的 login 方法,接收的 data 参数里面应该会包含请求里带的用户名与密码。先把它们解构出来,然后再用 UserService 服务上的 findByName 方法查找一下应用里到底有没有这个特定的用户,要是没有就会响应异常。如果有这个用户,接着会检查密码是否匹配,不匹配也会响应异常。
@nestjs/jwt
如果没什么问题,login 就会准备好签发 JWT,在这个 JWT 里面会包含用户的 id 还有 name 属性的值。签发 JWT 用的方法是 @nestjs/jwt 模块的 JwtService 服务上提供的,所以我们得先去安装一下这个模块。
在 @nestjs/jwt 模块里定义了一个叫 JwtModule 的模块,在我们自己的 AuthModule 里面要导入这个模块,并且要配置一下它。
import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { UserModule } from '../user/user.module'; import { JwtStrategy } from './strategies/jwt.strategy'; @Module({ imports: [ UserModule, JwtModule.register({ secretOrPrivateKey: '1AGy4bCUoECDZ4yI6h8DxHDwgj84EqStMNyab8nPChQ=', signOptions: { expiresIn: '12h' } }), PassportModule.register({ defaultStrategy: 'jwt' }) ], controllers: [AuthController], providers: [AuthService, JwtStrategy] }) export class AuthModule { }
配置 JwtModule 用一下 register 方法,需要设置签发 JWT 的时候用的密码或密钥,还有签发时用的一些选项,比如 JWT 的过期时间。
验证
注意在上面的模块里还导入并配置了 PassportModule,这个模块是用来验证用户身份用的,它来自 @nestjs/passport 。这个模块集成使用了 Passport.js,这个东西提供了很多验证用户身份的方法,其中就包括 JWT 这种方法,在我们的应用里可以使用它提供的方法验证请求用户的身份。
使用 Passport.js 来验证用户身份,我们需要自己去创建对应的验证策略,下面是一个自定义的基于 JWT 的验证方法策略:
import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy, ExtractJwt, VerifiedCallback } from 'passport-jwt'; import { JwtPayload } from '../auth.interface'; import { UserService } from '../../user/user.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly userService: UserService ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: '1AGy4bCUoECDZ4yI6h8DxHDwgj84EqStMNyab8nPChQ=' }); } async validate(payload: JwtPayload, done: VerifiedCallback) { console.log('payload: ', payload); const { name } = payload; const entity = await this.userService.findByName(name); if (!entity) { throw new UnauthorizedException('没找到用户。'); } return entity; } }