大数跨境
0
0

从零带你手写 JWT 认证,搞定前端登录鉴权!

从零带你手写 JWT 认证,搞定前端登录鉴权! 前端技术编程
2025-11-26
0

在前端开发的江湖里,“登录”是一个我们绕不开的话题。它看似简单,背后却隐藏着一系列关于状态、安全和用户体验的深思熟虑。我们都遇到过这样的场景:网站是如何“记住”我们的?为什么关闭浏览器再打开,依然是登录状态?

这一切的背后,都离不开“授权”二字。HTTP 协议本身是无状态的,它记不住你是谁。为了解决这个问题,前辈们想出了两种主流方案:一种是基于服务端的 Session-Cookie 机制,另一种则是我们今天要深入探讨的,更现代、更灵活的 JWT (JSON Web Token) 方案。

本文将带你从零开始,在一个完整的 React 项目中,一步步实现一个健壮、可扩展的 JWT 登录鉴权流程。我们将用到 React Router 进行路由管理,Zustand 进行全局状态管理,并用 Axios 处理 API 请求。

读完本文,你不仅能掌握 JWT 的核心思想,更能学会如何搭建一个清晰、优雅的前端认证系统。让我们开始吧!

一、项目架构:一切始于合理的规划

一个好的项目,始于清晰的目录结构。它能让我们的代码更易于维护和理解。在我们的 jwt-demo 项目中,代码被精心组织在以下几个核心目录中:

  • @/mock: 前后端分离开发的“模拟神器”。它让我们在没有后端接口的情况下,也能独立完成前端开发和调试。
  • @/api: 应用的“通信层”。所有与后端的数据交互都集中在这里,方便管理和维护。
  • @/store: 全局状态的“大脑”。我们使用 Zustand 在这里管理用户的登录状态和信息。
  • @/components: 可复用的“积木”。比如导航栏、路由守卫等公共组件。
  • @/views: 用户看到的“页面”。如首页、登录页、支付页等。
  • @/App.jsx & @/main.jsx: 应用的“心脏”。前者负责定义全局路由和布局,后者是整个 React 应用的入口。

这样的分层结构,让每一部分代码都各司其职,是现代前端工程化的最佳实践。

二、JWT 核心思想:一个“令牌”引发的故事

在深入代码之前,我们必须理解 JWT 到底是什么,以及它在整个认证流程中扮演的角色。

想象一下,你登录成功后,服务器不再是在自己家里(Session)给你记个小本本,而是发给你一个加密的“令牌”(Token)。这个令牌本身就包含了你的身份信息和权限。

我们先从宏观上看一下标准的认证流程:

屏幕截图 2025-07-25 102502.png

这个流程清晰地展示了“用户”、“客户端应用”、“授权服务器”和“资源服务器”之间的关系。用户通过授权服务器拿到令牌(Access Token),然后用这个令牌去访问资源服务器上的数据。

而这个令牌(Token),通常就是 JWT。它由三个部分组成:

  1. Header (头部) : 描述令牌的元数据,比如类型和加密算法。
  2. Payload (载荷) : 存放实际的用户信息或权限信息。
  3. Signature (签名) : 这是最关键的部分。服务器用一个密钥 (Secret) ,将头部和载荷进行加密,生成签名。

当你后续访问需要权限的页面时,只需在请求头里带上这个令牌。服务器收到后,会用相同的密钥来验证签名的真伪。如果验证通过,服务器就确认了你的身份和权限,并返回你想要的数据。

 这种方式的核心优势在于无状态。服务器无需存储任何 Session 信息,大大减轻了服务器的负担,也使得应用更容易横向扩展。

三、深度辨析:ID Token vs. Access Token

在更专业、更安全的认证体系(如 OAuth 2.0, OpenID Connect)中,我们通常会接触到两种令牌:ID Token 和 Access Token。虽然在我们的简单项目中只用了一个 JWT,但理解它们的区别对你的成长至关重要。

 这张图完美地解释了二者的差异:

  • ID Token (身份令牌,左侧蓝色) :

    • 作用:像一张身份证,用来确认用户的身份。
    • 内容:包含用户的基本信息,如姓名、邮箱、头像等。它的目的是告诉客户端应用:“嘿,这是张三,他已经成功登录了。”
    • 给谁看:主要由**前端(客户端)**使用。
  • Access Token (访问令牌,右侧粉色) :

    • 作用:像一把**“钥匙”** 或一张 “门禁卡”,用来访问受保护的资源。
    • 内容:通常只包含权限信息(scope),比如“读取数据”、“写入数据”。它不应该包含敏感的用户信息。
    • 给谁看:主要由**后端(资源服务器)**使用。资源服务器只关心这把“钥匙”能不能开门,不关心“钥匙”是谁的。

