在我們的 Spring Boot 應用程式中將 JWT(JSON Web 令牌)與 Spring Security 整合。這將使我們能夠透過使用 JWT 整合強大的身份驗證和授權機制來增強我們的安全框架。
目標:確保只有使用有效的 JWT 令牌才能訪問關鍵端點。在我們的 Spring Boot 專案中,我們主要有兩個關鍵的 REST 端點:一個用於獲取所有員工資料,另一個用於新增新員工,這些端點是安全的,並且需要基於 JWT 的身份驗證才能進行訪問。
什麼是JWT?
JWT(即 JSON Web 令牌)就像數字通行證一樣,有助於確保 Web 應用程式的安全。當有人登入應用程式時,伺服器會向他們提供 JWT。
這就像您每次回來時都會獲得一枚徽章來證明您是誰。每次使用者想要訪問應用程式的安全部分時,他們都會顯示此徽章,伺服器知道讓他們進入是安全的。
這就是 JWT 如此出色的原因:
- 易於管理:它們是簡單的文字字串,使其易於在計算機和裝置之間處理和傳送。
- 一體化:伺服器驗證使用者所需的所有內容都打包到 JWT 中。這意味著伺服器不必不斷向資料庫詢問資訊,從而加快速度。
- 安全:它們可以被加密和簽名,使未經授權的人很難弄亂它們。
JWT 令牌的組成部分:
JWT 由三個主要部分組成,每個部分用點 ( .) 分隔。這是一個簡單的細分:
- 標頭
- 目的:標頭通常告訴我們令牌的型別(JWT)以及用於簽名令牌的演算法,例如HMAC SHA256 或 RSA。
- 例子:{"alg": "HS256", "typ": "JWT"}
- 說明:這是一個簡單的 JSON 物件,表明 JWT 正在使用“HS256”演算法進行加密。它以 Base64Url 格式進行編碼,使其成為 URL 安全的。
有效載荷
- 目的:有效負載包含宣告。宣告是關於實體(通常是使用者)和附加資料的宣告。
- 例子:{"sub": "1234589670", "name": "Gaurav Sharma", "admin": true}
- 說明:此 JSON 物件包含有關使用者和其他後設資料的資訊。它表示 JWT 的主體(使用者)的 ID 為“ 1234589670”,名稱為“ Gaurav Sharma”,並且具有管理許可權。
簽名
- 目的:簽名用於驗證 JWT 的傳送者是否是其所說的傳送者,並確保訊息在此過程中沒有被更改。
- 製作方法:要建立簽名,您需要獲取編碼標頭、編碼有效負載、秘密、標頭中指定的演算法(如 HMAC SHA256),然後對其進行簽名。
- 說明:例如,如果您使用 HMAC SHA256 演算法,則將使用金鑰將 HMAC SHA256 演算法應用於 Base64Url 編碼標頭和負載的組合字串來建立簽名。
<ul>這三個部分共同構成了 JWT:header.payload.signature.
傳送 JWT 時,它顯示為由三個 Base64Url 編碼部分組成的字串,用點分隔,看起來像這樣:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。
這本質上就是 JWT 的組成部分以及每個部分的功能。
- 注意: JWT 已編碼但未加密,因此如果被攔截則可以讀取。因此,避免將密碼等敏感資料直接儲存在 JWT 中,因為任何有權訪問的人都可以解碼和提取此資訊。
- 相反,JWT 應僅包含使用者識別和會話管理所需的必要資訊,例如使用者 ID 或使用者名稱,以及有助於訪問控制決策的許可權或角色。為了確保令牌的完整性和真實性,它使用只有伺服器知道的金鑰進行數字簽名。儘管可以檢視內容,但簽名有助於確保令牌在頒發後不會被更改。
為什麼選擇JWT?
有人可能會想:如果我們可以簡單地使用使用者名稱和密碼來訪問 Web 應用程式中的安全端點,那麼為什麼我們選擇將 JWT 與 Spring Security 結合使用呢?
選擇帶有 Spring Security 的 JWT(JSON Web 令牌)而不是僅僅使用使用者名稱和密碼有幾個好處,特別是對於現代 Web 應用程式:
- 無需持續簽入:JWT 包含所有使用者資訊,因此伺服器可以驗證您的身份,而無需在您每次發出請求時詢問資料庫。這對於處理大量使用者來說非常有用,因為它減少了資料庫流量。
- 額外的安全性:當您仍然使用使用者名稱和密碼登入時,JWT 提供了一種更安全的方式來在您登入後繼續檢查您的身份。令牌受到保護並具有內建的過期時間,這有助於防止以免它們被濫用。
- 無處不在:JWT 擅長跨不同系統和裝置工作。這意味著一旦您登入到系統的某一部分,您也可以使用其他部分,而無需再次登入。
- 更快:因為您的伺服器並不總是詢問資料庫您是誰,所以事情可以執行得更快。使用 JWT,您的伺服器只需要檢查您隨請求傳送的令牌,從而加快速度。
- 攜帶更多資訊:JWT 還可以包含額外的詳細資訊,例如您擁有哪些許可權,這有助於伺服器知道您可以執行哪些操作,而無需一直查詢。
簡而言之,JWT 使事情變得更安全、更快速、更易於管理,特別是當您有大量使用者或需要跨系統的不同部分工作時。這使它們成為現代 Web 應用程式的有力選擇。對於微服務,涉及多個伺服器:
- JWT(JSON Web 令牌)廣泛用於微服務架構,其中 Web 應用程式是使用處理不同服務的多個伺服器構建的。在此類環境中,每次使用者發出請求時都從特定伺服器檢索使用者名稱和密碼可能效率低下,並且會降低系統速度。
- JWT 透過將使用者的身份和授權詳細資訊封裝在安全令牌中來解決這一挑戰。該令牌通常在使用者登入後發出一次,然後用於跨不同服務的後續請求。此方法無需連續查詢中央身份驗證伺服器或資料庫,從而簡化了跨多個伺服器的身份驗證流程並增強了整體系統效能。
在多個小型服務協同工作的微服務設定中,JWT(JSON Web 令牌)為有效管理安全性提供了一些明顯的好處:
- 獨立驗證:每個微服務都可以使用 JWT 中的資訊自行檢查您是誰。無需不斷要求中央伺服器驗證您的身份,從而減少了延遲。
- 更少的網路流量:由於 JWT 在其內部攜帶所有需要的使用者資訊,因此服務不需要不斷地相互通訊或與中央資料庫通訊來檢查使用者詳細資訊。這減少了網路流量並加快了速度。
- 跨服務工作:JWT 非常適合跨越不同區域或系統的設定。一旦您登入並擁有 JWT,系統中的任何服務都可以識別並信任它。
- 不需要記憶體:JWT 有助於保持系統簡單,因為服務不需要記住使用者會話。所需的一切都在 JWT 中,它支援微服務的無狀態操作。
總體而言,JWT 使微服務架構中的安全性變得更順暢、更快速且更具可擴充套件性,非常適合每個服務獨立且安全地執行的需求。
本文分為三個部分:
- 第一部分涉及開發一個基本 API 來新增員工和檢索所有員工。
- 第二部分重點介紹在 Spring Security 中實現使用者相關的類。
- 第三部分介紹了 JWT 與 Spring Security 的整合,以及在請求標頭中使用 jwt 訪問專案的關鍵端點。
第 1 章:在 Spring Boot 中構建員工管理 API案例
第 1 章提供了在 Spring Boot 中建立 API 以在員工管理系統中新增和檢索員工的基本概述。
第2章:在Spring Security中實現使用者相關的類
注意:將 JWT 與 Spring Boot 和 Spring Security 整合是一項艱鉅的任務。
為了設定我們的安全系統,我們需要建立一個包含使用者名稱和密碼等欄位的使用者類。這使我們能夠將使用者資訊儲存在資料庫中並根據這些憑據對使用者進行身份驗證。
然而,有一個重要的方面需要注意:Spring Security 不會自動識別這個自定義使用者類。相反,它使用其預定義的UserDetails介面。
簡單來說,UserDetails是Spring Security中的一個特殊介面,旨在以Spring Security可以理解的方式處理使用者資訊。這意味著,為了讓 Spring Security 能夠使用我們的自定義使用者類,我們需要調整我們的類以適應此介面。本質上,我們需要將使用者類轉換為實現該UserDetails介面的使用者類。
import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
<font>/** * UserInfo 類代表應用程式中的使用者實體。 * 用於儲存使用者相關資料,如姓名、密碼等。 */<i> @Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String userName; private String password;
}
|
對上述程式碼的解釋:
此程式碼設定一個簡單的User類來將使用者資訊儲存在資料庫中,特別是他們的 ID、使用者名稱和密碼。
實現spring security提供的UserDetails介面:
UserPrincipal.java
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection; import java.util.Collections; import java.util.Set;
public class UserPrincipal implements UserDetails { <font>/** * */<i> private static final long serialVersionUID = 1L; String userName = null; String password = null; Set<SimpleGrantedAuthority> authorities;
public UserPrincipal(User user) { userName = user.getUserName(); password = user.getPassword(); authorities = Collections.singleton(new SimpleGrantedAuthority("USER")); }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return userName; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
|
程式碼中的 UserPrincipal 類是 Spring Security 提供的 UserDetails 介面的自定義實現。該類用於將應用程式的使用者實體與 Spring Security 的身份驗證和授權機制整合。
下面是其功能的詳細介紹:
- 類欄位:該類有三個主要欄位:userName、password 和 authorities。它們分別代表使用者的登入憑證及其角色或許可權。
- 建構函式:UserPrincipal 建構函式將一個 User 物件作為引數。它從該使用者物件中提取使用者名稱和密碼,並初始化使用者的許可權。
- getAuthorities():該方法指定授予使用者的角色或許可權。在本例中,每個使用者都被授予 "USER "的單一許可權。
- getPassword() 和 getUsername():這些方法分別從使用者例項中獲取密碼和使用者名稱。
- 賬戶狀態方法:isAccountNonExpired()、isAccountNonLocked()、isCredentialsNonExpired() 和 isEnabled() 方法都被過載為返回 true。Spring Security 使用這些方法來確定賬戶是否仍處於活動狀態、是否已鎖定、憑據是否過期或是否已啟用。所有這些方法都返回 true 表明,在這個簡單的實現中,這些檢查並不是用來限制使用者訪問的。
UserRepo.java
import com.unlogged.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository public interface UserRepo extends JpaRepository<User, Integer> { <font>/** *findByName 方法用於根據使用者名稱檢索使用者。 * 返回 UserInfo 的一個可選項,如果沒有找到使用者,則返回空值。 */<i> Optional<User> findByUserName(String userName); }
|
上述程式碼的解釋:
UserRepo 介面中的 findByUsername(String username) 方法是一個專門函式,可讓你根據使用者名稱查詢和檢索使用者。
UserService.java
import com.unlogged.model.User; import com.unlogged.model.UserPrincipal; import com.unlogged.repo.UserRepo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service;
import java.util.Optional;
<font>/** * UserInfoService 提供與使用者相關的服務,包括載入使用者詳細資訊 *和管理儲存庫中的使用者資料。 */<i> @Service public class UserService implements UserDetailsService {
@Autowired private UserRepo userRepo;
@Autowired private PasswordEncoder passwordEncoder;
// 根據使用者名稱載入使用者的詳細資訊。<i> @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Optional<User> user = userRepo.findByUserName(username); return user.map(UserPrincipal::new) .orElseThrow(() -> new UsernameNotFoundException("UserName not found: " + username)); }
// 向版本庫新增新使用者,並在儲存密碼前對其進行加密。<i> public String addUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); userRepo.save(user); return "user added successfully"; }
}
|
上述程式碼的解釋:
- UserService 類實現了 Spring Security 的 UserDetailsService 介面,提供了一個從資料庫載入使用者詳細資訊的方法。這可確保該類與 Spring Security 的身份驗證流程無縫協作。
- UserService 類負責在 Spring Security 框架內處理使用者資訊。它包括兩個主要方法:
- loadUserByUsername: 該方法使用提供的使用者名稱從資料庫中檢索使用者詳細資訊。如果使用者存在,它將返回封裝在 UserPrincipal 物件中的詳細資訊,供 Spring Security 的身份驗證流程使用。如果找不到使用者名稱,則會丟擲異常。
- addUser:該方法獲取一個 User 物件,使用 PasswordEncoder 對使用者密碼進行安全加密,並將更新後的使用者儲存到資料庫中。該方法會以成功訊息確認新增成功。
第3章:將 JWT 與 Spring Security 整合
要在 Spring Boot Maven 專案中實現用於授權的 JSON Web 標記 (JWT),我們需要在 pom.xml 檔案中包含特定的依賴項。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </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> </dependency>
|
上述依賴關係的解釋:
- jjwt-jackson作用:將 Jackson 庫與 JWT 操作整合,最佳化 Java 應用程式對 JWT 中 JSON 資料的編碼和解碼。
- jjwt-api有什麼作用?提供用於構建和驗證 JWT 的基本類和介面,為 Java 中的 JWT 功能提供構建模組。
- jjwt-impl作用提供 jjwt-api 介面的實際實現,可根據應用程式的安全協議生成、解析和管理 JWT。
pom.xml:
<?xml version=<font>"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 https://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>3.2.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.unlogged</groupId> <artifactId>EmployeeManagementSystem</artifactId> <version>0.0.1-SNAPSHOT</version> <name>EmployeeManagementSystem</name> <description>EmployeeManagementSystem</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> </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> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
</project>
|
pom.xml 檔案中的每個依賴項:
- Spring Boot Starter Data JPA:將 Spring Data JPA 與 Spring Boot 整合,使用 JPA 資源庫簡化資料庫互動。
- Spring Boot Starter 安全:為 Spring Boot 應用程式新增身份驗證和授權等安全功能。
- Spring Boot Starter Web:提供了使用 Spring MVC 構建 Web 應用程式(包括 RESTful 應用程式)的所有必需品。
- Spring Boot DevTools:提供自動重啟工具和實時過載功能,以提高開發人員的工作效率。
- PostgreSQL 驅動程式:實現與 PostgreSQL 資料庫的 JDBC 連線,允許在應用程式和資料庫之間進行資料交易。
- Lombok:透過自動生成獲取器、設定器、構造器等,減少 Java 應用程式中的模板程式碼。
- jjwt-jackson:為處理 JSON 網路令牌的 JJWT 庫提供 Jackson JSON 處理功能。
- jjwt-api:為高效建立和驗證 JSON 網路令牌(JWT)提供 API 支援。
- jjwt-impl:實現 JJWT API,提供管理 JWT 的必要程式碼。
現在,我們將定義一些類,以便將 jwt 整合到我們的安全機制中。
JwtService 類用於建立、檢查和驗證安全令牌(JWT),以幫助確認應用程式中的使用者身份。
JwtService.java
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component;
import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function;
<font>//JwtService 負責處理 JWT(JSON 網路令牌)操作<i> //,例如令牌生成、提取宣告和令牌驗證。<i>
@Component public class JwtService {
// 用於簽署 JWT 的金鑰。該金鑰應保密。<i> private static final String SECRET = "TmV3U2VjcmV0S2V5Rm9ySldUU2lnbmluZ1B1cnBvc2VzMTIzNDU2Nzg=\r\n" + "";
// 為給定的使用者名稱生成 JWT 標記。<i> public String generateToken(String userName) { // 準備token宣告<i> Map<String, Object> claims = new HashMap<>();
// 建立包含宣告、主題、釋出時間、過期時間和簽名演算法的 JWT 令牌<i> // 令牌有效期為 3 分鐘<i> return Jwts.builder() .setClaims(claims) .setSubject(userName) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 3)) .signWith(getSignKey(), SignatureAlgorithm.HS256).compact(); }
//根據 base64 編碼的密文建立簽名金鑰。<i> // 返回一個用於簽署 JWT 的金鑰物件。<i> private Key getSignKey() { // 解碼 base64 編碼秘鑰並返回一個金鑰物件<i> byte[] keyBytes = Decoders.BASE64.decode(SECRET); return Keys.hmacShaKeyFor(keyBytes); }
//從 JWT 標記中提取使用者名稱。<i> //return -> The userName contained in the token.<i> public String extractUserName(String token) { // 從標記中提取並返回主題請求<i> return extractClaim(token, Claims::getSubject); }
// 從 JWT 令牌中提取到期日期。<i> //@return The expiration date of the token.<i> public Date extractExpiration(String token) { // 從令牌中提取並返回有效期宣告<i> return extractClaim(token, Claims::getExpiration); }
//從 JWT 標記中提取特定的請求。<i> // return-> The value of the specified claim.<i> private <T> T extractClaim(String token, Function<Claims, T> claimResolver) { //使用提供的函式提取指定的索賠<i> final Claims claims = extractAllClaims(token); return claimResolver.apply(claims); }
//從 JWT 標記中提取所有請求。<i> //return-> Claims 物件包含所有請求。<i> private Claims extractAllClaims(String token) { //解析並返回令牌中的所有請求<i> return Jwts.parserBuilder() .setSigningKey(getSignKey()) .build().parseClaimsJws(token).getBody(); }
//檢查 JWT 令牌是否過期。<i> //return-> 如果令牌已過期,則返回 true,否則返回 false。<i> public Boolean isTokenExpired(String token) { // 檢查令牌的到期時間是否早於當前時間<i> return extractExpiration(token).before(new Date()); }
//根據 UserDetails 驗證 JWT 令牌。<i> //return-> 如果令牌有效,則返回 true,否則返回 false。<i>
public Boolean validateToken(String token, UserDetails userDetails) { // 從令牌中提取使用者名稱,並檢查是否與 UserDetails 的使用者名稱匹配<i> final String userName = extractUserName(token); // 同時檢查令牌是否過期<i> return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
|
JwtService 類中的關鍵方法:
- generateToken生成令牌:為使用者生成一個安全令牌,其中包括使用者名稱和令牌的有效期。
- getSignKey:從金鑰中建立一個特殊金鑰,以確保令牌的安全。
JwtService 類中的秘鑰就像一個數字簽名,用於確保 JSON Web 標記(JWT)的安全。該金鑰採用 base64 編碼,這種方法可將金鑰轉換為字串格式,以便在程式設計環境中更容易處理。它的主要作用是確保 JWT 中的資料不被未經授權的方篡改。對該金鑰進行保密至關重要;如果它落入壞人之手,就會有人偽造令牌,從而有可能在未經授權的情況下訪問使用者資料和特權系統功能。因此,保持金鑰的機密性對應用程式的安全至關重要。
如果您想生成自己的金鑰,請看一個小演示:
建立金鑰的有用網站是:
https://asecuritysite.com/cryption/plain
- extractUserName:從令牌中獲取使用者名稱。
- extractExpiration:找出令牌何時停止工作。
- extractClaim:extractClaim類中的方法使用JwtService作為引數傳遞的函式從解析的 JWT 令牌中檢索特定宣告,例如使用者名稱或到期日期。
- isTokenExpired:檢查令牌是否已超時且不再有效。
- validateToken:確保令牌仍然有效並且與正確的使用者匹配,確認它可以安全地用於登入。
下面JwtFilter檢查每個傳入請求,以確保使用者具有有效的登入令牌,並在一切檢查完畢後設定其登入狀態。
import com.unlogged.service.JwtService; import com.unlogged.service.UserService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component public class JwtFilter extends OncePerRequestFilter {
@Autowired private JwtService jwtService;
@Autowired private ApplicationContext applicationContext;
<font>// 從 ApplicationContext 輕鬆獲取 UserService Bean 的方法<i> // 這樣做是為了避免迴圈依賴問題<i> private UserService getUserService() { return applicationContext.getBean(UserService.class); }
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 從請求標頭提取令牌<i> String authHeader = request.getHeader("Authorization"); String token = null; String userName = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) { // 從授權標頭提取令牌<i> token = authHeader.substring(7); // Extracting username from the token<i> userName = jwtService.extractUserName(token); }
//如果提取了使用者名稱,且當前安全上下文中沒有驗證<i> if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 根據從令牌中提取的使用者名稱載入 UserDetails<i> UserDetails userDetails = getUserService().loadUserByUsername(userName);
// 用載入的使用者詳情驗證令牌<i> if (jwtService.validateToken(token, userDetails)) { // 使用 UserDetails 建立身份驗證令牌<i> UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // 設定身份驗證詳細資訊<i> authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 在安全上下文中設定身份驗證令牌<i> SecurityContextHolder.getContext().setAuthentication(authToken); } }
// Proceeding with the filter chain<i> filterChain.doFilter(request, response); } }
|
JwtFilter類中的每個方法:
- getUserService:getUserService該類中的方法旨在JwtFilter僅UserService在需要時從 Spring 應用程式上下文中檢索例項。此方法用於防止與 Spring bean 載入順序相關的問題(稱為迴圈依賴問題)。本質上,它確保UserService在訪問時可用並完全初始化,避免由於依賴性衝突而導致任何啟動錯誤。
- doFilter內部:
- 目標用途:檢查每個傳入 HTTP 請求的授權標頭中的 JWT。
- 處理流程:如果找到有效的 JWT,它會提取使用者名稱並檢查以SecurityContextHolder確定使用者是否已透過當前會話的身份驗證。
- 使用者身份驗證:如果使用者未登入,則會檢索使用者詳細資訊,根據這些詳細資訊驗證令牌,如果驗證透過,則在 SecurityContextHolder 中設定使用者的身份驗證。
- 會話管理:該機制集中安全資訊,促進跨應用程式的使用者身份驗證和授權的輕鬆訪問和管理。
- 流程繼續:允許請求前進到過濾器鏈中的下一個階段或預期的最終目的地。
<ul>
SecurityContextHolder:這是 Spring Security 的一部分,用於儲存當前執行緒的安全上下文(即有關當前使用者及其許可權的詳細資訊)。它貫穿整個應用程式,用於授予或限制對系統不同部分的訪問許可權。
SecurityConfig.java 類為網路應用程式設定安全規則,以確保只有授權使用者才能訪問特定區域。
import com.unlogged.filter.JwtFilter; import com.unlogged.service.UserService; 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.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.bind.annotation.CrossOrigin;
@Configuration @EnableWebSecurity @CrossOrigin public class SecurityConfig {
@Autowired private JwtFilter jwtFilter;
<font>// 定義用於使用者身份驗證的 UserDetailsService Bean<i> @Bean public UserDetailsService userDetailsService() { return new UserService(); }
// 配置安全過濾鏈<i> @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .cors(Customizer.withDefaults()) // Apply CORS<i> .csrf(csrf -> csrf.disable()) // Disable CSRF protection<i> .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/addUser", "/auth/login") .permitAll()// Permit all requests to certain URLs<i> .anyRequest().authenticated()) // Require authentication for all other requests<i> .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Set session management to stateless<i> .authenticationProvider(authenticationProvider()) // Register the authentication provider<i> .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // Add the JWT filter before processing the request<i> .build(); }
//建立 DaoAuthenticationProvider 以處理使用者身份驗證<i> @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; }
//定義 PasswordEncoder Bean,預設使用 bcrypt 雜湊演算法對密碼進行編碼<i> @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }
//定義 AuthenticationManager Bean 以管理身份驗證流程<i> @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }
}
|
上述程式碼的解釋:
SecurityConfig 類中定義的每個方法:
- userDetailsService():初始化用於載入使用者特定資料的服務。它將應用程式連線到使用者服務,而使用者服務會檢索必要的使用者資訊,以便進行身份驗證。
securityFilterChain(HttpSecurity httpSecurity):
為應用程式中的 HTTP 請求配置安全策略。它設定瞭如何對請求進行身份驗證和授權。主要配置包括
- CORS 配置:應用跨起源資源共享的預設設定,以管理不同起源之間的資源互動。
- CSRF 保護:禁用跨站請求偽造保護,通常用於使用基於令牌的身份驗證而不是 cookie 的 API。
- 授權規則:定義訪問規則,允許不受限制地訪問某些路徑,如 /auth/addUser 和 /auth/login,並要求對所有其他請求進行身份驗證。
- 會話管理:將會話設定為無狀態,即伺服器不會在請求之間維護任何會話狀態。
- 身份驗證提供程式:整合 DaoAuthenticationProvider,使用使用者詳情服務和密碼編碼器來驗證使用者身份。
- JWT 過濾器整合:在標準使用者名稱-密碼身份驗證過濾器之前整合一個 JWT 過濾器,以處理和驗證請求中的 JWT。
authenticationProvider():
建立一個使用資料庫處理使用者身份驗證的身份驗證提供程式。該提供程式會檢索使用者詳細資訊,並使用指定的編碼器驗證密碼。
passwordEncoder():
建立密碼編碼方法,預設使用 BCrypt 演算法,該演算法在儲存密碼前會對密碼進行安全雜湊。
authenticationManager(AuthenticationConfiguration config):身份驗證管理器:
提供一個管理器來檢視身份驗證過程,這對處理身份驗證請求至關重要,也是 Spring Security 身份驗證框架的核心。