JWT 认证
JWT 是什么?
JWT 是 JSON Web Token 的简称,根据官网述说,它是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。由于此信息是经过数字签名的,因此可以被验证和信任。
应用场景?
- 授权:可以使用 JWT 作为登录后的请求认证,它的请求开销小且可以实现分布请求。
- 信息交换:使用 JWT 可以在各方之间安全地传输信息。可以对 JWT 进行签名,验证信息是否被修改。
JWT 特点
- 体积小
- 请求方式多样(GET/POST/HTTP-Header)
- 结构化,可携带信息
- 实现单点登录与跨域验证
JWT 原理
JWT 由以下结构组成:
header.payload.secret
- 标头(header【base64 后】)
- 有效荷载(payload【base64 后】)
- 签名(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-token
把这个 jwt-token 发回到前端
每次请求的时候带这个 token 和 uid
收到 jwt-token 首先比较对不对,完后用 secret 解密后再次比较内部信息对不对,是否被更改过。
认证通过就可以请求别的接口返回对应的 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(), ignoreExpiration: false, 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 { return super.canActivate(context); }
handleRequest(err, user, info): any { 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) { 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), }; } }
|
此处使用 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" }), 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 验证:
以上!