Nest.js 登录接口开发指南
随着Web应用的不断发展,安全性和用户体验成为了开发者关注的重点。Nest.js作为一个渐进式Node.js框架,它基于现代JavaScript标准,提供了丰富的功能来构建高效、可扩展的应用程序。本文将指导您如何在Nest.js中开发一个安全的登录接口,包括用户认证、密码校验、JWT生成及验证等关键步骤。
1. 登录接口技术方案设计
在开始编码之前,首先需要明确登录接口的技术方案。通常,登录接口需要完成以下任务:
- 验证用户提供的用户名和密码是否正确。
- 生成并返回一个安全的token给客户端。
- 在后续的API请求中,通过token验证用户身份。
为了实现这些功能,我们将使用以下技术栈:
- Nest.js 作为后端框架。
- TypeORM 作为ORM工具,用于数据库操作。
- JSON Web Tokens (JWT) 用于用户会话管理。
2. NestJs 请求守卫开发
Nest.js 中的守卫(Guards)可以用来控制访问特定路由的权限。在登录接口中,我们需要创建一个守卫来检查请求是否包含有效的token。这可以通过实现CanActivate接口来完成。
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';
import { JwtService } from '@nestjs/jwt';
import { JWT_SECRET_KEY } from './auth.jwt.secret';
/**
* `AuthGuard` 类实现了 NestJS 的 CanActivate 接口,用于控制路由的访问权限。
* 它通过检查路由是否标记为公共(无需认证)来决定是否允许访问。
*/
@Injectable()
export class AuthGuard implements CanActivate {
/**
* 构造函数,注入 Reflector 服务。
* @param reflector Nest 的反射器,用于获取元数据。
*/
constructor(
private reflector: Reflector,
private jwtService: JwtService,
) {}
/**
* 判断当前执行上下文是否可以激活,即是否允许访问。
* @param context 当前的执行上下文,包含请求和响应的信息。
* @returns 返回一个布尔值、布尔值的 Promise 或 Observable,表示是否允许访问。
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
// 从当前的执行上下文中获取处理函数和类,检查它们是否被标记为公共。
const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
// 如果当前路由被标记为公共,则允许访问。
if (isPublic) {
return true;
}
// 获取当前请求的上下文并提取HTTP请求对象
const request = context.switchToHttp().getRequest();
// 从请求中提取令牌
const token = extractToken(request);
// 如果没有令牌,则抛出未授权异常
if (!token) {
throw new UnauthorizedException();
}
try {
// 验证令牌并获取载荷数据
const payload = await this.jwtService.verifyAsync(token, {
secret: JWT_SECRET_KEY,
});
// 将验证后的用户信息附加到请求对象中
request['user'] = payload;
} catch (error) {
// 如果验证失败,抛出未授权异常,并包含错误信息
throw new UnauthorizedException(error.message);
}
// 返回true表示验证通过
return true;
}
}
/**
* 从请求中提取认证令牌
*
* 此函数旨在处理请求对象,并从中提取出认证令牌它假定认证信息遵循一定的格式,
* 即以'Bearer'类型后跟实际的令牌值如果请求的认证头不符合预期格式或不存在,
* 函数将返回undefined
*
* @param request 包含认证头的请求对象
* @returns 如果认证类型为'Bearer',则返回令牌字符串,否则返回undefined
*/
function extractToken(request) {
// 分割认证头以提取类型和令牌值如果认证头不存在或格式不正确,将得到一个空数组
const [type, token] = request.headers.authorization?.split(' ') ?? [];
// 如果认证类型是'Bearer',则返回令牌值;否则,返回undefined
return type === 'Bearer' ? token : undefined;
}
3. 实现登录鉴权接口的调用链路
登录接口的调用链路通常包括以下几个步骤:
- 用户发送包含用户名和密码的POST请求至
/auth/login。 - 后端接收到请求后,使用ORM查询数据库以验证用户信息。
- 如果验证成功,生成JWT token,并将其返回给客户端。
- 客户端存储此token,并在后续请求中将其作为认证凭据。
4. 登录密码校验的逻辑实现
密码校验是登录过程中非常重要的一步。我们使用md5对密码进行加密。在验证密码时,需要将用户输入的密码与数据库中加密过的密码进行比较。
// 登录方法,根据用户名和密码进行身份验证
async login(username, password) {
// 根据用户名查找用户
const user = await this.userService.findByUsername(username);
// 将输入的密码进行MD5加密并转为大写,以匹配存储的密码
const md5Password = md5(password).toUpperCase();
// 如果加密后的密码与数据库中的密码不匹配,抛出未授权异常
if (user.password !== md5Password) {
throw new UnauthorizedException();
}
// 创建JWT令牌的负载信息,包含用户名和用户ID
const payload = { username: user.username, userid: user.id };
// 返回包含JWT令牌的对象
return {
token: await this.jwtService.signAsync(payload),
};
}
}
5. JWT 基本概念
5.1 Token 是什么
Token 本质是字符串,用于请求时附带在请求头中,校验请求是否合法及判断用户身份
5.2 Token 与 Session、Cookie 的区别
- Session 保存在服务端,用于客户端与服务端连接时,临时保存用户信息,当用户释放连接后,Session 将被释放;
- Cookie 保存在客户端,当客户端发起请求时,Cookie 会附带在 http header 中,提供给服务端辨识用户身份;
- Token 请求时提供,用于校验用户是否具备访问接口的权限。
5.3 Token 的用途
Token 的用途主要有三点:
- 拦截无效请求,降低服务器处理压力;
- 实现第三方 API 授权,无需每次都输入用户名密码鉴权;
- 身份校验,防止 CSRF 攻击。
5.6 JWT 简析
JSON Web Token(JWT)是非常流行的跨域身份验证解决方案。
5.7 JWT 构成
下面是一串 JWT 字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.EJzdDeNLOQhy-2WmXuK1B49xF17Tk0pja1tCPp81YjY
1
它将被解析为以下三部分:
5.8 HEADER: ALGORITHM & TOKEN TYPE
JWT 头部分是一个描述 JWT 元数据的 JSON 对象:
- alg:表示加密算法,HS256 是 HMAC SHA256 的缩写
- typ:token 类型
{
"alg": "HS256",
"typ": "JWT"
}
5.9 PAYLOAD: DATA
JWT 数据部分,payload 是 JWT 的主体内容部分,也是一个 JSON 字符串,包含需要传递的数据,注意 payload 部分不要存储隐私数据,防止信息泄露
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
5.10 VERIFY SIGNATURE
JWT 签名部分是对上面两部分数据加密后生成的字符串,通过 header 指定的算法生成加密字符串,以确保数据不会被篡改。
生成签名时需要使用密钥(即下方示例中的 abcdefg),密钥只保存在服务端,不能向用户公开,它是一个字符串,我们可以自由指定。
生成签名时需要根据 header 中指定的签名算法,并根据下方的公式,即将 header 和 payload 的数据通过 base64加密后用 . 进行连接,然后通过密钥进行 SHA256 加密,由于加入了密钥,所以生成的字符串将无法被破译和篡改,只有在服务端才能还原
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
abcdefg
)
我们可以在 jwt.io/ 调试 JWT 字符串
6. JWT token 生成逻辑实现
在用户成功登录后,我们需要生成一个JWT token。Nest.js 提供了@nestjs/jwt模块来简化这一过程。
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async login(user: any) {
// 创建JWT令牌的负载信息,包含用户名和用户ID
const payload = { username: user.username, userid: user.id };
// 返回包含JWT令牌的对象
return {
token: await this.jwtService.signAsync(payload),
};
}
}
7. 后端请求守卫 token 验证逻辑
前面提到的AuthGuard类实现了token的基本验证逻辑。每当有受保护的路由被请求时,这个守卫会被自动调用。如果token有效,请求将继续执行;否则,请求将被拒绝。
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
...
// 从请求中提取令牌
const token = extractToken(request);
// 如果没有令牌,则抛出未授权异常
if (!token) {
throw new UnauthorizedException();
}
try {
// 验证令牌并获取载荷数据
const payload = await this.jwtService.verifyAsync(token, {
secret: JWT_SECRET_KEY,
});
// 将验证后的用户信息附加到请求对象中
request['user'] = payload;
} catch (error) {
// 如果验证失败,抛出未授权异常,并包含错误信息
throw new UnauthorizedException(error.message);
}
// 返回true表示验证通过
return true;
}
}
/**
* 从请求中提取认证令牌
*
* 此函数旨在处理请求对象,并从中提取出认证令牌它假定认证信息遵循一定的格式,
* 即以'Bearer'类型后跟实际的令牌值如果请求的认证头不符合预期格式或不存在,
* 函数将返回undefined
*
* @param request 包含认证头的请求对象
* @returns 如果认证类型为'Bearer',则返回令牌字符串,否则返回undefined
*/
function extractToken(request) {
// 分割认证头以提取类型和令牌值如果认证头不存在或格式不正确,将得到一个空数组
const [type, token] = request.headers.authorization?.split(' ') ?? [];
// 如果认证类型是'Bearer',则返回令牌值;否则,返回undefined
return type === 'Bearer' ? token : undefined;
}
通过上述步骤,我们可以构建一个安全且高效的登录接口。