Spring?Security的應用案例講解
Spring Security原理
Spring Security是一個強大的身份驗證和訪問控制框架,它提供了一套全面的安全解決方案,包括身份驗證、授權、防止攻擊等功能,用于保護Spring應用程序。它的設計理念是基于過濾器鏈(Filter Chain)和委托模式,通過一系列的過濾器來處理不同的安全功能。
1. 過濾器鏈(Filter Chain): Spring Security通過過濾器鏈的方式來處理安全性。每個過濾器負責一個特定的安全功能,例如身份驗證、授權、會話管理等。過濾器鏈是有序的,請求會依次通過這些過濾器,每個過濾器都有機會對請求進行處理。
2. 委托模式: Spring Security使用委托模式將不同的安全功能委托給不同的組件。例如,身份驗證(Authentication)的實現(xiàn)被委托給AuthenticationManager,而授權(Authorization)的實現(xiàn)則被委托給AccessDecisionManager。
3. 安全上下文(SecurityContext): 安全上下文是一個存儲當前用戶的地方,可以通過SecurityContextHolder來訪問。它包含了當前用戶的身份驗證信息(Authentication)以及其他與安全相關的信息。
一、核心能力
1.1身份認證 (Authentication) - “你是誰?”
- 多種認證方式:支持幾乎所有主流認證方案,如表單登錄(Username/Password)、HTTP Basic、HTTP Digest、OAuth 2.0、OIDC (OpenID Connect)、SAML 2.0、LDAP、JAAS、Pre-Authentication(如CAS)等。
- 表單登錄:最常用的方式,提供默認的登錄頁。
- HTTP Basic 認證:常用于 REST API。
- OAuth 2.0 / OpenID Connect:支持第三方登錄(如使用 Google, GitHub, Facebook 登錄)。
- LDAP:支持企業(yè)級目錄服務認證。
- JAAS:Java 認證和授權服務。
- 自定義認證:你可以集成任何你想用的認證方式。
- 靈活的密碼編碼:內置支持多種密碼加密器(如BCrypt、SCrypt、Pbkdf2、Argon2),并強烈推薦使用BCrypt,防止密碼明文存儲。
- “記住我”功能:通過持久化或基于令牌的機制實現(xiàn)長期登錄(通過 cookie 實現(xiàn)長期會話)。
- 多因素認證 (MFA):可以集成TOTP(如Google Authenticator)等二次驗證手段。
- 與現(xiàn)有系統(tǒng)集成:可以輕松地與已有的數(shù)據(jù)庫表結構、用戶服務進行對接。
1.2授權 (Authorization) - “你能做什么?”
- 請求級別授權:基于URL模式,控制用戶對某個API或頁面的訪問權限(例如
/admin/**需要ROLE_ADMIN角色)。 - 方法級別授權:通過注解(如
@PreAuthorize,@PostAuthorize,@Secured)在Service層或Controller層的方法上進行精細化的權限控制。 - 訪問控制列表 (ACL):支持對領域對象(Domain Object) 進行非常細粒度的權限控制(例如,用戶A可以“讀”文檔1,但不能“刪除”它)。這是一個相對復雜的功能,適用于特定場景。
- 動態(tài)權限:權限規(guī)則可以從數(shù)據(jù)庫或其他動態(tài)源加載,實現(xiàn)高度靈活的權限管理。
1.3防護常見攻擊
- CSRF (跨站請求偽造):默認開啟防護,尤其對非冪等的POST、PUT等請求進行令牌驗證。
- Session Fixation (會話固定):默認防護,認證成功后會自動創(chuàng)建新的Session。防止 Session 固定攻擊、控制并發(fā)會話數(shù)(單一用戶最多同時在線數(shù))、Session 超時處理等。
- 點擊劫持:可以通過設置HTTP頭
X-Frame-Options來防護。 - CORS (跨域資源共享):提供便捷的配置方式。
- 安全頭:自動設置一系列安全相關的HTTP頭,如
Content-Security-Policy,X-Content-Type-Options,X-Frame-Options,Strict-Transport-Security等來增強瀏覽器端的安全性。
1.4與其他技術無縫集成
- Spring生態(tài)系統(tǒng):與Spring Boot、Spring MVC、Spring WebFlux、Spring Data 深度整合,開箱即用,配置簡便。Spring Boot 更是通過自動配置讓集成變得極其簡單。
- Servlet API:基于Servlet Filter實現(xiàn),適用于任何Servlet容器(Tomcat, Jetty等)。
- 微服務架構:是構建微服務安全(如資源服務器、OAuth2客戶端)的事實標準。
1.5 能力邊界
- ? 在其邊界內(做得好):
- 應用級別的身份認證和授權。
- 會話管理。
- 防護基于Web的常見攻擊(CSRF, XSS的頭防護等)。
- ? 超出其邊界(不擅長或不做):
- 網(wǎng)絡層安全:如防火墻規(guī)則、VPN、DDoS防護、SSL/TLS終止(通常由網(wǎng)關/負載均衡器負責)。
- 操作系統(tǒng)/容器安全:如Linux內核安全加固、Docker鏡像漏洞掃描。
- 數(shù)據(jù)安全:如數(shù)據(jù)庫加密、數(shù)據(jù)傳輸過程中的加密(應由TLS負責)。
- 業(yè)務邏輯漏洞:無法自動防止業(yè)務層面的漏洞(例如,水平越權:用戶A通過修改ID訪問了用戶B的數(shù)據(jù),需要在授權邏輯中手動編寫檢查代碼)。
- 安全審計與日志:雖然可以與審計集成,但專業(yè)的日志分析和審計通常由ELK、Splunk等專用系統(tǒng)完成。
- WAF (Web應用防火墻) 功能:雖然能防護一些攻擊,但無法替代專業(yè)的WAF來防護復雜的SQL注入、XSS等攻擊(WAF基于規(guī)則和模式匹配,在更底層工作)。
二、核心架構與原理
Spring Security 的核心設計理念非常清晰:在 Servlet 過濾器(Filter)層面,為每一個進入應用的 HTTP 請求提供一系列的身份認證(Authentication)和授權(Authorization)檢查。
它本質上是一個過濾器鏈,請求必須逐一通過這條鏈上的每個過濾器,才能最終訪問到你的 Controller 中的資源。如果任何一個過濾器檢查失敗,請求就會被重定向、拋出異?;蛑苯臃祷劐e誤信息。
2.1 HTTP完整的請求過程
- 請求到達: HTTP 請求進入應用。
- 遍歷過濾器鏈: 請求依次經(jīng)過 Spring Security 的各個過濾器。
- 建立安全上下文:
SecurityContextPersistenceFilter從 Session 中恢復用戶的SecurityContext(如果已登錄)或創(chuàng)建一個空的。 - 處理登錄/認證:
- 如果是登錄請求(如
/loginPOST),UsernamePasswordAuthenticationFilter會攔截它,提取用戶名密碼,發(fā)起認證流程。 - 認證成功,一個包含用戶信息和權限的、已認證的
Authentication對象會被放入SecurityContext,并通常保存到 Session 中。
- 如果是登錄請求(如
- 處理匿名用戶: 如果用戶未認證,
AnonymousAuthenticationFilter會放入一個匿名 Token。 - 異常轉換:
ExceptionTranslationFilter準備捕獲后續(xù)的異常。 - 授權決策: 請求到達最終的
FilterSecurityInterceptor。- 它提取當前請求對應的權限規(guī)則 (
ConfigAttribute)。 - 它從
SecurityContextHolder中獲取已認證的Authentication對象。 - 它調用
AccessDecisionManager進行投票決策。
- 它提取當前請求對應的權限規(guī)則 (
- 決策結果:
- 允許訪問: 調用
FilterChain.doFilter(),請求最終到達你的 Controller,返回響應。 - 拒絕訪問: 拋出
AccessDeniedException。 - 異常處理:
ExceptionTranslationFilter捕獲到異常:- 如果是
AuthenticationException(認證失敗,用戶未知),啟動認證流程:清除SecurityContext,調用AuthenticationEntryPoint(例如:重定向到登錄頁或返回 WWW-Authenticate 頭)。 - 如果是
AccessDeniedException(授權失敗,權限不足),拒絕訪問:調用AccessDeniedHandler(例如:返回 403 錯誤頁面)。
- 如果是
- 清理上下文: 請求處理完畢,
SecurityContextPersistenceFilter將SecurityContext保存回 Session(如果需要),并清空ThreadLocal。
2.2 核心組成
2.2.1 過濾器鏈 (Filter Chain) - 心臟
這是 Spring Security 最核心的概念。整個安全機制都構建在 Servlet 規(guī)范定義的 Filter 之上。當一個 HTTP 請求到來時,它會經(jīng)過一個由多個安全過濾器組成的鏈條。
核心過濾器(按典型順序):
ChannelProcessingFilter: 決定是否需要重定向到 HTTPS 或 HTTP。SecurityContextPersistenceFilter: 至關重要。在請求開始時,從配置的SecurityContextRepository(默認是HttpSessionSecurityContextRepository)中讀取SecurityContext(安全上下文,包含用戶認證信息),并將其設置到SecurityContextHolder中;在請求結束后,清空SecurityContextHolder,并可能將SecurityContext保存回會話。CorsFilter: 處理跨域請求 (CORS)。CsrfFilter: 提供跨站請求偽造 (CSRF) 保護。LogoutFilter: 匹配退出登錄的 URL(如/logout),處理用戶退出邏輯,清除認證信息。UsernamePasswordAuthenticationFilter: 核心認證過濾器。嘗試處理表單登錄請求。它從 POST 請求中提取用戶名和密碼,創(chuàng)建一個UsernamePasswordAuthenticationToken(一個Authentication接口的實現(xiàn))并進行認證。DefaultLoginPageGeneratingFilter: 如果沒有配置登錄頁面,這個過濾器會生成一個默認的登錄頁。DefaultLogoutPageGeneratingFilter: 生成默認的退出頁面。BasicAuthenticationFilter: 處理 HTTP Basic 認證頭。RequestCacheAwareFilter: 用于在用戶認證成功后,恢復因登錄而中斷的原始請求。SecurityContextHolderAwareRequestFilter: 包裝原始的HttpServletRequest,提供一些 Spring Security 特有的方法,如getRemoteUser(),isUserInRole()等。AnonymousAuthenticationFilter: 至關重要。如果此時SecurityContextHolder中還沒有認證信息(即用戶未登錄),它會創(chuàng)建一個匿名的Authentication對象(AnonymousAuthenticationToken)并放入其中。這確保了安全上下文中永遠有一個Authentication對象,避免了空指針異常,統(tǒng)一了“已認證”和“未認證”的處理邏輯。SessionManagementFilter: 處理會話相關的策略,如同一個用戶的會話數(shù)量控制(防止同一賬號多次登錄)。ExceptionTranslationFilter: 至關重要。它是整個過濾器鏈的“看門人”,負責捕獲后續(xù)過濾器(特別是FilterSecurityInterceptor)拋出的異常,并將其轉換為相應的行為(如重定向到登錄頁、返回 403 錯誤等)。它本身不進行認證或授權。FilterSecurityInterceptor: 最終大門。這是授權發(fā)生的地方。它從SecurityContextHolder中獲取已認證的Authentication對象,然后根據(jù)配置的權限規(guī)則(訪問屬性配置,如hasRole(‘ADMIN’)),決定是允許請求繼續(xù)(調用FilterChain.doFilter())還是拒絕訪問(拋出AccessDeniedException)。
工作流程簡化視圖:HTTP Request -> Filter1 -> Filter2 -> ... -> FilterSecurityInterceptor -> DispatcherServlet -> Your Controller
2.2.2 認證 (Authentication) 核心組件
Authentication接口: 代表一個認證請求或一個已認證的主體(用戶)。它包含:principal: 主體標識,通常是用戶名、UserDetails 對象或用戶ID。credentials: 憑證,通常是密碼。認證成功后通常會擦除。authorities: 權限集合,即GrantedAuthority對象列表。SecurityContext接口: 持有Authentication對象。SecurityContextHolder.getContext().getAuthentication()是獲取當前用戶信息的標準方式。SecurityContextHolder: 存儲SecurityContext的策略容器。默認使用ThreadLocal策略,這意味著每個線程都有自己的SecurityContext,從而保證了用戶請求之間的隔離。AuthenticationManager: 認證的入口/大門。它只有一個方法:authenticate(Authentication authentication)。你通常不會直接使用它。ProviderManager:AuthenticationManager最常用的實現(xiàn)。它本身不處理認證,而是委托給一個AuthenticationProvider列表。它會遍歷這個列表,直到有一個Provider能夠處理當前的Authentication類型。AuthenticationProvider: 執(zhí)行具體認證邏輯的組件。例如:DaoAuthenticationProvider: 最常用的 Provider,從數(shù)據(jù)庫(DAO)中獲取用戶信息進行認證。它需要依賴一個UserDetailsService。JwtAuthenticationProvider: 用于處理 JWT Token 認證。LdapAuthenticationProvider: 用于 LDAP 認證。UserDetailsService: 核心接口,只有一個方法loadUserByUsername(String username)。它負責從存儲系統(tǒng)(數(shù)據(jù)庫、內存等)中根據(jù)用戶名加載用戶信息,并返回一個UserDetails對象。這是你需要自定義實現(xiàn)的最常見接口。UserDetails: 接口,代表從系統(tǒng)存儲中加載出來的用戶信息,包括用戶名、密碼、權限、賬戶是否過期等。框架提供的實現(xiàn)是User。
認證數(shù)據(jù)流:UsernamePasswordAuthenticationFilter -> 創(chuàng)建 UsernamePasswordAuthenticationToken (未認證) -> 調用 ProviderManager.authenticate() -> 委托給 DaoAuthenticationProvider -> 調用 UserDetailsService.loadUserByUsername() -> 獲取 UserDetails -> 比較密碼 -> 認證成功 -> 返回一個已認證的 Authentication 對象 -> 被過濾器設置到 SecurityContextHolder 中。
2.2.3 授權 (Authorization) 核心組件
AccessDecisionManager: 授權的決策管理器。它通過輪詢一組AccessDecisionVoter并進行投票,最終根據(jù)投票策略決定是否允許訪問。AccessDecisionVoter: 投票器。它檢查當前用戶的Authentication和受保護對象所需的配置屬性(ConfigAttribute,如ROLE_ADMIN),然后投贊成、反對或棄權票。ConfigAttribute: 保存著訪問受保護資源(如一個URL)所需的權限信息。通常來自你的配置:.antMatchers("/admin/**").hasRole("ADMIN")中的hasRole("ADMIN")就是一個ConfigAttribute。FilterSecurityInterceptor: 如上所述,它是授權發(fā)生的觸發(fā)器。它調用AccessDecisionManager進行決策。
授權數(shù)據(jù)流:
請求到達FilterSecurityInterceptor-> 獲取受保護資源的ConfigAttribute-> 調用AccessDecisionManager.decide()-> 輪詢所有AccessDecisionVoter.vote()-> 根據(jù)投票策略(如“一票否決”、“多數(shù)同意”)做出最終決定 -> 允許訪問或拋出AccessDeniedException-> 被上層的ExceptionTranslationFilter捕獲處理。
三、基本使用示例
需求:SpringBoot整合Spring Security頁面登陸,要求用戶信息存入數(shù)據(jù)庫,且密碼加密存儲,登錄成功后返回JWT令牌用于后續(xù)請求認證;要求體現(xiàn)不同用戶授予不同權限;要求必要的安全配置。
安全特性:
- 密碼使用BCrypt加密存儲
- 基于角色的訪問控制
- JWT令牌認證,無狀態(tài)會話。完整的安全JWT流程
- 登錄:用戶憑據(jù)驗證 → 生成簽名JWT
- 傳輸:通過HTTPS傳輸 → 防止竊聽
- 存儲:客戶端安全存儲 → 防止XSS
- 使用:每個請求攜帶 → 認證用戶
- 驗證:服務器驗證簽名和有效期 → 防止篡改
- 注銷:客戶端刪除令牌 → 服務器可黑名單
- CSRF保護禁用(因使用JWT)
- 會話管理設置為無狀態(tài)
項目結構:
src/ ├── main/ │ ├── java/com/example/demo/ │ │ ├── config/ │ │ │ ├── SecurityConfig.java │ │ │ ├── JwtAuthenticationFilter.java │ │ │ └── JwtUtil.java │ │ ├── controller/ │ │ │ ├── AuthController.java │ │ │ └── TestController.java │ │ ├── entity/ │ │ │ ├── User.java │ │ │ └── Role.java │ │ ├── mapper/ │ │ │ └── UserMapper.java │ │ ├── service/ │ │ │ ├── UserService.java │ │ │ └── CustomUserDetailsService.java │ │ └── DemoApplication.java │ └── resources/ │ ├── application.properties │ ├── schema.sql │ └── mapper/UserMapper.xml
3.1 依賴配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>3.2 應用配置 (application.properties)
# 服務器端口 server.port=8080 # 數(shù)據(jù)庫配置 spring.datasource.url=jdbc:mysql://localhost:3306/security_demo?useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis配置 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.example.demo.entity # JWT密鑰 jwt.secret=mySecretKey jwt.expiration=86400
密鑰配置:
- 密鑰長度至少與哈希算法安全性要求一致(HS512建議至少64字節(jié))
- 生產(chǎn)環(huán)境應從安全配置源獲取密鑰(環(huán)境變量、密鑰管理服務)
- 定期輪換密鑰
# 使用足夠長且復雜的密鑰 jwt.secret=mySuperLongAndComplexSecretKeyThatIsHardToGuess123!
雖然代碼中不直接體現(xiàn),但部署時必須使用HTTPS,防止中間人攻擊,加密整個通信通道
# 生產(chǎn)環(huán)境應強制使用HTTPS server.ssl.enabled=true server.ssl.key-store=classpath:keystore.p12 server.ssl.key-store-password=password server.ssl.key-store-type=PKCS12
3.3 數(shù)據(jù)庫初始化 (schema.sql)
CREATE DATABASE IF NOT EXISTS security_demo;
USE security_demo;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS user_roles (
user_id INT NOT NULL,
role_id INT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- 插入角色數(shù)據(jù)
INSERT IGNORE INTO roles (name) VALUES ('ROLE_USER');
INSERT IGNORE INTO roles (name) VALUES ('ROLE_ADMIN');
-- 插入用戶數(shù)據(jù)(密碼使用BCrypt加密,原始密碼均為"password")
INSERT IGNORE INTO users (username, password, enabled) VALUES
('user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 1),
('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 1);
-- 分配角色
INSERT IGNORE INTO user_roles (user_id, role_id) VALUES
(1, 1), -- user has ROLE_USER
(2, 2); -- admin has ROLE_ADMIN3.4 實體類
// User.java
package com.example.demo.entity;
import java.util.List;
public class User {
private Long id;
private String username;
private String password;
private Boolean enabled;
private List<Role> roles;
// 構造方法、getter和setter
public User() {}
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 省略getter和setter
}
// Role.java
package com.example.demo.entity;
public class Role {
private Long id;
private String name;
// 構造方法、getter和setter
public Role() {}
public Role(String name) {
this.name = name;
}
// 省略getter和setter
}3.5 MyBatis Mapper接口和XML
// UserMapper.java
package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
User findByUsername(String username);
User findById(Long id);
}<!-- UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<resultMap id="userResultMap" type="User">
<id property="id" column="id" />
<result property="username" column="username" />
<result property="password" column="password" />
<result property="enabled" column="enabled" />
<collection property="roles" ofType="Role">
<id property="id" column="role_id" />
<result property="name" column="role_name" />
</collection>
</resultMap>
<select id="findByUsername" resultMap="userResultMap">
SELECT u.*, r.id as role_id, r.name as role_name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.username = #{username}
</select>
<select id="findById" resultMap="userResultMap">
SELECT u.*, r.id as role_id, r.name as role_name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.id = #{id}
</select>
</mapper>3.6 服務層
// 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.List;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用戶不存在: " + username);
}
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities);
}
}
// UserService.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.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}3.7 JWT工具類
package com.example.demo.config;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* JWT工具類 - 負責JWT令牌的生成、解析和驗證
*
* 安全特性說明:
* 1. 使用HMAC-SHA512算法進行簽名,確保令牌完整性
* 2. 設置合理的過期時間,減少令牌泄露風險
* 3. 從配置文件中讀取密鑰,便于管理和輪換
* 4. 提供完整的異常處理,防止無效令牌導致系統(tǒng)異常
*/
@Component
public class JwtUtil {
// 從配置文件中注入JWT密鑰,生產(chǎn)環(huán)境應使用復雜且足夠長的密鑰
@Value("${jwt.secret}")
private String secret;
// 從配置文件中注入JWT過期時間(秒)
@Value("${jwt.expiration}")
private long expiration;
/**
* 生成JWT令牌
*
* 安全考慮:
* 1. 只包含必要信息(用戶名),不包含敏感數(shù)據(jù)
* 2. 設置簽發(fā)時間和過期時間,控制令牌有效期
* 3. 使用強加密算法(HS512)進行簽名
*
* @param authentication Spring Security認證對象
* @return JWT令牌字符串
*/
public String generateToken(Authentication authentication) {
// 從認證對象中獲取用戶信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
// 計算過期時間:當前時間 + 配置的過期時間(轉換為毫秒)
Date expiryDate = new Date(now.getTime() + expiration * 1000);
// 構建JWT令牌
return Jwts.builder()
.setSubject(userDetails.getUsername()) // 設置主題(用戶名)
.setIssuedAt(now) // 設置簽發(fā)時間
.setExpiration(expiryDate) // 設置過期時間
.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512算法和密鑰簽名
.compact(); // 生成緊湊的JWT字符串
}
/**
* 從JWT令牌中提取用戶名
*
* 安全考慮:
* 1. 驗證簽名確保令牌未被篡改
* 2. 解析前不信任任何令牌內容
*
* @param token JWT令牌
* @return 用戶名
*/
public String getUsernameFromToken(String token) {
// 解析JWT令牌,驗證簽名并獲取聲明(Claims)
Claims claims = Jwts.parser()
.setSigningKey(secret) // 設置簽名密鑰
.parseClaimsJws(token) // 解析JWS(已簽名的JWT)
.getBody(); // 獲取有效負載(Payload)
// 返回主題(用戶名)
return claims.getSubject();
}
/**
* 驗證JWT令牌的有效性
*
* 安全考慮:
* 1. 驗證簽名是否正確,防止偽造令牌
* 2. 檢查令牌是否過期
* 3. 捕獲所有可能異常,防止無效令牌導致系統(tǒng)異常
*
* @param token JWT令牌
* @return 令牌是否有效
*/
public boolean validateToken(String token) {
try {
// 嘗試解析令牌,如果成功則說明令牌有效
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (SignatureException ex) {
// 簽名不匹配 - 令牌可能被篡改
// 記錄日志但不拋出異常,避免信息泄露
} catch (MalformedJwtException ex) {
// 令牌格式錯誤 - 不是有效的JWT
} catch (ExpiredJwtException ex) {
// 令牌已過期 - 需要重新登錄獲取新令牌
} catch (UnsupportedJwtException ex) {
// 不支持的JWT令牌 - 可能使用了錯誤的算法
} catch (IllegalArgumentException ex) {
// JWT claims string is empty - 令牌為空
}
// 任何異常都意味著令牌無效
return false;
}
}3.8 JWT認證過濾
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT認證過濾器 - 處理每個請求的JWT認證
*
* 安全特性說明:
* 1. 在每個請求前執(zhí)行,確保所有請求都經(jīng)過認證檢查
* 2. 從Authorization頭中提取Bearer令牌
* 3. 驗證令牌有效性并設置安全上下文
* 4. 即使認證失敗也繼續(xù)過濾器鏈,確保公共接口可訪問
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 過濾器核心方法 - 處理每個HTTP請求
*
* 安全流程:
* 1. 從請求中提取JWT令牌
* 2. 驗證令牌有效性
* 3. 如果有效,從令牌中提取用戶名并加載用戶詳情
* 4. 設置安全上下文,供后續(xù)授權檢查使用
*
* @param request HTTP請求
* @param response HTTP響應
* @param filterChain 過濾器鏈
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 從HTTP請求中獲取JWT令牌
String jwt = getJwtFromRequest(request);
// 驗證令牌是否存在且有效
if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
// 從有效令牌中提取用戶名
String username = jwtUtil.getUsernameFromToken(jwt);
// 從數(shù)據(jù)庫加載用戶詳細信息(包括權限)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 創(chuàng)建認證令牌,包含用戶詳情和權限
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 添加請求詳情(如IP地址、會話ID等)
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 將認證信息設置到安全上下文中,供后續(xù)授權檢查使用
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
// 捕獲所有異常,避免因認證問題導致請求失敗
// 記錄錯誤日志但繼續(xù)處理請求(某些接口可能允許匿名訪問)
logger.error("Could not set user authentication in security context", ex);
}
// 繼續(xù)過濾器鏈處理(無論認證成功與否)
filterChain.doFilter(request, response);
}
/**
* 從HTTP請求中提取JWT令牌
*
* 安全考慮:
* 1. 只接受Bearer類型的認證頭
* 2. 移除"Bearer "前綴,獲取純令牌
*
* @param request HTTP請求
* @return JWT令牌或null(如果不存在)
*/
private String getJwtFromRequest(HttpServletRequest request) {
// 從Authorization頭獲取Bearer令牌
String bearerToken = request.getHeader("Authorization");
// 檢查令牌是否存在且格式正確(以"Bearer "開頭)
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
// 返回去掉"Bearer "前綴的純令牌
return bearerToken.substring(7);
}
// 沒有找到有效令牌
return null;
}
}3.9 Spring Security配置
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security配置類 - 定義應用程序的安全策略
*
* 安全特性說明:
* 1. 使用無狀態(tài)會話管理,適合RESTful API
* 2. 配置密碼編碼器,確保密碼安全存儲
* 3. 定義URL訪問規(guī)則,實現(xiàn)基于角色的訪問控制
* 4. 集成JWT認證過濾器,替代默認的表單登錄
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
/**
* 密碼編碼器Bean - 用于密碼加密和驗證
*
* 安全考慮:
* 1. 使用BCrypt強哈希算法,自動處理鹽值
* 2. 適合密碼存儲,抵抗彩虹表攻擊
*
* @return BCrypt密碼編碼器實例
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 認證管理器Bean - 暴露給其他組件使用
*
* 用途:
* 1. 在AuthController中用于手動認證用戶
* 2. 可以被其他需要認證服務的組件使用
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置認證管理器 - 設置自定義用戶詳情服務和密碼編碼器
*
* 安全流程:
* 1. 使用自定義UserDetailsService從數(shù)據(jù)庫加載用戶信息
* 2. 使用BCrypt密碼編碼器驗證密碼
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 配置HTTP安全策略 - 核心安全配置方法
*
* 安全策略:
* 1. 禁用CORS和CSRF(因使用無狀態(tài)JWT認證)
* 2. 使用無狀態(tài)會話管理
* 3. 配置URL訪問規(guī)則(基于角色)
* 4. 添加JWT認證過濾器
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 啟用CORS并禁用CSRF(因使用JWT而非Cookie)
.cors().and().csrf().disable()
// 會話管理設置為無狀態(tài)(不創(chuàng)建和使用HTTP會話)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 配置請求授權規(guī)則
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // 認證接口允許匿名訪問
.antMatchers("/api/user/**").hasRole("USER") // 用戶接口需要USER角色
.antMatchers("/api/admin/**").hasRole("ADMIN") // 管理員接口需要ADMIN角色
.anyRequest().authenticated() // 其他所有請求需要認證
.and();
// 添加JWT認證過濾器到UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 創(chuàng)建JWT認證過濾器Bean
*
* 說明:
* 1. 過濾器在每個請求前執(zhí)行
* 2. 負責提取和驗證JWT令牌
* 3. 設置安全上下文中的認證信息
*
* @return JWT認證過濾器實例
*/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}3.10 控制器
// AuthController.java
package com.example.demo.controller;
import com.example.demo.config.JwtUtil;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> loginRequest) {
String username = loginRequest.get("username");
String password = loginRequest.get("password");
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtil.generateToken(authentication);
User user = userService.findByUsername(username);
Map<String, Object> response = new HashMap<>();
response.put("token", jwt);
response.put("user", user);
return ResponseEntity.ok(response);
}
}
// TestController.java
package com.example.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/user/test")
@PreAuthorize("hasRole('USER')")
public String userAccess() {
return "用戶內容";
}
@GetMapping("/admin/test")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "管理員內容";
}
}3.11 主應用類
// DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}3.12 測試
登錄獲取令牌:
POST http://localhost:8080/api/auth/login
Content-Type: application/json
{
"username": "user",
"password": "password"
}訪問用戶API:
GET http://localhost:8080/api/user/test Authorization: Bearer <your_token>
訪問管理員API:
GET http://localhost:8080/api/admin/test Authorization: Bearer <your_token>
到此這篇關于Spring Security的基本使用示例的文章就介紹到這了,更多相關Spring Security使用內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
mybatis中 if-test 數(shù)字判斷的坑及解決
這篇文章主要介紹了mybatis中 if-test 數(shù)字判斷的坑及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06
Spring?Data?JPA框架快速入門之自定義Repository接口
Spring?Data?JPA是Spring基于JPA規(guī)范的基礎上封裝的?套?JPA?應?框架,可使開發(fā)者?極簡的代碼即可實現(xiàn)對數(shù)據(jù)庫的訪問和操作,本篇我們來了解Spring?Data?JPA框架的自定義Repository接口2022-04-04
Springboot使用Rabbitmq的延時隊列+死信隊列實現(xiàn)消息延期消費
本文介紹了RabbitMQ的延時隊列和死信隊列,解釋了它們的工作原理及其應用場景,延時隊列允許消息在設定的時間后被消費,結合實際案例,展示了如何實現(xiàn)和使用延時隊列和死信隊列,感興趣的朋友一起看看吧2025-01-01
java8 統(tǒng)計字符串字母個數(shù)的幾種方法總結(推薦)
下面小編就為大家分享一篇java8 統(tǒng)計字符串字母個數(shù)的幾種方法總結(推薦),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來吧2017-11-11
解決Springboot啟動報錯:類文件具有錯誤的版本61.0,應為?52.0
這篇文章主要給大家介紹了關于解決Springboot啟動報錯:類文件具有錯誤的版本?61.0,應為?52.0的相關資料,這是查閱了網(wǎng)上的很多資料才解決的,分享給大家,需要的朋友可以參考下2023-01-01
SpringSecurity微服務實戰(zhàn)之公共模塊詳解
這篇文章主要為大家介紹了SpringSecurity微服務實戰(zhàn)之公共模塊詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
使用dom4j實現(xiàn)xml轉map與xml轉json字符串
這篇文章主要為大家詳細介紹了如何使用dom4j實現(xiàn)xml轉map與xml轉json字符串功能,文中的示例代碼講解詳細,感興趣的小伙伴可以了解下2024-11-11

