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.Random
- module.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>
- <Form
- labelCol={{ span: 6 }}
- wrapperCol={{ span: 16 }}
- onFinish={onFinish}
- >
- <Form.Item
- label="用户名"
- name="username"
- rules={[
- { required: true, message: "请输入用户名" },
- {
- type: "string",
- min: 5,
- max: 20,
- message: "字符长度再5-20之间",
- },
- {
- pattern: /^\w+$/,
- message: "只能是字母数字下划线",
- },
- ]}
- >
- <Input />
- </Form.Item>
- <Form.Item
- label="密码"
- name="password"
- rules={[
- { required: true, message: "请输入用户名" },
- {
- min: 8,
- message: "密码长度最少8位",
- },
- ]}
- >
- <Input.Password />
- </Form.Item>
- <Form.Item
- label="确认密码"
- 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>
- <Form
- labelCol={{ span: 6 }}
- wrapperCol={{ span: 16 }}
- onFinish={onFinish}
- initialValues={{ remember: true }}
- form={form}
- >
- <Form.Item
- label="用户名"
- name="username"
- rules={[
- { required: true, message: "请输入用户名" },
- {
- type: "string",
- min: 5,
- max: 20,
- message: "字符长度再5-20之间",
- },
- {
- pattern: /^\w+$/,
- message: "只能是字母数字下划线",
- },
- ]}
- >
- <Input />
- </Form.Item>
- <Form.Item
- label="密码"
- name="password"
- rules={[
- { required: true, message: "请输入用户名" },
- {
- min: 8,
- message: "密码长度最少8位",
- },
- ]}
- >
- <Input.Password />
- </Form.Item>
- <Form.Item
- wrapperCol={{ 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("登录成功");
- // 存储token
- const { 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].
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|