文章目录
- 1 JWT
- 1.1 JWT结构
- 1.2 工作流程
- 1.3 优点
- 1.4 缺点
- 1.5 安全实践
- 1.6. 适用场景
- 1.7 JWT与OAuth2
- **8. 示例代码(Node.js)**
- 2 用户mock和api
- 3 注册
- 4 登录
- 5 token存储
- 6 请求拦截器设置token
- 6 获取用户信息
- 7 退出登录
- 结语
1 JWT
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全传输信息。它通过数字签名确保数据的完整性和可信性,常用于身份验证和授权。以下是JWT的详细介绍:
1.1 JWT结构
JWT由三部分组成,用点(.
)分隔:
- Header(头部)
包含令牌类型(typ: "JWT"
)和签名算法(如alg: HS256
)。
示例:{"alg": "HS256", "typ": "JWT"}
→ Base64Url编码后形成第一部分。 - Payload(载荷)
存放声明(claims),包括预定义声明(如用户ID、过期时间)和自定义数据。
常见预定义声明:iss
(签发者)、exp
(过期时间)、sub
(主题)、aud
(受众)等。
示例:{"sub": "123", "name": "Alice", "exp": 1516239022}
→ Base64Url编码后形成第二部分。
- Signature(签名)
对前两部分的签名,防止数据篡改。算法由Header指定(如HMAC SHA256)。
生成方式:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
。
最终JWT形式:xxxxx.yyyyy.zzzzz
。
1.2 工作流程
- 用户登录:客户端发送凭证(如用户名/密码)到服务器。
- 生成JWT:服务器验证凭证,生成并返回JWT。
- 客户端存储:客户端保存JWT(通常存在
localStorage
或Cookie中)。 - 携带令牌请求:客户端在请求头中添加
Authorization: Bearer <JWT>
。 - 服务器验证:服务器检查签名有效性、过期时间等,验证通过后处理请求。
1.3 优点
- 无状态:无需服务器存储会话信息,适合分布式系统。
- 跨域支持:适用于API网关、单页应用(SPA)等场景。
- 灵活性:载荷可自定义扩展,传递非敏感用户信息。
1.4 缺点
- 不可废止:令牌到期前无法强制失效,需借助黑名单或短过期时间。
- 存储风险:客户端存储不当可能导致XSS攻击窃取令牌。
- 信息暴露:载荷仅Base64编码,需避免存放敏感数据。
1.5 安全实践
- 使用HTTPS:防止令牌在传输中被截获。
- 强签名算法:如HMAC SHA256或RSA,避免弱算法(如HS256密钥过短)。
- 合理设置过期时间:缩短令牌有效期,减少泄露风险。
- 敏感数据加密:必要时使用JWE(JSON Web Encryption)加密载荷。
1.6. 适用场景
- API认证:RESTful API的无状态身份验证。
- 单点登录(SSO):跨多个系统的用户身份共享。
- 移动端应用:减少频繁查询数据库的开销。
1.7 JWT与OAuth2
- JWT常用作OAuth2的Bearer Token,传递用户身份和权限。
- OAuth2定义授权流程,JWT是实现令牌的一种方式。
8. 示例代码(Node.js)
const jwt = require('jsonwebtoken');// 生成JWT
const token = jwt.sign({ userId: 123, role: 'admin' },'your-secret-key',{ expiresIn: '1h' }
);// 验证JWT
jwt.verify(token, 'your-secret-key', (err, decoded) => {if (err) throw err;console.log(decoded); // { userId: 123, role: 'admin', iat: ..., exp: ... }
});
通过理解JWT的结构、流程及安全实践,开发者可以有效利用其在现代Web应用中实现安全、高效的身份验证。
2 用户mock和api
用户mock,user.js代码如下所示:
const Mock = require('mockjs')
const Random = Mock.Randommodule.exports = [{// 获取用户url: '/api/user/info',method: 'get',response() {return {errno: 0,data: {username: Random.title(),nickname: Random.cname(),},}}},{// 注册新用户url: '/api/user/register',method: 'post',response() {return {errno: 0}}},{// 用户登录url: '/api/user/login',method: 'post',response() {return {errno: 0,data: {token: Random.word(20)},}}},
]
前端user.ts 用户api接口代码如下所示:
import request, { ResDataType } from "../services/request";/*** 获取用户信息* @returns 用户信息*/
export async function getUserInfoApi(): Promise<ResDataType> {const url = "/api/user/info";const data = (await request.get(url)) as ResDataType;return data;
}/*** 注册新用户* @returns 注册是否成功*/
export async function registerApi(username: string,password: string,nickname?: string
): Promise<ResDataType> {const url = "/api/user/register";const body = { username, password, nickname: nickname || username };const data = (await request.post(url, body)) as ResDataType;return data;
}/*** 用户登录* @returns token*/
export async function loginApi(username: string,password: string
): Promise<ResDataType> {const url = "/api/user/login";const data = (await request.post(url, { username, password })) as ResDataType;return data;
}
3 注册
Register.tsx代码如下所示:
import { FC } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Typography, Space, Form, Input, Button, message } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { useRequest } from "ahooks";import { LOGIN_PATHNAME } from "../router";
import { registerApi } from "@/api/user";import styles from "./Register.module.scss";const { Title } = Typography;const Register: FC = () => {const nav = useNavigate();const { run: handleRegister } = useRequest(async (values) => {const { username, password, nickname } = values;return await registerApi(username, password, nickname);},{manual: true,onSuccess() {message.success("注册成功");// 跳转登录页nav(LOGIN_PATHNAME);},});function onFinish(values: any) {handleRegister(values);}return (<div className={styles.container}><div><Space><Title level={2}><UserAddOutlined /></Title><Title level={2}>注册新用户</Title></Space></div><div><FormlabelCol={{ span: 6 }}wrapperCol={{ span: 16 }}onFinish={onFinish}><Form.Itemlabel="用户名"name="username"rules={[{ required: true, message: "请输入用户名" },{type: "string",min: 5,max: 20,message: "字符长度再5-20之间",},{pattern: /^\w+$/,message: "只能是字母数字下划线",},]}><Input /></Form.Item><Form.Itemlabel="密码"name="password"rules={[{ required: true, message: "请输入用户名" },{min: 8,message: "密码长度最少8位",},]}><Input.Password /></Form.Item><Form.Itemlabel="确认密码"name="confirm"dependencies={["password"]}rules={[{required: true,message: "请输入确认密码",},({ getFieldValue }) => ({validator(_, value) {if (!value || getFieldValue("password") === value) {return Promise.resolve();} else {return Promise.reject(new Error("两次密码不一致"));}},}),]}><Input.Password /></Form.Item><Form.Item label="昵称" name="nickname"><Input /></Form.Item><Form.Item wrapperCol={{ offset: 6, span: 16 }}><Space><Button type="primary" htmlType="submit">注册</Button><Link to={LOGIN_PATHNAME}>已有账户,登录</Link></Space></Form.Item></Form></div></div>);
};
export default Register;
执行注册,成功挑战登录页,如下图所示:
4 登录
登录页Login.tsx代码如下所示:
import { FC, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Typography, Space, Form, Input, Button, Checkbox, message } from "antd";
import { UserAddOutlined } from "@ant-design/icons";
import { useRequest } from "ahooks";import { MANAGE_INDEX_PATHNAME, REGISTER_PATHNAME } from "../router";
import { loginApi } from "@/api/user";import styles from "./Register.module.scss";const { Title } = Typography;const USERNAME_KEY = "username";
const PASSWORD_KEY = "password";/*** 浏览器本地存储用户信息* @param username 用户名* @param password 密码*/
function rememberUser(username: string, password: string) {localStorage.setItem(USERNAME_KEY, username);localStorage.setItem(PASSWORD_KEY, password);
}/*** 浏览器本地删除用户信息* @param username 用户名* @param password 密码*/
function deleteUserFromStorage(username: string, password: string) {localStorage.removeItem(USERNAME_KEY);localStorage.removeItem(PASSWORD_KEY);
}/*** 浏览器本地获取用户信息*/
function getUserInfoFromStorage() {return {username: localStorage.getItem(USERNAME_KEY),password: localStorage.getItem(PASSWORD_KEY),};
}const Login: FC = () => {const nav = useNavigate()// 表单组件初始化const [form] = Form.useForm();useEffect(() => {const { username, password } = getUserInfoFromStorage();form.setFieldsValue({ username, password });// eslint-disable-next-line react-hooks/exhaustive-deps}, []);const { run: handleLogin } = useRequest(async (values) => {const { username, password } = values;return await loginApi(username, password);},{manual: true,onSuccess(res) {message.success("登录成功")// todo 存储token// 跳转我的问卷nav(MANAGE_INDEX_PATHNAME)},});function onFinish(values: any) {const { username, password, remember } = values || {};if (remember) {rememberUser(username, password);} else {deleteUserFromStorage(username, password);}handleLogin({ username, password });}return (<div className={styles.container}><div><Space><Title level={2}><UserAddOutlined /></Title><Title level={2}>用户登录</Title></Space></div><div><FormlabelCol={{ span: 6 }}wrapperCol={{ span: 16 }}onFinish={onFinish}initialValues={{ remember: true }}form={form}><Form.Itemlabel="用户名"name="username"rules={[{ required: true, message: "请输入用户名" },{type: "string",min: 5,max: 20,message: "字符长度再5-20之间",},{pattern: /^\w+$/,message: "只能是字母数字下划线",},]}><Input /></Form.Item><Form.Itemlabel="密码"name="password"rules={[{ required: true, message: "请输入用户名" },{min: 8,message: "密码长度最少8位",},]}><Input.Password /></Form.Item><Form.ItemwrapperCol={{ offset: 6, span: 16 }}name="remember"valuePropName="checked"><Checkbox>记住我</Checkbox></Form.Item><Form.Item wrapperCol={{ offset: 6, span: 16 }}><Space><Button type="primary" htmlType="submit">登录</Button><Link to={REGISTER_PATHNAME}>注册新用户</Link></Space></Form.Item></Form></div></div>);
};
export default Login;
登录成功后跳转我的问卷也,如下图所示:
5 token存储
用户登录成功后,需要存储token,userToken.ts代码如下所示
/*** @description localStorage管理用户token* @author gaogzhen*/const KEY = "USER-TOKEN"/*** 设置token* @param token */
export function setToken(token:string) {localStorage.setItem(KEY, token)
}/*** 获取token* @returns token*/
export function getToken() {return localStorage.getItem(KEY) || ''
}/*** 删除token*/
export function removeToken() {localStorage.removeItem(KEY)
}
登录页登录成功后,执行存储token,Login.tsx代码如下:
const { run: handleLogin } = useRequest(async (values) => {const { username, password } = values;return await loginApi(username, password);},{manual: true,onSuccess(res) {message.success("登录成功");// 存储tokenconst { token = "" } = res;setToken(token);// 跳转我的问卷nav(MANAGE_INDEX_PATHNAME);},});
localStorage存储如下图哦所示:
6 请求拦截器设置token
登录成功后,用户每次请求需要携带token,用户身份验证、权限验证等。这里通过请求拦截器实现,request.ts代码如下所示:
import axios from "axios";
import { message } from "antd";
import { AUTHORIZATION } from "@/constant";
import { getToken } from "@/utils/userToken";const request = axios.create({timeout: 5000,
});// request拦截:每次请求携带token
request.interceptors.request.use((config) => {// todo token 校验config.headers[AUTHORIZATION] = `Bearer ${getToken()}`;return config;
});// response 拦截:统一处理errno和msg
request.interceptors.response.use((res) => {const resData = (res.data || {}) as ResType;const { errno, data, msg } = resData;if (errno !== 0) {// 错误提示if (msg) {message.error(msg);}throw new Error(msg);}return data as any;
});
export default request;export type ResDataType = {[key: string]: any;
};export type ResType = {errno: number;data?: ResDataType;msg?: string;
};
效果如下图所示:
6 获取用户信息
用户登录之后,用户信息很多地方需要使用,在学习状态管理之后再处理,这里我们暂时在用户信息组件处理。
用户信息UserInfo.tsx代码如下所示:
import { FC } from "react";
import { Link } from "react-router-dom";
import { LOGIN_PATHNAME } from "../router/index";
import { useRequest } from "ahooks";
import { getUserInfoApi } from "@/api/user";
import { UserOutlined } from "@ant-design/icons";
import { Button } from "antd";const UserInfo: FC = () => {const { data } = useRequest(getUserInfoApi);const { username, nickname } = data || {};const User = (<><span style={{ color: "#e8e8e8" }}><UserOutlined />{nickname}</span><Button type="link">退出</Button></>);const Login = <Link to={LOGIN_PATHNAME}>登录</Link>;return <>{username ? User : Login}</>;
};
export default UserInfo;
效果如下图所示:
7 退出登录
UserInfo.tsx退出功能代码如下所示:
import { FC } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useRequest } from "ahooks";
import { Button } from "antd";
import { UserOutlined } from "@ant-design/icons";import { LOGIN_PATHNAME } from "../router/index";
import { getUserInfoApi } from "@/api/user";
import { removeToken } from "@/utils/userToken";const UserInfo: FC = () => {const nav = useNavigate()const { data } = useRequest(getUserInfoApi);const { username, nickname } = data || {};function logout() {removeToken()// 跳转登录页nav(LOGIN_PATHNAME)}const User = (<><span style={{ color: "#e8e8e8" }}><UserOutlined />{nickname}</span><Button type="link" onClick={logout}>退出</Button></>);const Login = <Link to={LOGIN_PATHNAME}>登录</Link>;return <>{username ? User : Login}</>;
};
export default UserInfo;
注:
- 执行退出,但是右上角还是显示登录状态,后面处理
结语
❓QQ:806797785
⭐️仓库地址:https://gitee.com/gaogzhen
⭐️仓库地址:https://github.com/gaogzhen
[1]ahook官网[CP/OL].
[2]mock文档[CP/OL].
[3]Ant Design官网[CP/OL].