在我们的 jwt-demo 项目中,为了简化,我们将身份和权限信息都放在了一个 JWT 里,它同时扮演了两个角色。但在大型应用中,将二者分离是更安全、更规范的设计。

四、实战演练:一步步构建认证流程

理论讲完了,让我们卷起袖子,用代码说话!

第 1 步:用 @/mock 模拟一个“假”后端

在 mock/login.js 中,我们用几行代码就模拟出了登录和获取用户信息的接口。

// mock/login.js
import jwt from'jsonwebtoken';
const secret = 'gjdkgjjg'// 用于加密的密钥,实际项目中应更复杂且保密

exportdefault [
    {
        url'/api/login',
        method'post',
        response(req) => {
            const {username, password} = req.body;
            if(username !== 'admin' || password !== '123456'){
                return { code1message'用户名或密码错误' };
            }
            // 登录成功,用 jwt.sign 颁发令牌
            const token = jwt.sign(
                { user: { id"001"username"admin" } }, 
                secret, 
                { expiresIn86400 } // 令牌有效期 24 小时
            );
            return { token, data: { id"001"username"admin" } };
        }
    },
    // ... 其他 mock 接口
]

这里的核心是 jwt.sign() 方法。它接收三样东西:你想塞进令牌的数据(Payload),加密用的密钥(Secret),以及一些配置项(如 expiresIn 有效期)。一个神圣的令牌就此诞生!

第 2 步:用 Zustand 管理全局状态

用户的登录状态是全局的,NavBar 需要知道是否显示欢迎语,路由需要知道是否允许访问。Zustand 是一个轻量级的状态管理库,非常适合这个场景。

在 store/user.js 中,我们创建了一个 useUserStore

// store/user.js
import { create } from'zustand';
import { doLogin } from'../api/user';

exportconst useUserStore = create(set => ({
    usernull,      // 用户信息
    isLoginfalse,  // 是否登录
    // 登录 action
    loginasync ({username, password}) => {
        const res = await doLogin({username, password});
        const { token, data: user } = res.data;
        // 核心操作:将令牌存入 localStorage
        localStorage.setItem('token', token);
        // 更新 store
        set({ user, isLogintrue });
    },
    // 登出 action
    logout() => {
        localStorage.removeItem('token');
        set({ user: nullisLoginfalse });
    }
}));

这个 Store 定义了所有与用户状态相关的“原子操作”:

  • login 方法负责调用 API,成功后将令牌存入 localStorage 并更新全局状态。
  • logout 方法则负责清理令牌和状态。

第 3 步:优雅地处理 API 请求

如何让每次 API 请求都自动带上我们的令牌呢?答案是 Axios 拦截器

在 api/config.js 中,我们配置了一个请求拦截器:

