上一篇已经处理好注册与登入的部分,但一个完整的帐户机制还需要包含 登入後 的身份识别,为什麽登入後还要做身份识别呢?试想今天如果只有注册与登入功能的话,当使用者登入後要在系统上使用某个会员功能时,该如何辨识的这个使用者是谁呢?要实作这样的识别功能有很多种做法,Token 正是其中一个被广泛运用的方案。
Token 就是一个用来表示身份的媒介,当使用者成功登入时,系统会产生出一个独一无二的 Token,并将该 Token 返回给使用者,只要在 Token 有效的期间内,该使用者在请求中带上该 Token,系统便会识别出此操作的使用者是谁。
在近几年有一项 Token 技术非常热门,其名为 Json Web Token (简称:JWT),本篇的身份识别就会用 JWT 来实作!
JWT 是一种较新的 Token 设计方法,它最大的特点是可以在 Token 中含有使用者资讯,不过仅限於较不敏感的内容,比如:使用者名称、性别等,原因是 JWT 是用 Base64 进行编码,使用者资讯可以透过 Base64 进行 还原,使用上需要特别留意!
一个 JWT 的格式如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhBTyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjJ9.d704zBOIq6KNcexbkfBTS5snNa9tXz-RXo7Wi4Xf6RA
会发现整个字串被两个「.」切割成三段,这三段可以透过 Base64 进行解码,它们各自有不同的内容:
标头为 JWT 第一段的部分,其内容包含「加密演算法」与「Token 类型」。上方 JWT 的标头进行解码可以得出下方资讯:
{
"alg": "HS256",
"typ": "JWT"
}
内容为 JWT 第二段的部分,这里通常会放一些简单的使用者资讯。上方 JWT 的内容进行解码可以得出下方资讯:
{
"sub": "1234567890",
"name": "HAO",
"admin": true,
"iat": 1516239022
}
签章为 JWT 第三段的部分,用来防止被窜改,在後端需要维护一组密钥来替 JWT 进行签章,密钥需要妥善保存避免被有心人士获取!
在开始实作之前,先透过 npm
安装 JWT 所需的套件,主要有 Nest 包装的模组、passport-jwt 以及其型别定义档:
$ npm install @nestjs/jwt passport-jwt
$ npm install @types/passport-jwt -D
首先,我们要先定义一组密钥来进行 JWT 的签章,并将该密钥放至 .env
中:
JWT_SECRET=YOUR_SECRET
接着,在 src/config
资料夹下新增 secret.config.ts
,将密钥类型的环境变数整合至 secrets
底下:
import { registerAs } from '@nestjs/config';
export default registerAs('secrets', () => {
const jwt = process.env.JWT_SECRET;
return { jwt };
});
在 app.module.ts
中进行套用:
import { APP_PIPE } from '@nestjs/core';
import { Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './features/user/user.module';
import { AuthModule } from './features/auth/auth.module';
import MongoConfigFactory from './config/mongo.config';
import SecretConfigFactory from './config/secret.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [MongoConfigFactory, SecretConfigFactory], // 套用至 ConfigModule
isGlobal: true
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.get<string>('mongo.uri'),
}),
}),
UserModule,
AuthModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
完成密钥的配置後,就来配置 JWT 吧!我们在处理验证的 AuthModule
中汇入 JwtModule
,并使用 registerAsync
方法来配置 JWT 的设定,最重要的就是将密钥带入:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
PassportModule,
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const secret = config.get('secrets.jwt');
return {
secret,
signOptions: {
expiresIn: '60s'
}
};
},
})
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy],
})
export class AuthModule {}
注意:本篇主要是实作一个简单的身分识别功能,所以详细的
JwtModule
配置项请参考 官方文件 以及 node-jsonwebtoken。
上一篇我们是让使用者登入後获得使用者资料,这篇我们将会把这个机制更换成回传 JWT,让使用者可以顺利拿到它来使用会员功能,所以我们要在 AuthService
设计一个 generateJwt
方法来调用 JwtService
的 sign
方法产生 JWT,该方法需要带入要放在「内容」区块的资料,这里我们就放入使用者的 id
与 username
:
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CommonUtility } from '../../core/utils/common.utility';
import { UserDocument } from '../../common/models/user.model';
import { UserService } from '../user';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
async validateUser(username: string, password: string) {
const user = await this.userService.findUser({ username });
const { hash } = CommonUtility.encryptBySalt(
password,
user?.password?.salt,
);
if (!user || hash !== user?.password?.hash) {
return null;
}
return user;
}
generateJwt(user: UserDocument) {
const { _id: id, username } = user;
const payload = { id, username };
return {
access_token: this.jwtService.sign(payload),
};
}
}
我们上一篇在 LocalStrategy
设定回传值只有 username
与 email
,这不符合我们产生 JWT 所需的资料,所以改成直接回传整个使用者资料:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string) {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
注意:
validate
最好是只回传重点资料。
最後就是在 AuthController
的 signin
方法回传 generateJwt
的结果:
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
import { UserDocument } from '../../common/models/user.model';
import { CreateUserDto, UserService } from '../user';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}
@Post('/signup')
signup(@Body() user: CreateUserDto) {
return this.userService.createUser(user);
}
@UseGuards(AuthGuard('local'))
@Post('/signin')
signin(@Req() request: Request) {
return this.authService.generateJwt(request.user as UserDocument);
}
}
透过 Postman 进行登入测试,成功的话会获得 access_token
:
接下来我们要制作 JwtStrategy
与 passport
进行串接,跟 LocalStrategy
的实作方式大同小异,必须继承 passport-jwt
的 strategy
,比较不同的地方在於 super
带入的参数。我们先在 src/features/auth/strategies
资料夹下新增 jwt.strategy.ts
:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('secrets.jwt'),
});
}
validate(payload: any) {
const { id, username } = payload;
return { id, username };
}
}
可以看到 super
带入了三个参数:
jwtFromRequest
:指定从请求中的哪里提取 JWT,这里可以使用 ExtractJwt
来辅助配置。ignoreExpiration
:是否忽略过期的 JWT,预设是 false
。secretOrKey
:放入 JWT 签章用的密钥。注意:更多的参数内容请参考 官方文件。
可以注意一下 validate
这个方法,基本上 JWT 在流程上就已经验证了其合法性与是否过期,故这里 可以不用 进行额外的检查,但如果要在这里向资料库提取更多的使用者资讯也是可以的。
完成 JwtStrategy
後记得要在 AuthModule
的 providers
里面添加它:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { UserModule } from '../user';
import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule,
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const secret = config.get('secrets.jwt');
return {
secret,
signOptions: {
expiresIn: '60s'
}
};
},
})
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
})
export class AuthModule {}
最後,我们设计一个取得使用者资料的 API 来套用 JWT 验证,透过 CLI 产生 UserController
:
$ nest generate controller features/user
然後修改一下 user.controller.ts
的内容:
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(AuthGuard('jwt'))
@Get(':id')
async getUser(@Param('id') id: string) {
const user = await this.userService.findUser({ _id: id });
const { password, ...others } = user.toJSON();
return others;
}
}
在 getUser
方法套用 AuthGuard
并指定使用 jwt
策略,将传入的 id
向资料库进行查询,取得 UserDocument
後,先把它转换成 JSON 格式,再透过解构的方式将 password
以外的属性回传到客户端。
先透过 Postman 进行登入取得 access_token
,并将其带入 Bearer token
中来测试取得使用者资料的 API:
如果带入过期或是错误的 JWT 则会收到下方错误讯息:
这两天的内容实现了一套简单的本地身份验证机制,相信大家已经了解 passport
的概念与使用方式了,有兴趣的读者可以尝试串接 Facebook 或 Google 的验证机制。这里附上今天的懒人包:
JwtModule
配置密钥、期限等参数,主要是用来建立 JWT。JwtStrategy
的 super
需要指定一些参数,主要是用来验证 JWT 的。
>>: 【Day24】Git 版本控制 - 修改 commit 纪录:amend
前言 Google 翻译团队在2016年发表了重要文章《Google’s Neural Machin...
我们来接续昨日Azure Site Recovery(ASR)的进度之前, 我想补充一下地端及云端容...
前言 因为公司前端资料已经处理成单层结构,所以都没注意到浅拷贝、深拷贝的实际差别。 在读完高手文章後...
今天来尝试部署 Cloudflare Workers 并写写看简单的信息收发。 response 如...
鬼故事 - 糟了,是世界奇观 Credit: Unkonwn (Skritch, Skritch) ...