要通過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設計風格,它:
- 簡化了系統架構 - 通過無狀態設計提高了可擴展性
- 提高了開發效率 - 標準化的接口減少了溝通成本
- 增強了系統靈活性 - 支持多種客户端和獨立部署
- 優化了性能 - 支持緩存和CDN等優化手段
3.登錄功能實現
1.後端代碼
1.創建模塊
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應用的配置文件
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.改寫啓動類
5.填寫bean包,mapper,service,controller等基本包
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);
}
}
6.加入JWT
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
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>
登錄頁面,登陸成功後跳轉首頁