// api/config.js
import axios from "axios";
// 请求拦截器
axios.interceptors.request.use((config) => {
    const token = localStorage.getItem('token') || "";
    if(token){  
        // 遵循 Bearer token 规范
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
})

export default axios;

这段代码太优雅了!它会在每个请求发送前,自动从 localStorage 读取令牌,并将其放入 Authorization 请求头中。从此,我们再也不用在每个 API 调用里手动添加 token 了。

第 4 步:打造登录页面和导航栏

我们的登录页面 views/Login/index.jsx 使用了 useRef 来获取表单值,这是一种非受控组件的实现方式,对于简单表单来说代码更简洁。

// views/Login/index.jsx
const Login = () => {
    const usernameRef = useRef();
    const passwordRef = useRef();
    const { login } = useUserStore(); // 从 store 中获取 login action
    const navigate = useNavigate();

    const handleLogin = (e) => {
        e.preventDefault();
        // ...
        login({ username, password }); // 调用 action
        navigate('/'); // 跳转到首页
    }
    // ... JSX
}

当用户点击登录,我们直接调用从 useUserStore 中解构出来的 login 方法,一行代码就驱动了整个登录流程!

同时,components/NavBar/index.jsx 组件也在监听 useUserStore 的变化,以动态地展示“登录”链接或“欢迎你,admin”:

// components/NavBar/index.jsx
const NavBar = () => {
    const { isLogin, user, logout } = useUserStore();
    return (
        <nav>
            {isLogin ? (
                <>
                    <span>欢迎:{user.username}</span>
                    <button onClick={logout}>Logout</button>
                </>

            ) : (
                <Link to="/login">Login</Link>
            )}
        </nav>
    )
}

第 5 步:用“路由守卫”保护你的页面

有些页面,比如“支付页”,我们不希望未登录的用户看到。这时,“路由守卫”就该登场了。

我们创建了一个 RequireAuth 组件:

// components/RequireAuth/index.jsx
import { useUserStore } from'../../store/user';
import { Navigate, useLocation } from'react-router-dom';

const RequireAuth = ({ children }) => {
    const { isLogin } = useUserStore();
    const { pathname } = useLocation();

    if (!isLogin) {
        // 如果未登录,重定向到 /login
        // 并将当前想访问的路径记录下来,以便登录后跳回
        return<Navigate to="/login" state={{ from: pathname }} replace />;
    }

    return children;
}
exportdefault RequireAuth;

这个组件逻辑很清晰:如果 isLogin 为 false,就重定向到登录页。

最后,在 App.jsx 中,我们像使用一个普通的组件一样,用它来包裹需要保护的路由:

// App.jsx
function App({
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
      <Route 
        path="/pay" 
        element={
          <RequireAuth>

            <Pay />
          </RequireAuth>
        } 
      />
    </Routes>

  );
}

现在,任何试图在未登录状态下访问 /pay 的用户,都会被优雅地引导至登录页面。

五、流程全景图

至此,我们已经串联起了整个认证流程。让我们用一张图来回顾一下:

用户登录页Zustand StoreAxios拦截器Mock API全局状态更新,UI自动响应isLogin is true1. 输入账号密码,点击登录2. 调用 login(data) action3. 发起 doLogin() API 请求4. [请求拦截] 附加 Authorization Header5. 验证通过,返回 Token6. 请求成功,返回数据7. 将 Token 存入 localStorage8. set({ isLogin: true, user })9. 导航到受保护页面(/pay)10. RequireAuth组件检查 isLogin11. 允许访问用户登录页Zustand StoreAxios拦截器Mock API

总结

通过这个项目,我们不仅实现了一个完整的 JWT 登录鉴权流程,更深入地理解了其背后的核心思想和行业最佳实践。

  • 职责分离:API、状态管理、UI 组件各司其职,授权服务器和资源服务器的分离也是这一思想的体现。
  • 状态驱动:UI 的变化由全局状态 isLogin 统一驱动,实现了真正的响应式。
  • 自动化:Axios 拦截器自动处理了 Token 的附加,避免了重复劳动。
  • 概念深化:我们辨析了 ID Token 和 Access Token 的区别,这对于构建更大型、更安全的应用至关重要。

希望这篇文章能帮助你彻底搞懂前端登录鉴权。当然,这个项目还有可以优化的地方,比如更优雅地处理 Token 过期、在登录后自动跳转回之前的页面等,这些都留给你作为练习。

作者:爱学习的茄子

https://juejin.cn/post/7530558924165922826

       
---END---

图片
        
          
最近面试BAT,整理一份面试资料前端面试BAT通关手册,覆盖了前端技术、CSS、JavaScript、框架、 数据库、数据结构等等。
获取方式:关注公众号并回复 前端 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡


【声明】内容源于网络
0
0
前端技术编程
专注于分享前端技术:JS,HTML5,CSS3,Vue.js,React,Angular 等前端框架和前端开源项目推荐!
内容 1148
粉丝 0
前端技术编程 专注于分享前端技术:JS,HTML5,CSS3,Vue.js,React,Angular 等前端框架和前端开源项目推荐!
总阅读103
粉丝0
内容1.1k