JWT原理与Nestjs的JWT方案

JWT 认证

JWT 是什么?

JWT 是 JSON Web Token 的简称,根据官网述说,它是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。

应用场景?

  • 授权:可以使用 JWT 作为登录后的请求认证,它的请求开销小且可以实现分布请求。
  • 信息交换:使用 JWT 可以在各方之间安全地传输信息。可以对 JWT 进行签名,验证信息是否被修改。

JWT 特点

  • 体积小
  • 请求方式多样(GET/POST/HTTP-Header)
  • 结构化,可携带信息
  • 实现单点登录与跨域验证

JWT 原理

JWT 由以下结构组成:

header.payload.secret

  1. 标头(header【base64 后】)
  2. 有效荷载(payload【base64 后】)
  3. 签名(secret)

包含加密算法与类别:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

然后将头部进行 base64 加密,构成了第一部分:

ewogICAgJ2FsZyc6ICdIUzI1NicsCiAgICAndHlwJzogJ0pXVCcKfQ==

payload

payload 可以看做有效数据的存储区域,这些信息包好三个部分:

  • 标准中的注册声明
  • 公共的声明
  • 私有的声明
标准中的注册声明

声明名称仅是三个字符,因为 JWT 是紧凑的,他们都是可选的:

  • iss:该 JWT 的签发者
  • iat(issued at):在什么时候签发的,UNIX 时间戳
  • exp(expires): 什么时候过期,这里是一个 UNIX 时间戳
  • aud:接收该 JWT 的一方
  • sub:该 JWT 所面向的用户(userid)
  • nbf (Not Before):如果当前时间在 nbf 里的时间之前,则 Token 不被接受
  • jti:jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
公共的声明

公共的声明可以添加任何的非敏感信息(base64 可逆),可以用来携带业务信息或用户信息。

私有的声明

私有声明是提供者和消费者所共同定义的声明。

举个例子:
1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"scopes": ["admin", "user"]
}

做一下 base64 加密:ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJzY29wZXMiOiBbICJhZG1pbiIsICJ1c2VyIiBdIAp9

secret

要创建签名部分,您必须获取编码的标头,编码的有效载荷,标头中指定的算法,并对其进行签名。

1
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
1
2
const encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
const signature = HMACSHA256(encodedString, 'secret');

组合

输出是三个由点分隔的 Base64-URL 字符串,可以在 HTML 和 HTTP 环境中轻松传递这些字符串,与基于 XML 的标准(例如 SAML)相比,它更紧凑。可以在这里解码,验证与生产 JWT。

JWT 验证流程

JWT使用流程

  1. 用户请求登录服务器

  2. 服务器接到请求生成一个 jwt-token

  3. 把这个 jwt-token 发回到前端

  4. 每次请求的时候带这个 token 和 uid

  5. 收到 jwt-token 首先比较对不对,完后用 secret 解密后再次比较内部信息对不对,是否被更改过。

  6. 认证通过就可以请求别的接口返回对应的 response 了

Nestjs 配置 JWT 认证

以下内容需要有一定的 Nestjs 基础,有 Spring Boot 的基础可以很快上手:Nestjs 文档

个人感觉 Nestjs 开发比 Spring 爽很多,生态肯定是被 Spring 完爆,但是满足企业级快速开发是绰绰有余的,前端可以了解一下。

下面我们来说使用 Nestjs 做 JWT 认证方案:

passport

passport 是一个 nodejs 认证库,他可以使用在许多的生成应用中,Nestjs 可以与 passport 做很好的集成。

认证策略

我们需要两种策略:

  • 用户名/密码身份验证机制(login)
  • Token 身份验证(限制资源获取)

passport 提供了两种包实现这种策略:

  • passport-local
  • passport-jwt

我们需要安装这些包:

1
2
yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt
yarn add @types/passport-local @types/passport-jwt -D

对于不同的规则,有不同的实现,所以我们需要有两套策略的实现

文件目录

我们分别看一下这两种策略的实现:

local-auth.guard.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { AuthService } from "./auth.service";
import { Injectable, UnauthorizedException, Request } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { ModuleRef, ContextIdFactory } from "@nestjs/core";

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

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

authService.validateUser 是对登录验证的模拟,我们会在后面讲解

jwt.strategy.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { jwtConstants } from "./../../common/constants/contants";
import { Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ExtractJwt } from "passport-jwt";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 提供从请求中提取 JWT 的方法
ignoreExpiration: false, // 确保 JWT 没有过期的责任委托给 Passport 模块
secretOrKey: jwtConstants.secret, //提供对称的秘钥来签署令牌
});
}

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

其中,我们需要提供一个秘钥来做 jwt 的加密,所以我们可以将这个常量提取放入 jwtConstants 中为一个单独文件。此处省略那个步骤。

接下来我们需要两种守卫限制路由的权限。

守卫文件

他们分别继承自 AuthGuard(‘local’)与 AuthGuard(‘jwt’),你可以直接使用他们作为守卫,但是为了扩展性和可读性,我们将他们提取出来。

local=auth.guard.ts

1
2
3
4
5
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGurad extends AuthGuard("local") {}

jwt-auth.guard.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
Injectable,
ExecutionContext,
UnauthorizedException,
} from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
canActivate(context: ExecutionContext): any {
// 在这里添加自定义的认证逻辑
// 例如调用 super.logIn(request) 来建立一个session
return super.canActivate(context);
}

handleRequest(err, user, info): any {
// 可以抛出一个基于info或者err参数的异常
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

我们可以通过继承自定义验证逻辑。

通过 service 我们可以实现所有的主要逻辑,包括验证用户与 token 签发

auth-service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { UsersService } from "../users/users.service";
import { JwtService } from "@nestjs/jwt";
import { Injectable } from "@nestjs/common";

interface IUser {
username: string;
userId: number;
}

@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService
) {}

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

async login(user: IUser): Promise<{ access_token: string }> {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload), //返回带有payload的token
};
}
}

此处使用 validateUser 来模拟密码验证,login 模拟 token 的签发。

然后为 Passport 模块做一下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { JwtStrategy } from "./jwt.strategy";
import { jwtConstants } from "./../../common/constants/contants";
import { JwtModule } from "@nestjs/jwt";
import { LocalStrategy } from "./local.strategy";
import { UsersModule } from "../users/users.module";
import { AuthService } from "./auth.service";
import { Module } from "@nestjs/common";
import { AuthController } from "./auth.controller";
import { PassportModule } from "@nestjs/passport";

@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: "jwt" }), //配置默认的策略(JWT),默认策略可以不用指定策略
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: "60s" }, //失效时间
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

使用 controller 模拟路由逻辑即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { AuthService } from './auth.service';
import { Controller, UseGuards, Post, Request, Get } from '@nestjs/common';
import { LocalAuthGurad } from 'src/common/guards/local-auth.guard';
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@UseGuards(LocalAuthGurad)
@Post('login')
async login(@Request() req: any): Promise<any> {
return this.authService.login(req.user);
}

@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req: any) { //这里不能使用异步
return req.user;
}

token 获取:

token获取

token 验证:

token验证

以上!

查看评论