🧱 第一步:環境準備
? 1. 創建數據庫(MySQL)
-- 創建數據庫,使用 utf8mb4 字符集支持 emoji 和多語言
CREATE DATABASE security_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 使用該數據庫
USE security_demo;-- 用戶表結構
-- id: 主鍵自增
-- username: 唯一,不允許重復
-- password: 存儲 BCrypt 加密后的密碼(明文不可逆)
-- role: 存儲用戶角色,如 ROLE_ADMIN、ROLE_USER
CREATE TABLE user (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) UNIQUE NOT NULL,password VARCHAR(100) NOT NULL, -- BCrypt 加密后長度約 60role VARCHAR(50) NOT NULL
);-- 插入測試數據
-- 注意:密碼 '123456' 已通過 BCrypt 加密(強度為 10)
-- 生成工具:https://www.devglan.com/online-tools/bcrypt-hash-generator
INSERT INTO user (username, password, role) VALUES
('admin', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_ADMIN'),
('alice', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER'),
('bob', '$2a$10$RRLCewx/5eYR60ZJ6y6U7eM8V6a8y6U7eM8V6a8y6U7eM8V6a8y6U7', 'ROLE_USER');
🔐 安全提示:
- 不要將明文密碼存入數據庫。
BCrypt
?是 Spring Security 推薦的密碼加密算法,自帶鹽值(salt),防彩虹表攻擊。$2a$10$...
?中的?10
?是加密強度(log rounds),值越大越安全但越慢。
? 2. Redis
# 確保 Redis 正在運行
redis-server
💡 用途說明:
- 緩存用戶角色信息,避免每次請求都查詢數據庫。
- 提升系統性能,尤其在高并發場景下。
- 鍵名格式:
user_role:用戶名
📁 第二步:Spring Boot 項目結構
src/main/java/com/example/demo/
├── DemoApplication.java # 主啟動類
├── config/
│ ├── SecurityConfig.java # 安全核心配置
│ └── MyBatisConfig.java # MyBatis 配置(可選)
├── controller/
│ └── UserController.java # 用戶操作接口
├── entity/
│ └── User.java # 用戶實體類
├── mapper/
│ └── UserMapper.java # 數據訪問接口
├── service/
│ ├── CustomUserDetailsService.java # 自定義用戶認證邏輯
│ └── RedisService.java # Redis 操作封裝
└── util/└── PasswordUtil.java # 密碼工具類(未使用,建議補全)
📦?pom.xml
?依賴詳解
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.3</version> <!-- 使用最新穩定版 Spring Boot --><relativePath/> <!-- 查找父 POM 從本地開始 --></parent><groupId>com.example</groupId><artifactId>security-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>security-demo</name><properties><java.version>17</java.version> <!-- 推薦使用 LTS 版本 --></properties><dependencies><!-- Web 支持:Tomcat + Spring MVC --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 安全框架:Spring Security --><!-- 提供認證、授權、CSRF、Session 等功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- MyBatis 啟動器 --><!-- 簡化 MyBatis 配置,自動掃描 Mapper --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><!-- MySQL 驅動 --><!-- 運行時依賴,編譯時不需要 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- Redis 支持 --><!-- 用于緩存用戶權限信息 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok --><!-- 自動生成 getter/setter/toString 等方法 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><!-- 打包時排除 Lombok,避免運行時報錯 --><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
? 第三步:代碼實現
1.?DemoApplication.java
?- 主啟動類
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** Spring Boot 主啟動類* @SpringBootApplication 注解 = @Configuration + @EnableAutoConfiguration + @ComponentScan* 自動掃描 com.example.demo 包下所有組件*/
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
2.?User.java
?- 實體類
package com.example.demo.entity;import lombok.Data;/*** 用戶實體類* 對應數據庫 user 表* 使用 Lombok @Data 自動生成:* - getter/setter* - toString()* - equals()/hashCode()* - requiredArgsConstructor*/
@Data
public class User {private Long id;private String username;private String password;private String role; // 如 ROLE_ADMIN
}
3.?UserMapper.java
?- MyBatis Mapper
package com.example.demo.mapper;import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;/*** 數據訪問接口(DAO)* @Mapper 注解:讓 Spring 能掃描到該接口并創建代理對象* SQL 注解方式:@Select 直接寫 SQL,適合簡單查詢*/
@Mapper
public interface UserMapper {/*** 根據用戶名查詢用戶信息* #{username} 是預編譯參數,防止 SQL 注入* @param username 用戶名* @return 用戶對象,不存在返回 null*/@Select("SELECT * FROM user WHERE username = #{username}")User findByUsername(String username);
}
4.?RedisService.java
?- Redis 工具類
package com.example.demo.service;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;/*** Redis 操作服務類* 封裝常用操作,便于業務調用* 使用 StringRedisTemplate(只處理字符串),適合緩存簡單鍵值對*/
@Service
public class RedisService {private final StringRedisTemplate redisTemplate;/*** 構造器注入 RedisTemplate* Spring 自動注入 RedisConnectionFactory 創建的模板*/public RedisService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/*** 設置字符串值,并設置過期時間(分鐘)* @param key 鍵* @param value 值* @param timeout 過期時間(分鐘)*/public void set(String key, String value, long timeout) {redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.MINUTES);}/*** 獲取字符串值* @param key 鍵* @return 值,不存在返回 null*/public String get(String key) {return redisTemplate.opsForValue().get(key);}/*** 刪除指定鍵* @param key 鍵*/public void delete(String key) {redisTemplate.delete(key);}
}
5.?CustomUserDetailsService.java
?- 自定義用戶詳情服務
package com.example.demo.service;import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.Collection;
import java.util.Collections;/*** 自定義用戶詳情服務* Spring Security 通過此服務加載用戶信息用于認證* 實現 UserDetailsService 接口是必須的*/
@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RedisService redisService;/*** 根據用戶名加載用戶詳情* 調用時機:用戶登錄時(/login)* 流程:* 1. 先查 Redis 緩存* 2. 緩存命中 → 返回* 3. 未命中 → 查數據庫 → 寫入緩存* @param username 用戶名* @return UserDetails(Spring Security 用戶模型)* @throws UsernameNotFoundException 用戶不存在時拋出*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. 嘗試從 Redis 獲取角色String cachedRole = redisService.get("user_role:" + username);if (cachedRole != null) {System.out.println("? Redis 緩存命中: " + username);return buildUserDetails(username, "******", cachedRole);}// 2. 緩存未命中,查詢數據庫User user = userMapper.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("? 用戶不存在: " + username);}// 3. 將角色寫入 Redis,有效期 30 分鐘redisService.set("user_role:" + username, user.getRole(), 30);System.out.println("🔥 數據庫查詢并緩存: " + username);return buildUserDetails(user.getUsername(), user.getPassword(), user.getRole());}/*** 構建 Spring Security 的 UserDetails 對象* @param username 用戶名* @param password 加密后的密碼* @param role 角色(如 ROLE_ADMIN)* @return UserDetails 實例*/private UserDetails buildUserDetails(String username, String password, String role) {// 將角色封裝為 GrantedAuthority(權限對象)Collection<? extends GrantedAuthority> authorities =Collections.singletonList(new SimpleGrantedAuthority(role));// 創建 Spring Security 內置用戶對象// 參數:用戶名、密碼、權限集合return new org.springframework.security.core.userdetails.User(username,password,authorities);}
}
6.?SecurityConfig.java
?- 安全配置
package com.example.demo.config;import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;/*** Spring Security 配置類* 控制認證、授權、密碼編碼、會話等行為*/
@Configuration
@EnableWebSecurity // 啟用 Web 安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 啟用方法級安全(支持 @PreAuthorize)
public class SecurityConfig {@Autowiredprivate CustomUserDetailsService userDetailsService;/*** 密碼編碼器 Bean* 用于比對用戶輸入密碼與數據庫加密密碼* @return BCryptPasswordEncoder 實例*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 安全過濾鏈配置* 定義哪些請求需要認證、使用何種認證方式等*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable() // 禁用 CSRF(適合無狀態 API).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 無狀態會話.and().authorizeHttpRequests(authz -> authz.requestMatchers("/login").permitAll() // 登錄接口放行.anyRequest().authenticated() // 其他請求需認證).httpBasic(); // 使用 HTTP Basic 認證(測試用)return http.build();}/*** Redis Template Bean* 用于操作 Redis* Spring Boot 自動配置 RedisConnectionFactory*/@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {return new StringRedisTemplate(factory);}
}
7.?UserController.java
?- 控制器
package com.example.demo.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;/*** 用戶操作控制器* 演示 @PreAuthorize 方法級權限控制*/
@RestController
@RequestMapping("/api/users")
public class UserController {/*** 刪除用戶接口* @PreAuthorize("hasRole('ROLE_ADMIN')")* 只有擁有 ROLE_ADMIN 角色的用戶才能調用* 注意:hasRole() 會自動添加 ROLE_ 前綴,所以寫 'ADMIN' 也可以*/@PreAuthorize("hasRole('ROLE_ADMIN')")@DeleteMapping("/{username}")public String deleteUser(@PathVariable String username) {return "🗑? 用戶 " + username + " 已刪除";}/*** 查看用戶信息* @PreAuthorize("authentication.principal.username == #username")* 表達式含義:* 當前登錄用戶名(authentication.principal.username)* 必須等于路徑參數 #username* 實現“只能查看自己信息”的業務邏輯*/@PreAuthorize("authentication.principal.username == #username")@GetMapping("/{username}")public String getUserInfo(@PathVariable String username) {return "👤 用戶信息: " + username;}
}
8.?application.yml
?- 配置文件
server:port: 8080 # 服務端口spring:datasource:url: jdbc:mysql://localhost:3306/security_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: yourpassword # 請替換為真實密碼driver-class-name: com.mysql.cj.jdbc.Driverredis:host: localhostport: 6379 # Redis 服務地址mybatis:type-aliases-package: com.example.demo.entity # 別名包,SQL 中可用類名代替全路徑configuration:map-underscore-to-camel-case: true # 數據庫下劃線字段自動映射到 Java 駝峰屬性logging:level:com.example.demo.mapper: debug # 顯示 MyBatis 執行的 SQL
?? 第四步:運行與測試
1. 啟動項目
服務啟動后,訪問
http://localhost:8080
會跳轉登錄頁(Basic Auth)。
2. 測試命令
? 測試1:管理員查看自己
curl -u admin:123456 http://localhost:8080/api/users/admin
# 響應:👤 用戶信息: admin
# 說明:用戶名匹配,授權通過
? 測試2:管理員刪除用戶
curl -X DELETE -u admin:123456 http://localhost:8080/api/users/alice
# 響應:🗑? 用戶 alice 已刪除
# 說明:admin 擁有 ROLE_ADMIN,權限通過
? 測試3:普通用戶刪別人
curl -X DELETE -u alice:123456 http://localhost:8080/api/users/bob
# 響應:403 Forbidden
# 說明:alice 是 ROLE_USER,不滿足 hasRole('ROLE_ADMIN')
? 查看 Redis 緩存
redis-cli
> KEYS user_role:*
# 輸出:
# "user_role:admin"
# "user_role:alice"
# "user_role:bob"
> GET user_role:admin
# "ROLE_ADMIN"
🏁 總結:核心知識點
技術 | 作用 |
---|---|
@PreAuthorize | 方法級權限控制,支持 SpEL 表達式 |
hasRole() | 檢查角色(自動加?ROLE_ ?前綴) |
authentication.principal.username | 獲取當前登錄用戶名 |
#param | 引用方法參數 |
Redis 緩存 | 提升性能,避免重復查庫 |
BCrypt | 安全存儲密碼 |
HTTP Basic | 簡單認證方式(適合測試) |
KEY: user_role:admin
VAL: ROLE_ADMINKEY: user_role:alice
VAL: ROLE_USER