文章目錄
- 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].