登录注册模块的 JWT 认证机制详解

登录注册模块的 JWT 认证机制详解

深入浅出地讲解本项目中的双 Token 机制、AuthGuard 路由守卫,以及前端后端的完整认证闭环。


一、认证系统的全景架构

在开始深入细节之前,先用一张俯瞰图理解整个认证系统的数据流向:

bash 复制代码
┌──────────────────────────────────────────────────────────────────┐
│                         用户浏览器 (Frontend)                       │
│                                                                      │
│   ┌──────────┐    ┌──────────────┐    ┌─────────────────────────┐  │
│   │ Login 页面 │───▶│ Zustand Store │───▶│  localStorage           │  │
│   │            │    │ (useUserStore)│    │  key: "user-store"       │  │
│   └──────────┘    └──────┬───────┘    └─────────────────────────┘  │
│                           │                                           │
│                           ▼                                           │
│                  ┌────────────────┐                                  │
│                  │  Axios 拦截器    │                                  │
│                  │  自动附加 Bearer │                                  │
│                  │  Token 到请求头   │                                  │
│                  └───────┬────────┘                                  │
│                          │                                            │
│   ┌──────────────────────┼──────────────────────────┐               │
│   │  App.tsx              │     BottomNav.tsx         │               │
│   │  useEffect 路由守卫    │     点击导航守卫           │               │
│   │  (检查 needsLoginPath)│     (拦截未登录点击)        │               │
│   └──────────────────────┴──────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────┘
                           │  HTTP Request
                           │  Authorization: Bearer <access_token>
                           ▼
