基于JWT的RESTful登录系统实现

要通过JWT简单的令牌验证和使用JSON 格式 REST风格的API进行实现登录功能先得认识JWT的RESTful。


1.JSON Web Token (JWT)

1.JWT是什么?


JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。

JWT的基本概念

JWT是一个紧凑的、自包含的令牌,可以在不同服务之间安全传递信息。它由三部分组成,用点号(.)分隔:

复制代码
Header.Payload.Signature

JWT的三部分结构

1. Header(头部)

包含令牌的元信息,通常指定算法和令牌类型:

java 复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}
2. Payload(载荷)

包含要传递的数据,称为Claims(声明):

java 复制代码
{
  "sub": "xingchen",
  "iat": 1640995200,
  "exp": 1641081600,
  "id": 12345,
  "name": "张三"
}
3. Signature(签名)

用于验证令牌的完整性和真实性:

java 复制代码
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  APP_SECRET
)

JWT的工作原理

1. 生成过程

用户登录 → 服务器验证 → 生成JWT → 返回给客户端

2. 验证过程

客户端请求 → 携带JWT → 服务器验证签名 → 提取用户信息 → 执行业务逻辑

具体生成的JWT编码就如下图左边所示的代码,右边则是它的三部分结构头部,和信息


2.为什么要使用JWT?

JWT的优势

1. 无状态性
  • 服务器不需要存储会话信息
  • 所有必要信息都在令牌中
  • 便于分布式系统扩展
2. 自包含
  • 令牌包含用户身份和权限信息
  • 减少数据库查询
  • 提高响应速度
3. 安全性
  • 数字签名防止篡改
  • 可以设置过期时间
  • 支持加密算法
JWT vs 传统Session
特性 JWT Session
存储位置 客户端 服务器
状态管理 无状态 有状态
扩展性 容易 困难
跨域支持 优秀 有限
安全性 依赖签名 依赖服务器

JWT的使用场景

1. 身份认证

用户登录后获得JWT,后续请求携带JWT进行身份验证。

2. 信息交换

在不同服务之间安全传递用户信息。

3. 授权

JWT中包含用户权限信息,用于访问控制。


总的来说,JWT是现代Web应用中非常重要的认证和授权技术,它简化了分布式系统中的身份管理,提高了系统的可扩展性和安全性。


2.RESTful


1.RESTful是什么?

RESTful是一种软件架构风格和设计原则,用于设计网络应用程序,特别是Web API。

RESTful的基本概念

REST(Representational State Transfer,表述性状态转移)是由Roy Fielding在2000年提出的架构风格。RESTful是指遵循REST原则的API设计。

RESTful的核心原则

1. 客户端-服务器架构
  • 客户端负责用户界面和用户体验
  • 服务器负责数据存储、处理和业务逻辑
  • 双方通过标准化的接口通信
2. 无状态性
  • 每个请求都包含处理该请求所需的全部信息
  • 服务器不保存客户端的会话状态
3. 可缓存性
  • 响应应该明确标识是否可缓存
  • 提高系统性能和可扩展性
4. 统一接口
  • 使用标准的HTTP方法
  • 资源通过URI标识
  • 使用标准的状态码

2.RESTful的优点是什么

1. 可扩展性

  • 无状态设计便于水平扩展
  • 可以轻松添加新的服务器节点
  • 负载均衡更加简单

2. 灵活性和可维护性

  • 前后端分离,独立开发
  • 客户端和服务器可以独立演进
  • 支持多种客户端类型(Web、移动端、桌面应用)

3. 标准化

  • 使用成熟的HTTP协议
  • 统一的接口设计
  • 开发者学习成本低

4. 性能优化

  • 支持缓存机制
  • 可以使用CDN
  • 减少不必要的数据传输

RESTful vs 其他API风格

特性 RESTful GraphQL gRPC
协议 HTTP HTTP HTTP/2
数据格式 JSON JSON Protobuf
缓存 支持 有限 不支持
实时通信 不支持 支持 支持
学习曲线

总的来说

RESTful是一种成熟、实用的API设计风格,它:

  1. 简化了系统架构 - 通过无状态设计提高了可扩展性
  2. 提高了开发效率 - 标准化的接口减少了沟通成本
  3. 增强了系统灵活性 - 支持多种客户端和独立部署
  4. 优化了性能 - 支持缓存和CDN等优化手段

3.登录功能实现

1.后端代码

1.创建模块

2.引入相关依赖

java 复制代码
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.12</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.20</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--jwt的依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- jaxb依赖包 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>

3.往resourcess中添加Spring Boot应用的配置文件

