0805登录_注册_token_用户信息_退出-网络ajax请求2-react-仿低代码平台项目

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 工作流程

  1. 用户登录:客户端发送凭证(如用户名/密码)到服务器。
  2. 生成JWT:服务器验证凭证,生成并返回JWT。
  3. 客户端存储:客户端保存JWT(通常存在localStorage或Cookie中)。
  4. 携带令牌请求:客户端在请求头中添加Authorization: Bearer <JWT>
  5. 服务器验证:服务器检查签名有效性、过期时间等,验证通过后处理请求。

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].

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值