要通過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(頭部)

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

{
  "alg": "HS256",
  "typ": "JWT"
}
2. Payload(載荷)

包含要傳遞的數據,稱為Claims(聲明):

{
  "sub": "xingchen",
  "iat": 1640995200,
  "exp": 1641081600,
  "id": 12345,
  "name": "張三"
}
3. Signature(簽名)

用於驗證令牌的完整性和真實性:

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.創建模塊

Rest安全接口的實現(Jwt)_#restful

2.引入相關依賴

<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應用的配置文件

Rest安全接口的實現(Jwt)_#後端_02

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數據庫創建代碼
/*
 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.改寫啓動類

Rest安全接口的實現(Jwt)_#JWT_03

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

Rest安全接口的實現(Jwt)_#mysql_04

Rest安全接口的實現(Jwt)_#JWT_05

Rest安全接口的實現(Jwt)_#spring boot_06

UserServiceImpl

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);
    }
}

Rest安全接口的實現(Jwt)_#後端_07

6.加入JWT

Rest安全接口的實現(Jwt)_#後端_08

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

Rest安全接口的實現(Jwt)_#mysql_09

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;
    }
}
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

<!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

<!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>

登錄頁面,登陸成功後跳轉首頁

Rest安全接口的實現(Jwt)_#spring boot_10

Rest安全接口的實現(Jwt)_#spring boot_11