java 复制代码
spring:
  datasource:
    username: "你的MYSQL数据库账号"
    password: "你的MYSQL数据库密码"
    url: jdbc:mysql:"你的MYSQL数据库URL"
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  data:
    redis:
      host: "redis服务器IP地址"
      port: 6379  # Redis服务端口
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.1MySQL数据库创建代码
sql 复制代码
/*
 Navicat Premium Dump SQL

 Source Server         : MySQL
 Source Server Type    : MySQL
 Source Server Version : 80021 (8.0.21)
 Source Host           : localhost:3306
 Source Schema         : house_rental

 Target Server Type    : MySQL
 Target Server Version : 80021 (8.0.21)
 File Encoding         : 65001

 Date: 29/10/2025 22:56:09
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `role` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', 'admin123', 'ADMIN', '13800138000', 'admin@example.com', '2025-06-18 09:40:08', '2025-06-18 09:40:08');
INSERT INTO `user` VALUES (2, '111', '111', 'USER', '11111', '11111', '2025-06-18 10:11:46', '2025-06-18 10:11:46');

SET FOREIGN_KEY_CHECKS = 1;

4.改写启动类

5.填写bean包,mapper,service,controller等基本包

UserServiceImpl

java 复制代码
package com.jiangzhong.mingxing.boot.boot07.service.impl;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jiangzhong.mingxing.boot.boot07.bean.User;
import com.jiangzhong.mingxing.boot.boot07.config.JwtConfig;
import com.jiangzhong.mingxing.boot.boot07.mapper.UserMapper;
import com.jiangzhong.mingxing.boot.boot07.service.UserService;
import com.jiangzhong.mingxing.boot.boot07.util.JsonResult;
import com.jiangzhong.mingxing.boot.boot07.util.ResultTool;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import org.mockito.internal.matchers.Null;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public JsonResult login(User user) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",user.getUsername());
        queryWrapper.eq("password",user.getPassword());
        User one = getOne(queryWrapper);
        if (one == null) {
            return ResultTool.fail("账号或者密码错误", 400);
        }
        // 2.2 正确
        // 3. 生成token令牌
        String token = JwtConfig.getJwtToken(one);
        // 4. 保存token令牌
        stringRedisTemplate.opsForValue().set("token:" + one.getId(), token, 1, TimeUnit.DAYS);
        // 5. 返回token令牌
        return ResultTool.success(token);
    }

    @Override
    public JsonResult isLogin(String token) {
        // 1. 检查token是否篡改过
        boolean b = JwtConfig.checkToken(token);
        if (!b) {
            return ResultTool.fail("用户没有登陆", 401);
        }
        // 2. 获取到token中保存的id信息
        Claims claims = JwtConfig.parseJWT(token);
        Object id = claims.get("id");
        // 3. 获取到redis中存储的token
        String redisToken = stringRedisTemplate.opsForValue().get("token:" + id);
        // 4. 检查token是否一致
        return Objects.equals(token, redisToken) ? ResultTool.success("success") : ResultTool.fail("用户没有登陆", 401);
    }
}

6.加入JWT

java 复制代码
import com.jiangzhong.mingxing.boot.boot07.bean.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import java.util.Date;

public class JwtConfig {
    public static final long EXPIRE = 1000*60*60*24;
    public static final String APP_SECRET = "1234";
    //	@param id 当前用户ID
    //	@param issuer 该JWT的签发者,是否使用是可选的
    //	@param subject 该JWT所面向的用户,是否使用是可选的
    //	@param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
    //	@param audience 接收该JWT的一方,是否使用是可选的
    //生成token字符串的方法
    public static String getJwtToken(User user) {

        //头部信息
        //头部信息
        //下面这部分是payload部分
        // 设置默认标签
        //设置jwt所面向的用户
        //设置签证生效的时间
        //设置签证失效的时间
        //自定义的信息,这里存储id和姓名信息
        //设置token主体部分 ,存储用户信息
        //下面是第三部分
        // 生成的字符串就是jwt信息,这个通常要返回出去
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")    //头部信息
                .setHeaderParam("alg", "HS256")    //头部信息
                //下面这部分是payload部分
                // 设置默认标签
                .setSubject("xingchen")    //设置jwt所面向的用户
                .setIssuedAt(new Date())    //设置签证生效的时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))    //设置签证失效的时间
                //自定义的信息,这里存储id和姓名信息
                .claim("id", user.getId())  //设置token主体部分 ,存储用户信息
                .claim("name", user.getUsername())
                //下面是第三部分
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 解析JWT
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
        return claims;
    }


}

7.加入util

java 复制代码
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@NoArgsConstructor
@Data
public class JsonResult<T> implements Serializable {

    private T data;
    private Boolean success;
    private String message;
    private Integer code;

    public JsonResult(T data) {
        this.data = data;
        this.success = true;
        this.code = 200;
    }

    public JsonResult(String message, Integer code) {
        this.message = message;
        this.code = code;
        this.success = false;
    }
}
java 复制代码
public class ResultTool {

    public static JsonResult success(Object data) {
        return new JsonResult(data);
    }

    public static JsonResult fail(String error, int code) {
        return new JsonResult(error, code);
    }
}

2.简单的前端代码实现

1.首页 index

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    我是首页
</div>
</body>
</html>

<script src="../js/vue.min.js"></script>
<script src="../js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {}
        },
        created() {
            // 1.判断是否有token
            let token = localStorage.getItem('token')
            if (!token) {
                // 1.1 如果没有token,则跳转到登录页
                location.href = 'login.html'
                return
            }
            // 1.2 如果有token,则继续请求
            axios({
                method: 'get',
                url: `https://localhost:8080/auth/is_login`,
                // 将请求放到请求头中进行传递
                headers: {
                    token: token
                }
            }).then(resp => {
                if (!resp.data.success) {
                    location.href = 'login.html'
                    return
                }
            })
        }
    })
</script>

2.登录页面 login

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户登录</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <style>
        .gradient-bg {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }

        .input-focus:focus {
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3);
        }

        .shake {
            animation: shake 0.5s;
        }

        @keyframes shake {
            0%, 100% {
                transform: translateX(0);
            }
            20%, 60% {
                transform: translateX(-5px);
            }
            40%, 80% {
                transform: translateX(5px);
            }
        }
    </style>
</head>
<body class="min-h-screen flex items-center justify-center gradient-bg">
<div class="w-full max-w-md px-8 py-12 bg-white rounded-2xl shadow-xl" id="app">
    <div class="text-center mb-10">
        <i class="fas fa-user-circle text-6xl text-indigo-500 mb-4"></i>
        <h1 class="text-3xl font-bold text-gray-800">欢迎回来</h1>
        <p class="text-gray-500 mt-2">{{error}}</p>
    </div>

    <div>
        <label for="email" class="block text-sm font-medium text-gray-700 mb-1">账号</label>
        <div class="relative">
            <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                <i class="fas fa-envelope text-gray-400"></i>
            </div>
            <input id="email" type="text" required v-model="username"
                   class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg input-focus transition duration-200"
                   placeholder="example@domain.com">
        </div>
        <p id="emailError" class="mt-1 text-sm text-red-600 hidden">请输入有效的邮箱地址</p>
    </div>

    <div>
        <label for="password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
        <div class="relative">
            <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                <i class="fas fa-lock text-gray-400"></i>
            </div>
            <input id="password" name="password" type="password" required minlength="6"
                   v-model="password"
                   class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg input-focus transition duration-200"
                   placeholder="至少6位字符">
            <button type="button" id="togglePassword" class="absolute inset-y-0 right-0 pr-3 flex items-center">
                <i class="fas fa-eye text-gray-400 hover:text-indigo-500"></i>
            </button>
        </div>
        <p id="passwordError" class="mt-1 text-sm text-red-600 hidden">密码长度至少6位</p>
    </div>

    <div class="flex items-center justify-between">
        <div class="flex items-center">
            <input id="remember" name="remember" type="checkbox"
                   class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
            <label for="remember" class="ml-2 block text-sm text-gray-700">记住我</label>
        </div>
        <a href="#" class="text-sm text-indigo-600 hover:text-indigo-500">忘记密码?</a>
    </div>

    <button type="submit" @click="login"
            class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-200">
        登录
    </button>

    <div class="mt-6 text-center">
        <p class="text-sm text-gray-600">
            还没有账号?
            <a href="#" class="font-medium text-indigo-600 hover:text-indigo-500">立即注册</a>
        </p>
    </div>
</div>

</body>
</html>
<script src="../js/vue.min.js"></script>
<script src="../js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                username: "",
                password: "",
                error: ''
            }
        },
        methods: {
            login() {
                let data = new URLSearchParams()
                data.append("username", this.username)
                data.append("password", this.password)
                axios({
                    method: 'post',
                    url: 'http://localhost:8080/auth/login',
                    data: data
                }).then(resp => {
                    if (resp.data.success) {
                        // 1.保存token
                        localStorage.setItem('token', resp.data.data)
                        // 2.跳转到首页中
                        location.href = 'index.html'
                    } else {
                        // 1.提示登录失败
                        this.error = '账号或者密码错误'
                    }
                })
            }
        }
    })
</script>

登录页面,登陆成功后跳转首页

相关推荐
陶甜也4 分钟前
使用Blender进行现代建筑3D建模:前端开发者的跨界探索
前端·3d·blender
五阿哥永琪13 分钟前
Spring Boot 中自定义线程池的正确使用姿势:定义、注入与最佳实践
spring boot·后端·python
我命由我1234541 分钟前
VSCode - Prettier 配置格式化的单行长度
开发语言·前端·ide·vscode·前端框架·编辑器·学习方法
HashTang42 分钟前
【AI 编程实战】第 4 篇:一次完美 vs 五轮对话 - UnoCSS 配置的正确姿势
前端·uni-app·ai编程
Victor3561 小时前
Netty(16)Netty的零拷贝机制是什么?它如何提高性能?
后端
JIngJaneIL1 小时前
基于java + vue校园快递物流管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js
Victor3561 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
后端
廋到被风吹走1 小时前
【数据库】【MySQL】分库分表策略 分类、优势与短板
数据库·mysql·分类
asdfg12589631 小时前
JS中的闭包应用
开发语言·前端·javascript