┌──────────────────────────────────────────────────────────────────────┐
│                      NestJS 后端 (Backend)                             │
│                                                                        │
│   ┌────────────┐     ┌───────────────┐     ┌────────────────────┐    │
│   │ AuthController│───▶│  AuthService   │───▶│  JwtService          │    │
│   │ /api/auth/*   │    │  login()       │    │  signAsync()          │    │
│   │                │    │  refreshToken()│    │  verifyAsync()        │    │
│   └────────────┘     └───────────────┘     └────────────────────┘    │
│                                                                        │
│   ┌────────────────────────────────────────────────────────────┐     │
│   │  JwtAuthGuard (路由守卫)                                      │     │
│   │  extends AuthGuard('jwt')                                     │     │
│   │       │                                                        │     │
│   │       ▼                                                        │     │
│   │  JwtStrategy (验证策略)                                        │     │
│   │  - 从 Authorization Header 提取 Token                         │     │
│   │  - 验证签名 & 过期时间                                          │     │
│   │  - 将用户信息挂载到 request.user                                │     │
│   └────────────────────────────────────────────────────────────┘     │
│                                                                        │
│   ┌──────────┐     ┌───────────────┐                                 │
│   │ UsersModule│◀───│  PrismaService │──▶  PostgreSQL                  │
│   │ register() │    │  (数据库连接)   │                                 │
│   └──────────┘     └───────────────┘                                 │
└──────────────────────────────────────────────────────────────────────┘

二、注册:一切从"你是谁"开始

注册是用户身份体系的入口。用户在客户端填写用户名和密码,数据穿越网络到达后端,经过层层校验后,最终在数据库中落地------但密码永远不会以明文存储。

2.1 前端发起注册

前端收集用户输入的用户名和密码,以简单的 POST 请求发送到 /api/users/register。请求体结构非常简洁:

json 复制代码
{
  "name": "zhangsan",
  "password": "my-secret-123"
}

注意:当前前端工程中尚未实现独立的注册页面,但后端接口已完全就绪,只需一个表单即可打通。

2.2 后端的层层防线

后端收到请求后,首先经过 NestJS 的 ValidationPipe 全局管道:

typescript 复制代码
// main.ts
app.useGlobalPipes(new ValidationPipe({
    whitelist: true,          // 自动剔除 DTO 未定义的字段
    forbidNonWhitelisted: true, // 发现未知字段直接拒绝
    transform: true,           // 自动转换类型(如字符串转数字)
}));

这个管道就像是门口的安检员,任何不符合 DTO 定义的数据都会被拦在门外。其中 CreateUserDto 定义了它的"准入标准":

typescript 复制代码
export class CreateUserDto {
    @IsNotEmpty()
    @IsString()
    name: string;

    @IsNotEmpty()
    @IsString()
    @MinLength(6)    // 密码至少 6 位
    password: string;
}

通过管道校验后,UsersService.register() 执行真正的注册逻辑------这里有两道核心防线:

第一道:查重。 用户名必须全局唯一。通过 Prisma 在数据库中查询同名用户,若已存在则直接抛出 BadRequestException('用户名已存在')

第二道:加密。 密码绝对不能以明文存入数据库------这是安全红线。使用 bcrypt.hash(password, 10) 进行哈希处理,其中 10 代表盐轮次(salt rounds)。10 轮意味着哈希函数会迭代 2^10 = 1024 次,即使是现代硬件也无法快速暴力破解。

最终存入数据库的是一串看起来毫无意义的密文:

perl 复制代码
明文: my-secret-123
密文: $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
       │  │                          │
       │  └── 盐轮次(10)              └── 最终的哈希值
       └── bcrypt 算法标识
typescript 复制代码
// users.service.ts --- register 核心逻辑
async register(createUserDto: CreateUserDto) {
    // 查重
    const existing = await this.prisma.user.findUnique({
        where: { name: createUserDto.name }
    });
    if (existing) throw new BadRequestException('用户名已存在');

    // 哈希
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // 入库(只返回 id 和 name,密码哈希不见天日)
    return this.prisma.user.create({
        data: { name: createUserDto.name, password: hashedPassword },
        select: { id: true, name: true }
    });
}

关键细节select: { id: true, name: true } 确保了密码哈希永远不会出现在 API 响应中。这是一种最小暴露原则------即使某个地方的代码逻辑出错,查询层面就已经把密码字段剪掉了。


三、登录与双 Token 机制:认证系统的灵魂

如果注册是在户籍系统里建立身份档案,那么登录就是每次进入大楼时签发通行证。而本项目采用的"双 Token 机制",则是通行证体系中的一项精巧设计。

3.1 为什么需要两个 Token?

一个直观的类比:

类比物 对应 有效期 用途
门禁卡 access_token 15 分钟 每次进门刷卡,频繁使用
身份证 refresh_token 7 天 门禁卡过期后证明"我确实是我",换取新卡

设计双 Token 的核心原因是一个安全与体验的平衡

  • 如果只有 短期 Token(比如 15 分钟),用户每隔 15 分钟就要重新登录,体验极差。
  • 如果只有 长期 Token(比如 7 天),一旦被窃取,攻击者拥有长达 7 天的操作窗口,风险极高。

双 Token 方案巧妙地将"高频使用"和"长期有效"拆成两张令牌:

  • access_token (高频使用,短期有效):挂在每个请求的 Authorization 头中。即使被中间人截获,15 分钟后自动失效,攻击窗口极小。
  • refresh_token(低频使用,长期有效):只在 access_token 过期时才拿出来用一次,大幅减少在网络中传输的次数,降低了暴露风险。

3.2 登录流程:两张令牌的诞生

用户输入用户名和密码,点击登录。数据流经以下路径:

scss 复制代码
前端 Login.tsx
    │
    │  POST /api/auth/login  { name, password }
    ▼
后端 AuthController.login()
    │
    ▼
AuthService.login()
    │
    ├── 1. prisma.user.findUnique({ where: { name } })   ← 查数据库
    │
    ├── 2. bcrypt.compare(password, user.password)         ← 验证密码
    │       │  失败 → UnauthorizedException('用户名或密码错误')
    │       │  成功 ↓
    │
    └── 3. generateTokens(user.id, user.name)              ← 签发双 Token
            │
            ├── 生成 payload = { sub: user.id, name: user.name }
            │
            └── Promise.all([
                    jwtService.signAsync(payload, { expiresIn: '15m' }),   ← access_token
                    jwtService.signAsync(payload, { expiresIn: '7d'  }),   ← refresh_token
                ])

两个 Token 同时签发,共享同一个 payload(用户 ID 和用户名),使用同一个 TOKEN_SECRET 签名,唯一区别就是过期时间不同。后端将其一并返回:

json 复制代码
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": { "id": 1, "name": "zhangsan" }
}

3.3 前端存储:Zustand + localStorage

前端拿到这两个 Token 后,将它们交给 Zustand 的 useUserStore 管理:

typescript 复制代码
// useUserStore.ts --- login action
login: async (credentials: Credential) => {
    const res = await doLogin(credentials);          // 调用 POST /auth/login
    set({
        accessToken: res.access_token,               // 存到内存
        refreshToken: res.refresh_token,             // 存到内存
        user: res.user,
        isLogin: true,                               // 设置登录状态
    });
}

但 Store 在刷新页面后会丢失数据------这时 zustand/middlewarepersist 中间件登场了:

typescript 复制代码
const useUserStore = create<UserState>()(
    persist(
        (set) => ({ /* state & actions */ }),
        {
            name: 'user-store',                    // localStorage 中的 key
            partialize: (state) => ({              // 只持久化这些字段
                accessToken: state.accessToken,
                refreshToken: state.refreshToken,
                user: state.user,
                isLogin: state.isLogin,
            }),
        }
    )
);

数据流转全景

scss 复制代码
登录成功
    │
    ├──▶ Zustand State (内存)      ← 组件通过 useUserStore() 实时读取
    │
    └──▶ localStorage "user-store" ← 持久化,页面刷新后自动恢复
              │
              ▼
        下次页面加载时,persist 中间件自动 rehydrate
              │
              ▼
        内存和本地存储同步恢复,用户无需重复登录

3.4 每次请求:Token 的自动化装载

用户登录后,每个发往后端的请求都需要携带 access_token。如果让开发者每次手动添加 Token,不仅繁琐还容易遗漏。解决方案是 Axios 请求拦截器

typescript 复制代码
// api/config.ts --- 请求拦截器
axios.interceptors.request.use(config => {
    const token = useUserStore.getState().accessToken;   // 直接从 Store 读取(非响应式)
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

注意这里用的是 useUserStore.getState() 而非 useUserStore()。前者是 Zustand 提供的命令式读取方法,适用于拦截器这种不在 React 组件树中的场景------它直接读取 Store 的当前快照,不触发任何响应式更新。


四、AuthGuard 路由守卫:保护你的后端资源

前端可以随意修改,但后端不行。后端的路由守卫是整个安全体系的最后一道防线,它必须绝对可靠。

4.1 守卫的注册与使用

在 NestJS 中,守卫通过 @UseGuards() 装饰器绑定到控制器方法上。本项目中的 JwtAuthGuard 可以保护任何需要认证的接口:

typescript 复制代码
@Controller('posts')
export class PostsController {
    @Get('mine')
    @UseGuards(JwtAuthGuard)    // ← 只有携带有效 Token 的请求能进来
    async getMyPosts(@Req() req) {
        return this.postsService.findByUser(req.user.id);
    }
}

4.2 守卫的执行链路

当一个携带 Authorization: Bearer <token> 头的请求到达受保护的接口时,执行链路如下:

css 复制代码
HTTP Request
    │
    │  Authorization: Bearer eyJhbGciOi...
    ▼
┌──────────────────────────────────────────────────────┐
│  JwtAuthGuard                                        │
│  extends AuthGuard('jwt')                            │
│                                                      │
│  "这是一个 jwt 策略的守卫"                               │
│  将请求委托给 → JwtStrategy                            │
└──────────────────┬───────────────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────────────┐
│  JwtStrategy (extends PassportStrategy)               │
│                                                      │
│  ① ExtractJwt.fromAuthHeaderAsBearerToken()          │
│     从请求头中提取 "Bearer " 后面的 token 字符串         │
│                                                      │
│  ② 使用 TOKEN_SECRET 验证签名                           │
│     - 签名不对?→ 401 Unauthorized                     │
│     - 签名正确 ↓                                      │
│                                                      │
│  ③ ignoreExpiration: false (默认)                     │
│     检查 Token 是否过期                                 │
│     - 过期了?→ 401 Unauthorized                       │
│     - 未过期 ↓                                        │
│                                                      │
│  ④ validate(payload) 被调用                           │
│     return { id: payload.sub, name: payload.name }   │
│     ↓                                                │
│     返回值自动挂载到 request.user                       │
└──────────────────┬───────────────────────────────────┘
                   │
                   ▼
           Controller 方法执行
           req.user = { id: 1, name: 'zhangsan' }
           开发者可以直接用 req.user.id 获取当前用户

4.3 JwtStrategy 源码解析

typescript 复制代码
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor() {
        super({
            // 告诉 Passport 从哪里找 Token
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            // false = 严格模式,Token 过期直接拒绝
            ignoreExpiration: false,
            // 验签密钥
            secretOrKey: process.env.TOKEN_SECRET || ""
        });
    }

    async validate(payload: any) {
        // 返回值会被注入到 request.user
        // 这里做了一层"翻译":把 JWT 内部字段名转为业务字段名
        return {
            id: payload.sub,    // JWT 规范中 subject 字段 → 业务中的 id
            name: payload.name,
        };
    }
}

值得品味的设计细节:

  • jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() :这是 passport-jwt 提供的内置方法,它会自动解析 Authorization: Bearer <token> 格式的请求头。不需要自己写正则提取逻辑。
  • ignoreExpiration: false:这是 Passport JWT 的默认行为,显式写出来是为了强调"过期 Token 绝不放过"的安全态度。
  • validate() 中的字段映射 :JWT 标准使用 sub(subject)字段存储用户标识,但业务代码中使用 id 更自然。validate() 做了这一层语义转换,让后续代码可以写 req.user.id 而非 req.user.sub

4.4 一个具体的受保护请求示例

当用户在 "我的" 页面查看自己的帖子时:

typescript 复制代码
// 请求:GET /api/posts/mine
// 请求头:Authorization: Bearer eyJ...access_token...

// guard 通过后,Controller 直接拿到已认证的用户信息
@Get('mine')
@UseGuards(JwtAuthGuard)
async getMyPosts(@Req() req) {
    console.log(req.user);  // { id: 1, name: 'zhangsan' }
    // 无需从请求参数中手动获取用户 ID
    // 无需担心用户伪造身份
    return this.postsService.findByUser(req.user.id);
}

这就是 AuthGuard 最大的价值:它将"身份验证"和"业务逻辑"彻底解耦 。Controller 不需要知道 Token 是如何验证的,它只需要信任 req.user 已经是真实有效的用户信息。


五、Token 刷新:不打扰用户的续期机制

想象一个场景:你正在填写一个长表单,花了 20 分钟。当你点击提交时,access_token 已经过期(15 分钟),你被无情地踢回登录页,所有填好的数据都丢了。这正是 Token 刷新机制要解决的问题。

5.1 刷新流程

后端的刷新逻辑非常简洁:

typescript 复制代码
// auth.service.ts
async refreshToken(rt: string) {
    try {
        // 验证 refresh_token 是否有效
        const payload = await this.jwtService.verifyAsync(rt, {
            secret: process.env.TOKEN_SECRET,
        });
        // 有效 → 签发全新的 Token 对
        return this.generateTokens(payload.sub, payload.name);
    } catch (e) {
        // 无效或过期 → 用户需要重新登录
        throw new UnauthorizedException('Refresh Token 已失效,请重新登录');
    }
}

调用方式:前端在感知到 access_token 过期后(通常是通过 401 响应),取出存储的 refresh_token,调用 POST /api/auth/refresh,然后无缝替换新的 access_token

bash 复制代码
用户视角:
  "一切正常,我甚至不知道 Token 刷新了。"

后台实际发生的事情:
  access_token 过期
    → 前端拦截器检测 401
    → 自动调用 /api/auth/refresh
    → 获得新的 access_token 和 refresh_token
    → 用新 access_token 重试原来的请求
    → 用户毫无感知

注意:当前前端代码中尚未实现这一自动刷新逻辑------Axios 的响应拦截器在收到非 2xx 状态码时只是简单抛出错误。这是后续迭代的重要方向。

5.2 Refresh Token 的滚动更新

注意 refreshToken() 在验证通过后调用的是 generateTokens()------这意味着每次刷新都会同时获得新的 access_token 和新的 refresh_token。这不只是换了一张新门禁卡,连身份证也换了。这个策略的优势在于:

  • 缩短攻击窗口:即使某个 refresh_token 被窃取,一旦合法用户进行了刷新操作,旧的 refresh_token 就变成了废纸。
  • 无限续期:只要用户在 7 天内使用过一次应用,登录状态就能持续保持。只有连续 7 天不访问,才需要重新输入密码。

六、前端的路由守卫:一道优雅的门禁

后端有 AuthGuard 保护接口,前端也需要对应的守卫保护页面。想象一个未登录用户直接在浏览器地址栏输入 /mine------如果没有前端守卫,他会看到一堆报错的空白态,体验很差。

6.1 核心守卫逻辑

本项目实现了两个层面的前端守卫:

应用级守卫(App.tsx)------监听 URL 变化:

typescript 复制代码
const needsLoginPath = ['/mine', '/order', '/chat'];

function App() {
    const { isLogin } = useUserStore();
    const navigate = useNavigate();
    const { pathname } = useLocation();

    useEffect(() => {
        if (!isLogin && needsLoginPath.includes(pathname)) {
            navigate('/login');         // 未登录 → 跳转登录页
        }
    }, [isLogin, navigate, pathname]);

    // ... 渲染路由
}

这个 useEffect 像是一个站岗的哨兵 ------每当 URL 路径 (pathname) 或登录状态 (isLogin) 发生变化时,它都会重新检查一次:当前路径是否需要登录?用户是否已登录?如果答案是"需要但未登录",立刻重定向到登录页。

导航级守卫(BottomNav.tsx)------拦截点击行为:

typescript 复制代码
const handleNav = (path: string) => {
    if (path === pathname) return;              // 不重复导航到当前页
    if (needsLoginPath.includes(path) && !isLogin) {
        navigate('/login');                     // 拦截未登录点击
        return;
    }
    navigate(path);
};

这层守卫确保用户即便从底部导航栏点击受保护的 tab,也会在进入页面前被拦下。它与应用级守卫形成了双重保障

6.2 为什么前端路由守卫不能替代后端守卫?

一个常见的误解是:"前端已经做了路由守卫,后端是不是就可以放松了?"

答案是否定的。 因为前端代码运行在用户的浏览器中,用户可以:

  • 打开浏览器 DevTools,修改 localStorage 中的 isLogintrue
  • 直接使用 curl 或 Postman 发送请求,绕过浏览器
  • 修改前端源代码,注释掉守卫逻辑

因此后端的 JwtAuthGuard 才是真正的安全防线 。前端路由守卫的目的是提升体验------在用户看到报错之前就引导到正确的页面------而非保证安全。


七、完整的认证时序图

下面用一张完整的时序图将注册、登录、Token 刷新和路由守卫串联起来:

bash 复制代码
  用户浏览器                     前端 App               Zustand Store         后端 NestJS              PostgreSQL
     │                             │                        │                     │                       │
     │                             │                        │                     │                       │
     │  ═══ 注册 ═══               │                        │                     │                       │
     │                             │                        │                     │                       │
     │  填写注册表单                 │                        │                     │                       │
     │ ───────────────────────────▶│                        │                     │                       │
     │                             │  POST /api/users/register                     │                       │
     │                             │ ───────────────────────────────────────────▶│                       │
     │                             │                        │                     │  bcrypt.hash(pw,10)   │
     │                             │                        │                     │ ────────────────────▶│
     │                             │                        │                     │  存入 users 表         │
     │                             │                        │                     │ ◀────────────────────│
     │                             │   返回 { id, name }    │                     │                       │
     │                             │ ◀───────────────────────────────────────────│                       │
     │                             │                        │                     │                       │
     │  ═══ 登录 ═══               │                        │                     │                       │
     │                             │                        │                     │                       │
     │  填写用户名密码               │                        │                     │                       │
     │ ───────────────────────────▶│                        │                     │                       │
     │                             │  POST /api/auth/login  │                     │  bcrypt.compare()     │
     │                             │ ───────────────────────────────────────────▶│ ────────────────────▶│
     │                             │                        │                     │                       │
     │                             │  access_token (15min) + refresh_token (7d)   │                       │
     │                             │ ◀───────────────────────────────────────────│                       │
     │                             │                        │                     │                       │
     │                             │  login() 更新状态       │                     │                       │
     │                             │ ─────────────────────▶│                     │                       │
     │                             │                        │  persist 到 localStorage                  │
     │                             │  导航到首页             │                     │                       │
     │ ◀───────────────────────────│                        │                     │                       │
     │                             │                        │                     │                       │
     │  ═══ 访问受保护页面 ═══       │                        │                     │                       │
     │                             │                        │                     │                       │
     │  点击 "我的" tab             │                        │                     │                       │
     │ ───────────────────────────▶│                        │                     │                       │
     │                             │  BottomNav 守卫检查     │                     │                       │
     │                             │  isLogin === true ✓    │                     │                       │
     │                             │  放行                      │                     │                       │
     │                             │                        │                     │                       │
     │  页面发起数据请求             │                        │                     │                       │
     │ ───────────────────────────▶│                        │                     │                       │
     │                             │  Axios 拦截器           │                     │                       │
     │                             │  自动附加 Bearer Token  │                     │                       │
     │                             │                        │                     │                       │
     │                             │  GET /api/posts/mine   │                     │                       │
     │                             │  Authorization: Bearer <access_token>        │                       │
     │                             │ ───────────────────────────────────────────▶│                       │
     │                             │                        │     JwtAuthGuard     │                       │
     │                             │                        │     → JwtStrategy    │                       │
     │                             │                        │       验证签名 ✓      │                       │
     │                             │                        │       未过期 ✓       │                       │
     │                             │                        │       validate()     │                       │
     │                             │                        │       → req.user     │                       │
     │                             │                        │                     │  Controller 执行业务   │
     │                             │                        │                     │ ◀────────────────────│
     │                             │   返回用户数据           │                     │                       │
     │                             │ ◀───────────────────────────────────────────│                       │
     │  看到自己的内容               │                        │                     │                       │
     │ ◀───────────────────────────│                        │                     │                       │
     │                             │                        │                     │                       │
     │  ═══ Token 刷新 ═══          │                        │                     │                       │
     │  (access_token 已过期 15min+)│                        │                     │                       │
     │                             │                        │                     │                       │
     │                             │  请求返回 401          │                     │                       │
     │                             │ ◀───────────────────────────────────────────│                       │
     │                             │                        │                     │                       │
     │                             │  POST /api/auth/refresh                      │                       │
     │                             │  { refresh_token }                            │                       │
     │                             │ ───────────────────────────────────────────▶│                       │
     │                             │                        │    verify refresh    │                       │
     │                             │   新的 access_token     │    签发新双 Token     │                       │
     │                             │   + refresh_token      │                     │                       │
     │                             │ ◀───────────────────────────────────────────│                       │
     │                             │                        │                     │                       │
     │                             │  更新 Store            │                     │                       │
     │                             │ ─────────────────────▶│                     │                       │
     │                             │                        │  persist 新 Token    │                       │
     │                             │                        │                     │                       │
     │                             │  重试原来的请求 ✓        │                     │                       │
     │                             │                        │                     │                       │

八、安全设计要点

8.1 密码安全

  • bcrypt 哈希(盐轮次 = 10):即使数据库被拖库,攻击者也难以从哈希值逆向出明文密码。
  • 最小暴露 :数据库查询层面使用 select 限定返回字段,密码哈希永远不会随 API 响应泄露。
  • 管道白名单whitelist: true + forbidNonWhitelisted: true,防止客户端注入未预期的字段。

8.2 Token 安全

机制 说明
短期 access_token (15min) 即使被窃取,攻击窗口仅 15 分钟
长期 refresh_token (7d) 低频传输,降低暴露概率
HS256 签名 服务端保管密钥,任何人无法伪造 Token
滚动刷新 每次刷新同时更新两个 Token,旧 Token 立即失效
过期检查 ignoreExpiration: false,过期 Token 直接拒绝

8.3 当前系统的改进空间

作为一个正在演进中的项目,当前的认证系统还有以下可以优化的方向:

  1. 前端 Token 刷新拦截器:目前响应拦截器在收到 401 时没有自动重试,这是待实现的关键特性。
  2. 登出功能 :Zustand Store 中缺少 logout action,Token 无法主动失效。
  3. refresh_token 持久化校验:可以考虑在数据库中存储 refresh_token 的哈希,实现服务端的主动撤销能力。
  4. CORS 细粒度配置 :当前 cors: true 允许所有来源,生产环境应限制为具体域名。
  5. 速率限制 :登录和注册接口应添加限流(如 @nestjs/throttler),防止暴力破解。
  6. 注册页面前端实现:后端注册接口已就绪,前端注册页面尚未实现。

九、技术栈一览

层级 技术 角色
前端状态管理 Zustand + persist 管理 Token 和用户信息,持久化到 localStorage
前端 HTTP Axios + 拦截器 自动附加 Bearer Token,统一错误处理
前端路由守卫 React Router v6 + useEffect 检测登录状态,拦截未授权页面访问
后端框架 NestJS 模块化架构,依赖注入
身份验证 Passport + passport-jwt JWT 策略,Token 提取和验证
加密 bcrypt (10 轮) 密码哈希存储
JWT @nestjs/jwt (jsonwebtoken) Token 签发和验证,HS256 签名
数据校验 class-validator + class-transformer DTO 验证,类型转换
数据库 Prisma ORM + PostgreSQL 用户数据持久化

十、总结

本项目的登录注册模块实现了一个结构清晰、安全可用的 JWT 认证体系。它遵循了以下核心设计原则:

  • 短 Token 高高频,长 Token 低低频:access_token 只有 15 分钟的生命周期,最大限度地缩小了 Token 失窃的风险窗口;refresh_token 用 7 天的有效期换来了流畅的用户体验。
  • 守卫分层,各司其职 :后端的 JwtAuthGuard 提供不可绕过安全屏障,前端的路由守卫提供无缝的用户体验,两者互补而非替代。
  • Token 携带自动化:Axios 拦截器让开发者几乎忘记 Token 的存在------它被静默地注入每个请求,就像刷卡门禁在你靠近时自动感应。
  • 密码不留痕:bcrypt 哈希 + 查询层裁剪,确保密码的明文和密文都不会出现在任何 API 响应中。

一个成熟的认证系统,不在于它有多少花哨的特性,而在于每一个设计决策背后都有清晰的"为什么"。15 分钟和 7 天的选择不是随意的数字,而是经过对安全性、用户体验和工程复杂度的综合权衡。理解这些"为什么",比记住具体的代码实现更为重要。

相关推荐
木易 士心1 小时前
深度解析:一个 Java 对象究竟占用多少字节?
java·开发语言·后端
夜猫子ing1 小时前
《嵌入式 Linux 控制服务从零搭建(二):从目录结构到 CMakeLists,搭一个像样的 C++ 工程骨架》
java·前端·c++
Lee川8 小时前
面试通关:JWT 认证与双 Token 机制深度解析
后端·面试
kyriewen9 小时前
百度用6%成本碾压硅谷?中国AI把性价比玩明白了
前端·百度·ai编程
kyriewen10 小时前
你还在手动敲命令部署?GitHub Actions 让你 push 即上线,摸鱼时间翻倍
前端·面试·github
想学习java初学者10 小时前
SpringBoot整合Vertx-Mqtt多租户(优化版)
java·spring boot·后端
Csvn11 小时前
Python 性能优化与 Profiling 工具
后端·python
Csvn11 小时前
Pinia 状态管理
前端
不减20斤不改头像12 小时前
手机一句话开发贪吃蛇!TRAE SOLO 移动端 AI 编程实测
前端·后端