JWT?+?Spring?Security?/?OAuth2.0:微服務(wù)統(tǒng)一登錄、鑒權(quán)、單點(diǎn)登錄全解析

在微服務(wù)架構(gòu)中,服務(wù)被拆分為多個(gè)獨(dú)立部署的節(jié)點(diǎn),跨服務(wù)訪問(wèn)、用戶身份統(tǒng)一管理成為核心痛點(diǎn)——用戶在每個(gè)服務(wù)都需單獨(dú)登錄、權(quán)限無(wú)法統(tǒng)一管控、多系統(tǒng)切換頻繁登錄,這些問(wèn)題不僅影響用戶體驗(yàn),更會(huì)帶來(lái)嚴(yán)重的安全隱患。
一、微服務(wù)身份認(rèn)證與鑒權(quán)的核心痛點(diǎn)
在單體應(yīng)用中,我們通常通過(guò)Session存儲(chǔ)用戶身份信息,實(shí)現(xiàn)登錄與鑒權(quán),但這種方式在微服務(wù)架構(gòu)中完全失效,核心痛點(diǎn)集中在3點(diǎn):
1.1 Session共享問(wèn)題
單體應(yīng)用中,Session存儲(chǔ)在服務(wù)器內(nèi)存,微服務(wù)中多個(gè)服務(wù)部署在不同節(jié)點(diǎn),Session無(wú)法跨服務(wù)共享,導(dǎo)致用戶在A服務(wù)登錄后,訪問(wèn)B服務(wù)仍需重新登錄,體驗(yàn)極差。
1.2 權(quán)限管控分散
每個(gè)微服務(wù)單獨(dú)維護(hù)一套權(quán)限規(guī)則,無(wú)法實(shí)現(xiàn)統(tǒng)一的角色、資源管控,不僅開(kāi)發(fā)冗余,更易出現(xiàn)權(quán)限漏洞(如某服務(wù)遺漏權(quán)限校驗(yàn)、權(quán)限規(guī)則不一致)。
1.3 多系統(tǒng)單點(diǎn)登錄需求
企業(yè)通常有多個(gè)關(guān)聯(lián)系統(tǒng)(如電商系統(tǒng)、后臺(tái)管理系統(tǒng)、APP接口),用戶希望一次登錄,即可訪問(wèn)所有授權(quán)系統(tǒng),無(wú)需重復(fù)輸入賬號(hào)密碼,這就需要單點(diǎn)登錄(SSO)能力。
而JWT + Spring Security + OAuth2.0的組合,正是解決上述痛點(diǎn)的最優(yōu)解:JWT實(shí)現(xiàn)無(wú)狀態(tài)令牌傳輸,Spring Security實(shí)現(xiàn)權(quán)限管控,OAuth2.0實(shí)現(xiàn)授權(quán)與單點(diǎn)登錄,三者協(xié)同,構(gòu)建微服務(wù)統(tǒng)一身份認(rèn)證與鑒權(quán)體系。
二、JWT、Spring Security、OAuth2.0
2.1 JWT:無(wú)狀態(tài)令牌,解決Session共享難題
JWT(JSON Web Token)是一種輕量級(jí)的令牌規(guī)范,核心作用是在客戶端與服務(wù)器之間安全地傳輸用戶身份信息,采用無(wú)狀態(tài)設(shè)計(jì),無(wú)需在服務(wù)器存儲(chǔ)Session,完美適配微服務(wù)架構(gòu)。
2.1.1 JWT核心結(jié)構(gòu)(3部分,用點(diǎn)號(hào)分隔)
JWT令牌由 Header(頭部)、Payload(載荷)、Signature(簽名) 三部分組成,示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MTUyODc2MDAsImlhdCI6MTcxNTI4NDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header(頭部):指定JWT的簽名算法和令牌類型,默認(rèn)算法為HS256(HMAC SHA256),示例:
{
??"alg": "HS256", // 簽名算法
??"typ": "JWT" // 令牌類型
}- Payload(載荷):存儲(chǔ)用戶核心信息(如用戶ID、用戶名、角色)和令牌過(guò)期時(shí)間,分為標(biāo)準(zhǔn)聲明和自定義聲明:
{ "userId": 1, // 自定義聲明:用戶ID "userName": "admin", // 自定義聲明:用戶名 "role": "ADMIN", // 自定義聲明:角色 "exp": 1715287600, // 標(biāo)準(zhǔn)聲明:過(guò)期時(shí)間 "iat": 1715284000 // 標(biāo)準(zhǔn)聲明:簽發(fā)時(shí)間 }- 標(biāo)準(zhǔn)聲明(可選,但推薦使用):
- sub:令牌面向的用戶
- iat:令牌簽發(fā)時(shí)間
- exp:令牌過(guò)期時(shí)間(時(shí)間戳,單位毫秒)
- iss:令牌簽發(fā)者
- 自定義聲明:根據(jù)業(yè)務(wù)需求添加,如用戶ID(userId)、角色(role)、權(quán)限(permissions)等,注意:Payload不加密,不能存儲(chǔ)敏感信息(如密碼)。
- Signature(簽名):核心安全保障,通過(guò)Header指定的算法,將Header(Base64編碼)、Payload(Base64編碼)和密鑰(secret)進(jìn)行加密,生成簽名。服務(wù)器接收令牌后,會(huì)重新計(jì)算簽名,若與令牌中的簽名不一致,則說(shuō)明令牌被篡改,直接拒絕訪問(wèn)。 簽名計(jì)算公式:
HMACSHA256( Base64Encode(Header) + "." + Base64Encode(Payload), secret )
2.1.2 JWT核心優(yōu)勢(shì)與注意事項(xiàng)
- 優(yōu)勢(shì):
- 無(wú)狀態(tài):服務(wù)器無(wú)需存儲(chǔ)Session,僅通過(guò)令牌即可驗(yàn)證用戶身份,減輕服務(wù)器壓力,適配微服務(wù)集群部署;
- 跨語(yǔ)言:基于JSON格式,支持所有語(yǔ)言(Java、Python、Go等),適配多端(Web、APP、小程序);
- 自包含:Payload中包含用戶核心信息,無(wú)需頻繁查詢數(shù)據(jù)庫(kù),提升接口響應(yīng)速度;
- 可擴(kuò)展:支持自定義聲明,適配不同業(yè)務(wù)場(chǎng)景的身份信息傳輸需求。
- 注意事項(xiàng):
- Payload不加密:嚴(yán)禁存儲(chǔ)敏感信息(如密碼、手機(jī)號(hào)),僅存儲(chǔ)非敏感的用戶標(biāo)識(shí)和權(quán)限信息;
- 密鑰安全:簽名密鑰(secret)必須妥善保管,一旦泄露,攻擊者可偽造令牌,引發(fā)安全風(fēng)險(xiǎn);
- 令牌過(guò)期:必須設(shè)置合理的過(guò)期時(shí)間(如1小時(shí)),過(guò)期后需重新登錄獲取新令牌;
- 無(wú)法撤銷:JWT令牌一旦簽發(fā),在過(guò)期前無(wú)法主動(dòng)撤銷(除非結(jié)合Redis黑名單機(jī)制)。
2.2 Spring Security:微服務(wù)權(quán)限管控核心框架
Spring Security是Spring生態(tài)中成熟的權(quán)限管理框架,核心作用是實(shí)現(xiàn)用戶認(rèn)證(登錄校驗(yàn))和授權(quán)(權(quán)限管控),提供了完善的安全防護(hù)機(jī)制(如CSRF防護(hù)、XSS防護(hù)、會(huì)話管理),可無(wú)縫整合JWT和OAuth2.0,是微服務(wù)權(quán)限管控的首選框架。
2.2.1 Spring Security核心概念
- 認(rèn)證(Authentication):驗(yàn)證用戶身份的合法性(如賬號(hào)密碼是否正確),認(rèn)證通過(guò)后,生成認(rèn)證信息(Authentication對(duì)象),存儲(chǔ)在SecurityContext中。
- 授權(quán)(Authorization):驗(yàn)證用戶是否擁有訪問(wèn)某個(gè)資源的權(quán)限(如普通用戶能否訪問(wèn)管理員接口),核心是“資源-角色-用戶”的關(guān)聯(lián)關(guān)系。
- SecurityContext:存儲(chǔ)用戶認(rèn)證信息的上下文,線程安全,可通過(guò)
SecurityContextHolder.getContext()獲取當(dāng)前登錄用戶信息。 - UserDetailsService:核心接口,用于加載用戶信息(如從數(shù)據(jù)庫(kù)查詢用戶賬號(hào)、密碼、角色),是認(rèn)證流程的核心組件。
- PasswordEncoder:密碼加密器,用于對(duì)用戶密碼進(jìn)行加密存儲(chǔ)(如BCrypt加密),避免明文存儲(chǔ)密碼,提升安全性。
- FilterChain:安全過(guò)濾器鏈,Spring Security通過(guò)一系列過(guò)濾器(如UsernamePasswordAuthenticationFilter、JwtAuthenticationFilter)處理請(qǐng)求,完成認(rèn)證和授權(quán)。
2.2.2 Spring Security核心流程(認(rèn)證+授權(quán))
- 用戶發(fā)起登錄請(qǐng)求(如POST /login),攜帶賬號(hào)密碼;
- UsernamePasswordAuthenticationFilter攔截請(qǐng)求,將賬號(hào)密碼封裝為Authentication對(duì)象;
- 調(diào)用AuthenticationManager(認(rèn)證管理器),觸發(fā)認(rèn)證流程;
- AuthenticationManager調(diào)用UserDetailsService,加載數(shù)據(jù)庫(kù)中的用戶信息(UserDetails);
- PasswordEncoder對(duì)比用戶提交的密碼與數(shù)據(jù)庫(kù)中加密后的密碼,驗(yàn)證是否一致;
- 認(rèn)證通過(guò):生成包含用戶信息和權(quán)限的Authentication對(duì)象,存入SecurityContext;
- 認(rèn)證失?。簰伋霎惓?,返回登錄失敗提示;
- 用戶訪問(wèn)受保護(hù)資源時(shí),F(xiàn)ilterSecurityInterceptor攔截請(qǐng)求,校驗(yàn)當(dāng)前用戶是否擁有該資源的訪問(wèn)權(quán)限;
- 授權(quán)通過(guò):允許訪問(wèn)資源;授權(quán)失?。悍祷?03 Forbidden。
2.3 OAuth2.0:授權(quán)協(xié)議,實(shí)現(xiàn)單點(diǎn)登錄與第三方授權(quán)
OAuth2.0是一種開(kāi)放的授權(quán)協(xié)議,核心作用是實(shí)現(xiàn)“第三方授權(quán)”和“單點(diǎn)登錄(SSO)”,允許用戶通過(guò)一個(gè)賬號(hào)(如微信、QQ)登錄多個(gè)關(guān)聯(lián)系統(tǒng),無(wú)需重復(fù)注冊(cè)和登錄,同時(shí)避免用戶將核心賬號(hào)密碼泄露給第三方系統(tǒng)。
注意:OAuth2.0是授權(quán)協(xié)議,不是認(rèn)證協(xié)議,它的核心是“授權(quán)”——用戶授權(quán)第三方系統(tǒng)訪問(wèn)自己的資源(如微信授權(quán)某APP獲取用戶昵稱、頭像),而認(rèn)證是驗(yàn)證用戶身份的過(guò)程(如微信登錄時(shí)驗(yàn)證賬號(hào)密碼)。
2.3.1 OAuth2.0核心角色
- 資源所有者(Resource Owner):用戶,擁有資源的所有權(quán)(如微信用戶擁有自己的昵稱、頭像等資源)。
- 客戶端(Client):需要獲取用戶資源的應(yīng)用(如某APP、某網(wǎng)站),需提前在授權(quán)服務(wù)器注冊(cè),獲取客戶端ID(client_id)和客戶端密鑰(client_secret)。
- 授權(quán)服務(wù)器(Authorization Server):負(fù)責(zé)驗(yàn)證用戶身份、頒發(fā)授權(quán)令牌(如access_token),是OAuth2.0的核心組件(如微信授權(quán)服務(wù)器)。
- 資源服務(wù)器(Resource Server):存儲(chǔ)用戶資源的服務(wù)器(如微信的用戶信息服務(wù)器),客戶端通過(guò)授權(quán)令牌訪問(wèn)資源服務(wù)器,獲取用戶資源。
2.3.2 OAuth2.0核心授權(quán)流程(通用流程)
- 客戶端(APP)引導(dǎo)用戶跳轉(zhuǎn)到授權(quán)服務(wù)器,請(qǐng)求用戶授權(quán);
- 用戶驗(yàn)證身份(如登錄微信),并同意授權(quán)客戶端訪問(wèn)自己的資源;
- 授權(quán)服務(wù)器頒發(fā)授權(quán)碼(code)給客戶端;
- 客戶端攜帶授權(quán)碼(code)、客戶端ID、客戶端密鑰,向授權(quán)服務(wù)器請(qǐng)求訪問(wèn)令牌(access_token);
- 授權(quán)服務(wù)器驗(yàn)證信息無(wú)誤后,頒發(fā)access_token(訪問(wèn)令牌)和refresh_token(刷新令牌)給客戶端;
- 客戶端攜帶access_token,向資源服務(wù)器請(qǐng)求訪問(wèn)用戶資源;
- 資源服務(wù)器驗(yàn)證access_token的合法性,驗(yàn)證通過(guò)后,返回用戶資源給客戶端。
2.3.3 OAuth2.0 4種授權(quán)模式(重點(diǎn)掌握2種)
OAuth2.0提供4種授權(quán)模式,適配不同的業(yè)務(wù)場(chǎng)景,其中授權(quán)碼模式和密碼模式是微服務(wù)中最常用的兩種。
- 授權(quán)碼模式(Authorization Code):
- 特點(diǎn):最安全、最常用的模式,通過(guò)授權(quán)碼獲取access_token,避免直接傳遞賬號(hào)密碼,適合Web應(yīng)用、APP等場(chǎng)景;
- 適用場(chǎng)景:?jiǎn)吸c(diǎn)登錄(SSO)、第三方授權(quán)(如APP用微信登錄);
- 核心優(yōu)勢(shì):安全性高,授權(quán)碼僅短期有效,且客戶端無(wú)需存儲(chǔ)用戶賬號(hào)密碼。
- 密碼模式(Password):
- 特點(diǎn):用戶直接向客戶端提供賬號(hào)密碼,客戶端攜帶賬號(hào)密碼向授權(quán)服務(wù)器請(qǐng)求access_token;
- 適用場(chǎng)景:微服務(wù)內(nèi)部系統(tǒng)(如后臺(tái)管理系統(tǒng)),客戶端與授權(quán)服務(wù)器屬于同一信任體系,且用戶信任客戶端;
- 注意:安全性較低,僅適用于內(nèi)部信任場(chǎng)景,嚴(yán)禁用于第三方授權(quán)。
- 簡(jiǎn)化模式(Implicit):無(wú)需授權(quán)碼,直接頒發(fā)access_token,安全性低,僅適用于純前端應(yīng)用(如Vue、React),不推薦生產(chǎn)使用。
- 客戶端憑證模式(Client Credentials):客戶端通過(guò)自身的client_id和client_secret獲取access_token,無(wú)需用戶參與,適用于服務(wù)間通信(如微服務(wù)A調(diào)用微服務(wù)B)。
2.3.4 OAuth2.0核心令牌
- access_token(訪問(wèn)令牌):用于訪問(wèn)資源服務(wù)器的令牌,短期有效(如1小時(shí)),過(guò)期后需重新獲取;
- refresh_token(刷新令牌):用于在access_token過(guò)期后,無(wú)需重新登錄,直接獲取新的access_token,長(zhǎng)期有效(如7天);
- 授權(quán)碼(code):用于獲取access_token的臨時(shí)憑證,短期有效(如5分鐘),一次使用后失效。
三、實(shí)操落地:Spring Security + JWT 實(shí)現(xiàn)微服務(wù)統(tǒng)一登錄與鑒權(quán)
先實(shí)現(xiàn)最基礎(chǔ)的“統(tǒng)一登錄與鑒權(quán)”:基于Spring Security + JWT,實(shí)現(xiàn)用戶登錄生成JWT令牌,后續(xù)請(qǐng)求攜帶令牌完成身份驗(yàn)證和權(quán)限管控,適配微服務(wù)架構(gòu)(無(wú)狀態(tài))。
3.1 第一步:導(dǎo)入依賴(Maven)
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT依賴 -->
<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>
<!-- 數(shù)據(jù)庫(kù)依賴(模擬用戶數(shù)據(jù),可替換為MySQL) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>3.2 第二步:配置JWT工具類(生成令牌、驗(yàn)證令牌)
核心工具類,負(fù)責(zé)JWT令牌的生成、解析、驗(yàn)證,封裝通用方法,便于后續(xù)調(diào)用。
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
// JWT簽名密鑰(生產(chǎn)環(huán)境需配置在配置中心,如Nacos,嚴(yán)禁硬編碼)
@Value("${jwt.secret}")
private String secret;
// JWT過(guò)期時(shí)間(單位:毫秒,此處配置1小時(shí))
@Value("${jwt.expiration}")
private long expiration;
// 從令牌中獲取用戶名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 從令牌中獲取過(guò)期時(shí)間
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 從令牌中獲取自定義聲明
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 解析令牌,獲取所有聲明(需驗(yàn)證簽名)
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secret.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
}
// 判斷令牌是否過(guò)期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成JWT令牌(基于用戶信息)
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 自定義聲明:添加用戶角色(可根據(jù)需求添加更多信息)
claims.put("roles", userDetails.getAuthorities());
return doGenerateToken(claims, userDetails.getUsername());
}
// 生成令牌核心方法
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims) // 自定義聲明
.setSubject(subject) // 用戶名(唯一標(biāo)識(shí))
.setIssuedAt(new Date(System.currentTimeMillis())) // 簽發(fā)時(shí)間
.setExpiration(new Date(System.currentTimeMillis() + expiration)) // 過(guò)期時(shí)間
.signWith(SignatureAlgorithm.HS256, secret.getBytes()) // 簽名算法+密鑰
.compact();
}
// 驗(yàn)證令牌(驗(yàn)證簽名、過(guò)期時(shí)間、用戶名匹配)
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}3.3 第三步:配置Spring Security核心配置
核心配置類,用于配置認(rèn)證流程、授權(quán)規(guī)則、JWT過(guò)濾器等,替代默認(rèn)的Session認(rèn)證。
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.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.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;
@Configuration
@EnableWebSecurity // 啟用Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 啟用方法級(jí)別的權(quán)限控制
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// 密碼加密器(BCrypt加密,不可逆,安全性高)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 認(rèn)證提供者(關(guān)聯(lián)UserDetailsService和PasswordEncoder)
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// 認(rèn)證管理器(核心認(rèn)證組件)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
// 核心安全配置(配置授權(quán)規(guī)則、過(guò)濾器、會(huì)話管理等)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 關(guān)閉CSRF防護(hù)(微服務(wù)中JWT無(wú)狀態(tài),無(wú)需CSRF)
.csrf().disable()
// 配置未認(rèn)證請(qǐng)求的處理方式(返回401)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
// 配置會(huì)話管理:無(wú)狀態(tài)(不創(chuàng)建Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 配置授權(quán)規(guī)則
.authorizeRequests()
// 登錄接口、注冊(cè)接口允許匿名訪問(wèn)
.antMatchers("/api/auth/login", "/api/auth/register").permitAll()
// 靜態(tài)資源允許匿名訪問(wèn)
.antMatchers("/static/**", "/swagger-ui/**").permitAll()
// 管理員接口僅允許ADMIN角色訪問(wèn)
.antMatchers("/api/admin/**").hasRole("ADMIN")
// 普通用戶接口允許USER或ADMIN角色訪問(wèn)
.antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
// 其他所有請(qǐng)求都需要認(rèn)證
.anyRequest().authenticated();
// 注冊(cè)認(rèn)證提供者
http.authenticationProvider(authenticationProvider());
// 添加JWT過(guò)濾器(在用戶名密碼過(guò)濾器之前執(zhí)行,先驗(yàn)證令牌)
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}3.4 第四步:實(shí)現(xiàn)JWT過(guò)濾器與認(rèn)證異常處理
JWT過(guò)濾器負(fù)責(zé)攔截所有請(qǐng)求,提取請(qǐng)求頭中的JWT令牌,驗(yàn)證令牌合法性,若驗(yàn)證通過(guò),將用戶信息存入SecurityContext,實(shí)現(xiàn)無(wú)狀態(tài)認(rèn)證。
4.1 JWT過(guò)濾器(JwtAuthenticationFilter)
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.stereotype.Component;
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;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
// 攔截請(qǐng)求,驗(yàn)證JWT令牌
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 從請(qǐng)求頭中提取JWT令牌(請(qǐng)求頭格式:Authorization: Bearer <token>)
String jwt = getJwtFromRequest(request);
// 2. 驗(yàn)證令牌是否存在且有效
if (jwt != null && !jwt.isEmpty() && jwtTokenUtil.validateToken(jwt, userDetailsService.loadUserByUsername(jwtTokenUtil.getUsernameFromToken(jwt)))) {
// 3. 從令牌中獲取用戶名,加載用戶信息
String username = jwtTokenUtil.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. 創(chuàng)建認(rèn)證對(duì)象,存入SecurityContext
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("無(wú)法設(shè)置用戶認(rèn)證信息: {}", e);
}
// 繼續(xù)執(zhí)行過(guò)濾器鏈
filterChain.doFilter(request, response);
}
// 從請(qǐng)求頭中提取JWT令牌
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // 截取Bearer后面的令牌部分
}
return null;
}
}4.2 認(rèn)證異常處理(JwtAuthenticationEntryPoint)
當(dāng)令牌無(wú)效、過(guò)期或未攜帶令牌時(shí),返回統(tǒng)一的401響應(yīng),替代默認(rèn)的登錄頁(yè)面。
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
// 未認(rèn)證請(qǐng)求的處理邏輯
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
// 設(shè)置響應(yīng)狀態(tài)碼401,返回JSON格式的錯(cuò)誤信息
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"code\":401,\"message\":\"未認(rèn)證,請(qǐng)先登錄獲取令牌\"}");
}
}3.5 第五步:實(shí)現(xiàn)UserDetailsService(加載用戶信息)
自定義UserDetailsService,從數(shù)據(jù)庫(kù)中加載用戶賬號(hào)、密碼、角色信息,適配Spring Security的認(rèn)證流程。
5.1 實(shí)體類(User)
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@Entity
@Table(name = "sys_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username; // 用戶名(唯一)
@Column(nullable = false)
private String password; // 加密后的密碼
private String role; // 角色(如ADMIN、USER)
// 實(shí)現(xiàn)UserDetails接口方法:獲取用戶權(quán)限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// 將角色轉(zhuǎn)換為Spring Security認(rèn)可的權(quán)限格式(ROLE_前綴)
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
return authorities;
}
// 實(shí)現(xiàn)UserDetails接口方法:賬號(hào)是否未過(guò)期(默認(rèn)true)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 實(shí)現(xiàn)UserDetails接口方法:賬號(hào)是否未鎖定(默認(rèn)true)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 實(shí)現(xiàn)UserDetails接口方法:憑證是否未過(guò)期(默認(rèn)true)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 實(shí)現(xiàn)UserDetails接口方法:賬號(hào)是否啟用(默認(rèn)true)
@Override
public boolean isEnabled() {
return true;
}
}5.2 UserRepository(數(shù)據(jù)庫(kù)操作)
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 根據(jù)用戶名查詢用戶(Spring Security認(rèn)證核心方法)
Optional<User> findByUsername(String username);
}5.3 自定義UserDetailsService實(shí)現(xiàn)
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.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// 加載用戶信息(根據(jù)用戶名)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數(shù)據(jù)庫(kù)查詢用戶,若不存在則拋出異常
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用戶不存在:" + username));
// 返回User對(duì)象(已實(shí)現(xiàn)UserDetails接口)
return user;
}
}3.6 第六步:實(shí)現(xiàn)登錄接口(生成JWT令牌)
自定義登錄接口,接收用戶賬號(hào)密碼,完成認(rèn)證后,生成JWT令牌并返回給客戶端。
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.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
// 登錄接口:接收賬號(hào)密碼,返回JWT令牌
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest loginRequest) {
// 1. 執(zhí)行認(rèn)證(驗(yàn)證賬號(hào)密碼)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 2. 認(rèn)證通過(guò),將認(rèn)證信息存入SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 加載用戶信息,生成JWT令牌
UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
String jwt = jwtTokenUtil.generateToken(userDetails);
// 4. 返回令牌和用戶信息
Map<String, String> response = new HashMap<>();
response.put("token", jwt);
response.put("username", userDetails.getUsername());
response.put("role", userDetails.getAuthorities().iterator().next().getAuthority().replace("ROLE_", ""));
return ResponseEntity.ok(response);
}
// 注冊(cè)接口(可選,用于測(cè)試)
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody RegisterRequest registerRequest) {
// 檢查用戶名是否已存在
if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) {
return ResponseEntity.badRequest().body("用戶名已存在");
}
// 創(chuàng)建用戶,加密密碼
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setRole(registerRequest.getRole()); // 如"USER"、"ADMIN"
userRepository.save(user);
return ResponseEntity.ok("注冊(cè)成功");
}
// 登錄請(qǐng)求參數(shù)封裝
public static class LoginRequest {
private String username;
private String password;
// getter/setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// 注冊(cè)請(qǐng)求參數(shù)封裝
public static class RegisterRequest {
private String username;
private String password;
private String role;
// getter/setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
}3.7 第七步:配置文件(application.yml)
spring:
# 數(shù)據(jù)庫(kù)配置(H2內(nèi)存數(shù)據(jù)庫(kù),用于測(cè)試)
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: 123456
# JPA配置
jpa:
hibernate:
ddl-auto: update # 自動(dòng)創(chuàng)建表結(jié)構(gòu)
show-sql: true
properties:
hibernate:
format_sql: true
# H2控制臺(tái)配置(訪問(wèn):http://localhost:8080/h2-console)
h2:
console:
enabled: true
path: /h2-console
# JWT配置
jwt:
secret: abc1234567890abc1234567890abc1234 # 簽名密鑰(生產(chǎn)環(huán)境需修改,建議至少32位)
expiration: 3600000 # 過(guò)期時(shí)間(1小時(shí),單位:毫秒)
# 服務(wù)器端口
server:
port: 80803.8 測(cè)試驗(yàn)證
- 啟動(dòng)項(xiàng)目,訪問(wèn) http://localhost:8080/h2-console,登錄H2數(shù)據(jù)庫(kù),插入測(cè)試用戶:
INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密碼:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密碼:123456- 調(diào)用注冊(cè)接口(可選):POST http://localhost:8080/api/auth/register,請(qǐng)求體: { "username": "test", "password": "123456", "role": "USER" }
- 調(diào)用登錄接口:
POST http://localhost:8080/api/auth/login,請(qǐng)求體: { "username": "admin", "password": "123456" }響應(yīng)結(jié)果(包含JWT令牌): { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9XSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", "username": "admin", "role": "ADMIN" } - 訪問(wèn)受保護(hù)接口(如管理員接口):GET http://localhost:8080/api/admin/test,請(qǐng)求頭添加Authorization: Bearer 令牌,即可正常訪問(wèn);若不攜帶令牌或令牌無(wú)效,返回401。
四、OAuth2.0 + JWT + Spring Security 實(shí)現(xiàn)單點(diǎn)登錄(SSO)
前面實(shí)現(xiàn)了單個(gè)微服務(wù)的登錄與鑒權(quán),而微服務(wù)架構(gòu)中通常有多個(gè)服務(wù)(如訂單服務(wù)、用戶服務(wù)、后臺(tái)管理服務(wù)),需要實(shí)現(xiàn)“單點(diǎn)登錄”——用戶一次登錄,即可訪問(wèn)所有授權(quán)服務(wù)。
核心方案:基于 OAuth2.0 授權(quán)碼模式,搭建獨(dú)立的授權(quán)服務(wù)器(統(tǒng)一處理登錄、頒發(fā)令牌)和資源服務(wù)器(各微服務(wù)),結(jié)合JWT實(shí)現(xiàn)無(wú)狀態(tài)單點(diǎn)登錄。
4.1 架構(gòu)設(shè)計(jì)(核心組件)
- 授權(quán)服務(wù)器(Authorization Server):獨(dú)立部署,負(fù)責(zé)用戶認(rèn)證、頒發(fā)JWT令牌(access_token、refresh_token)、處理授權(quán)請(qǐng)求,是單點(diǎn)登錄的核心。
- 資源服務(wù)器(Resource Server):各個(gè)微服務(wù)(如訂單服務(wù)、用戶服務(wù)),配置OAuth2.0和JWT,驗(yàn)證令牌合法性,實(shí)現(xiàn)權(quán)限管控。
- 客戶端(Client):需要接入單點(diǎn)登錄的應(yīng)用(如Web后臺(tái)、APP、小程序),提前在授權(quán)服務(wù)器注冊(cè)。
4.2 第一步:搭建授權(quán)服務(wù)器(Authorization Server)
基于Spring Security OAuth2.0,搭建獨(dú)立的授權(quán)服務(wù)器,實(shí)現(xiàn)用戶登錄、授權(quán)碼頒發(fā)、JWT令牌生成。
2.1 導(dǎo)入依賴(Maven)
<!-- 新增OAuth2.0授權(quán)服務(wù)器依賴 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.8.RELEASE</version>
</dependency>
<!-- 其他依賴(Spring Boot Web、Spring Security、JWT、數(shù)據(jù)庫(kù))同上 -->2.2 配置授權(quán)服務(wù)器(AuthorizationServerConfig)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableAuthorizationServer // 啟用授權(quán)服務(wù)器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 客戶端ID(提前注冊(cè),用于客戶端身份驗(yàn)證)
private static final String CLIENT_ID = "client1";
// 客戶端密鑰(加密存儲(chǔ),密碼:123456)
private static final String CLIENT_SECRET = "$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6";
// 授權(quán)范圍
private static final String SCOPE = "all";
// 授權(quán)碼模式
private static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
// 密碼模式
private static final String GRANT_TYPE_PASSWORD = "password";
// 刷新令牌模式
private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
// 令牌有效期(1小時(shí))
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 3600;
// 刷新令牌有效期(7天)
private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 604800;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
// JWT簽名密鑰(與資源服務(wù)器、JWT工具類一致,生產(chǎn)環(huán)境配置在配置中心)
private static final String JWT_SECRET = "abc1234567890abc1234567890abc1234";
// 配置令牌存儲(chǔ)(JWT),用于存儲(chǔ)和解析JWT令牌
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
// 配置JWT令牌轉(zhuǎn)換器(設(shè)置簽名密鑰,確保令牌生成和驗(yàn)證的一致性)
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(JWT_SECRET); // 與JWT工具類的密鑰完全一致
return converter;
}
// 配置客戶端信息(客戶端在授權(quán)服務(wù)器注冊(cè)的核心信息,用于客戶端身份校驗(yàn))
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客戶端ID(唯一標(biāo)識(shí),客戶端需攜帶此ID請(qǐng)求授權(quán))
.withClient(CLIENT_ID)
// 客戶端密鑰(加密存儲(chǔ),客戶端請(qǐng)求時(shí)需攜帶加密前的密鑰進(jìn)行校驗(yàn))
.secret(CLIENT_SECRET)
// 授權(quán)范圍,用于限制客戶端可訪問(wèn)的資源范圍
.scopes(SCOPE)
// 支持的授權(quán)模式,此處兼容授權(quán)碼模式(SSO核心)、密碼模式(內(nèi)部系統(tǒng))、刷新令牌模式
.authorizedGrantTypes(GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_PASSWORD, GRANT_TYPE_REFRESH_TOKEN)
// 訪問(wèn)令牌有效期(1小時(shí),避免令牌長(zhǎng)期有效帶來(lái)的安全風(fēng)險(xiǎn))
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
// 刷新令牌有效期(7天,用戶無(wú)需頻繁登錄,提升體驗(yàn))
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS)
// 回調(diào)地址(授權(quán)碼模式必填,授權(quán)服務(wù)器頒發(fā)授權(quán)碼后,跳轉(zhuǎn)至此地址傳遞授權(quán)碼)
.redirectUris("http://localhost:8081/callback");
}
// 配置授權(quán)服務(wù)器端點(diǎn)(核心組件關(guān)聯(lián),確保認(rèn)證和令牌生成流程正常)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 關(guān)聯(lián)認(rèn)證管理器(用于密碼模式,驗(yàn)證用戶賬號(hào)密碼合法性)
.authenticationManager(authenticationManager)
// 關(guān)聯(lián)令牌存儲(chǔ)(JWT),用于存儲(chǔ)和讀取令牌信息
.tokenStore(tokenStore())
// 關(guān)聯(lián)令牌轉(zhuǎn)換器(JWT),用于生成和解析JWT令牌
.accessTokenConverter(accessTokenConverter());
}
// 配置授權(quán)服務(wù)器安全規(guī)則(控制授權(quán)服務(wù)器端點(diǎn)的訪問(wèn)權(quán)限)
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 允許所有客戶端訪問(wèn)token_key端點(diǎn)(獲取JWT簽名公鑰,用于資源服務(wù)器驗(yàn)證令牌)
.tokenKeyAccess("permitAll()")
// 允許所有客戶端訪問(wèn)check_token端點(diǎn)(驗(yàn)證令牌的合法性,資源服務(wù)器會(huì)調(diào)用此端點(diǎn))
.checkTokenAccess("permitAll()")
// 允許客戶端通過(guò)表單認(rèn)證(用于客戶端身份校驗(yàn),簡(jiǎn)化客戶端請(qǐng)求流程)
.allowFormAuthenticationForClients();
}
}4.3 授權(quán)服務(wù)器配套配置(完善認(rèn)證流程)
授權(quán)服務(wù)器需依賴前文實(shí)現(xiàn)的UserDetailsService、PasswordEncoder等組件,同時(shí)補(bǔ)充Spring Security配置(避免默認(rèn)登錄頁(yè)面干擾),確保認(rèn)證流程正常。
4.3.1 授權(quán)服務(wù)器Spring Security配置(SecurityConfig)
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.dao.DaoAuthenticationProvider;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
// 密碼加密器(與前文一致,BCrypt不可逆加密,確保密碼安全)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 認(rèn)證提供者(關(guān)聯(lián)UserDetailsService和PasswordEncoder,用于加載用戶信息并校驗(yàn)密碼)
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
// 認(rèn)證管理器(核心認(rèn)證組件,授權(quán)服務(wù)器密碼模式需依賴此組件)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
// 安全過(guò)濾器鏈配置(關(guān)閉Session,允許授權(quán)相關(guān)端點(diǎn)匿名訪問(wèn))
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 關(guān)閉CSRF防護(hù)(授權(quán)服務(wù)器無(wú)狀態(tài),無(wú)需CSRF)
.csrf().disable()
// 關(guān)閉Session(授權(quán)服務(wù)器無(wú)需存儲(chǔ)用戶會(huì)話,適配微服務(wù)無(wú)狀態(tài)架構(gòu))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 授權(quán)規(guī)則配置
.authorizeRequests()
// 授權(quán)服務(wù)器核心端點(diǎn)允許匿名訪問(wèn)(客戶端請(qǐng)求授權(quán)、獲取令牌需訪問(wèn)這些端點(diǎn))
.antMatchers("/oauth/authorize", "/oauth/token", "/oauth/check_token", "/oauth/token_key").permitAll()
// 登錄接口、注冊(cè)接口允許匿名訪問(wèn)(用于用戶注冊(cè)和登錄驗(yàn)證)
.antMatchers("/api/auth/login", "/api/auth/register").permitAll()
// 其他所有請(qǐng)求需認(rèn)證(避免未授權(quán)訪問(wèn))
.anyRequest().authenticated();
// 注冊(cè)認(rèn)證提供者
http.authenticationProvider(authenticationProvider());
return http.build();
}
}4.3.2 授權(quán)服務(wù)器配置文件(application.yml)
配置端口、數(shù)據(jù)庫(kù)、JWT等信息,與前文保持一致,確保組件協(xié)同工作:
spring:
# 數(shù)據(jù)庫(kù)配置(H2內(nèi)存數(shù)據(jù)庫(kù),用于測(cè)試,生產(chǎn)環(huán)境替換為MySQL)
datasource:
url: jdbc:h2:mem:authdb
driver-class-name: org.h2.Driver
username: sa
password: 123456
# JPA配置(自動(dòng)創(chuàng)建表結(jié)構(gòu),簡(jiǎn)化測(cè)試)
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# H2控制臺(tái)配置(訪問(wèn):http://localhost:8080/h2-console,用于插入測(cè)試用戶)
h2:
console:
enabled: true
path: /h2-console
# JWT配置(與資源服務(wù)器、令牌轉(zhuǎn)換器一致)
jwt:
secret: abc1234567890abc1234567890abc1234 # 簽名密鑰,生產(chǎn)環(huán)境需修改并配置在配置中心
expiration: 3600000 # 訪問(wèn)令牌有效期(1小時(shí),與授權(quán)服務(wù)器配置一致)
# 服務(wù)器端口(授權(quán)服務(wù)器獨(dú)立部署,端口設(shè)為8080,避免與資源服務(wù)器沖突)
server:
port: 80804.4 授權(quán)服務(wù)器測(cè)試驗(yàn)證
啟動(dòng)授權(quán)服務(wù)器,完成以下測(cè)試,確保授權(quán)服務(wù)器可正常頒發(fā)授權(quán)碼和JWT令牌:
- 啟動(dòng)項(xiàng)目,訪問(wèn)H2控制臺(tái)(http://localhost:8080/h2-console),登錄后插入測(cè)試用戶(與前文一致):
INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密碼:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密碼:123456- 請(qǐng)求授權(quán)碼(授權(quán)碼模式):訪問(wèn)以下地址,引導(dǎo)用戶登錄并授權(quán),獲取授權(quán)碼(code): http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all
- 訪問(wèn)后會(huì)跳轉(zhuǎn)至登錄頁(yè)面,輸入用戶名(admin)和密碼(123456);
- 登錄成功后,會(huì)跳轉(zhuǎn)至回調(diào)地址(http://localhost:8081/callback),地址欄會(huì)攜帶授權(quán)碼(code參數(shù)),例如:http://localhost:8081/callback?code=abc123(code為臨時(shí)憑證,5分鐘內(nèi)有效)。
- 通過(guò)授權(quán)碼獲取訪問(wèn)令牌(access_token):使用Postman發(fā)送POST請(qǐng)求,地址:http://localhost:8080/oauth/token,參數(shù)如下:
- 請(qǐng)求頭:添加Authorization: Basic Y2xpZW50MToxMjM0NTY=(client1:123456的Base64編碼);
- 請(qǐng)求體(form-data): grant_type=authorization_code、code=步驟2獲取的授權(quán)碼、redirect_uri=http://localhost:8081/callback、scope=all;
- 響應(yīng)結(jié)果:會(huì)返回access_token(JWT令牌)、refresh_token、token_type等信息,示例:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjoidXNlciIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjoidXNlciIsImF0aSI6ImFiYzEyMyIsImV4cCI6MTcxNTg5MjgwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8", "expires_in": 3599, "scope": "all" }
五、第二步:搭建資源服務(wù)器(Resource Server)
資源服務(wù)器即各個(gè)微服務(wù)(如訂單服務(wù)、用戶服務(wù)),需配置OAuth2.0和JWT,實(shí)現(xiàn)令牌驗(yàn)證和權(quán)限管控,確保只有攜帶合法JWT令牌的請(qǐng)求才能訪問(wèn)受保護(hù)資源。
5.1 導(dǎo)入資源服務(wù)器依賴(Maven)
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2.0資源服務(wù)器依賴 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.8.RELEASE</version>
</dependency>
<!-- JWT依賴(與授權(quán)服務(wù)器一致) -->
<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>5.2 配置資源服務(wù)器(ResourceServerConfig)
核心配置:關(guān)聯(lián)JWT令牌轉(zhuǎn)換器和令牌存儲(chǔ),配置資源訪問(wèn)權(quán)限,驗(yàn)證令牌合法性。
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableResourceServer // 啟用資源服務(wù)器
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// 資源ID(唯一標(biāo)識(shí),與授權(quán)服務(wù)器客戶端配置的資源范圍對(duì)應(yīng))
private static final String RESOURCE_ID = "all";
// JWT簽名密鑰(與授權(quán)服務(wù)器完全一致,否則無(wú)法驗(yàn)證令牌)
private static final String JWT_SECRET = "abc1234567890abc1234567890abc1234";
// 配置令牌存儲(chǔ)(JWT),與授權(quán)服務(wù)器一致
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
// 配置JWT令牌轉(zhuǎn)換器(設(shè)置簽名密鑰,用于驗(yàn)證令牌簽名)
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(JWT_SECRET);
return converter;
}
// 配置資源服務(wù)器核心信息(令牌存儲(chǔ)、資源ID)
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
// 關(guān)聯(lián)資源ID(與授權(quán)服務(wù)器客戶端的scope對(duì)應(yīng))
.resourceId(RESOURCE_ID)
// 關(guān)聯(lián)令牌存儲(chǔ)(用于驗(yàn)證令牌合法性)
.tokenStore(tokenStore())
// 令牌驗(yàn)證失敗時(shí),返回401未授權(quán)響應(yīng)
.stateless(true);
}
// 配置資源訪問(wèn)權(quán)限規(guī)則(根據(jù)用戶角色控制資源訪問(wèn))
@Override
public void configure(org.springframework.security.config.annotation.web.builders.HttpSecurity http) throws Exception {
http
// 關(guān)閉CSRF防護(hù)(資源服務(wù)器無(wú)狀態(tài),無(wú)需CSRF)
.csrf().disable()
// 關(guān)閉Session(適配微服務(wù)無(wú)狀態(tài)架構(gòu))
.sessionManagement().sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.STATELESS).and()
// 授權(quán)規(guī)則配置
.authorizeRequests()
// 公開(kāi)接口允許匿名訪問(wèn)(如健康檢查接口)
.antMatchers("/health/**").permitAll()
// 管理員接口僅允許ADMIN角色訪問(wèn)
.antMatchers("/api/admin/**").hasRole("ADMIN")
// 普通用戶接口允許USER或ADMIN角色訪問(wèn)
.antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
// 其他所有資源需攜帶合法令牌才能訪問(wèn)
.anyRequest().authenticated();
}
}5.3 資源服務(wù)器配置文件(application.yml)
配置端口(與授權(quán)服務(wù)器不同,避免端口沖突)、JWT等信息:
# 服務(wù)器端口(資源服務(wù)器獨(dú)立部署,設(shè)為8081,與授權(quán)服務(wù)器8080區(qū)分)
server:
port: 8081
# JWT配置(與授權(quán)服務(wù)器完全一致)
jwt:
secret: abc1234567890abc1234567890abc1234
expiration: 3600000
# OAuth2.0資源服務(wù)器配置
security:
oauth2:
resource:
# 令牌驗(yàn)證端點(diǎn)(授權(quán)服務(wù)器的check_token端點(diǎn),用于驗(yàn)證令牌合法性)
token-info-uri: http://localhost:8080/oauth/check_token
# 資源ID(與資源服務(wù)器配置的RESOURCE_ID一致)
id: all5.4 實(shí)現(xiàn)資源服務(wù)器測(cè)試接口
創(chuàng)建測(cè)試接口,用于驗(yàn)證單點(diǎn)登錄和權(quán)限管控是否生效:
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
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 {
// 普通用戶接口(USER、ADMIN角色可訪問(wèn))
@GetMapping("/user/test")
public String userTest() {
// 獲取當(dāng)前登錄用戶信息
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return "普通用戶接口訪問(wèn)成功!當(dāng)前登錄用戶:" + userDetails.getUsername() + ",角色:" + userDetails.getAuthorities();
}
// 管理員接口(僅ADMIN角色可訪問(wèn))
@GetMapping("/admin/test")
public String adminTest() {
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return "管理員接口訪問(wèn)成功!當(dāng)前登錄用戶:" + userDetails.getUsername() + ",角色:" + userDetails.getAuthorities();
}
// 回調(diào)接口(用于接收授權(quán)服務(wù)器頒發(fā)的授權(quán)碼,僅授權(quán)碼模式使用)
@GetMapping("/callback")
public String callback(String code) {
// 此處可接收授權(quán)碼,后續(xù)可結(jié)合客戶端邏輯獲取access_token(實(shí)際場(chǎng)景中由客戶端處理)
return "授權(quán)碼接收成功!code:" + code;
}
}六、第三步:?jiǎn)吸c(diǎn)登錄(SSO)完整測(cè)試
啟動(dòng)授權(quán)服務(wù)器(8080端口)和資源服務(wù)器(8081端口),完成以下測(cè)試,驗(yàn)證單點(diǎn)登錄效果(一次登錄,多服務(wù)訪問(wèn)):
6.1 測(cè)試流程(授權(quán)碼模式,SSO核心流程)
- 獲取授權(quán)碼:訪問(wèn)
http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all,登錄admin用戶(密碼123456),獲取回調(diào)地址中的code; - 獲取access_token:通過(guò)Postman發(fā)送POST請(qǐng)求到
http://localhost:8080/oauth/token,攜帶code、client_id、client_secret等參數(shù),獲取access_token(JWT令牌);- 訪問(wèn)資源服務(wù)器接口:
- 訪問(wèn)普通用戶接口:
http://localhost:8081/api/user/test,請(qǐng)求頭添加Authorization: Bearer 第一步獲取的access_token,可正常訪問(wèn); - 訪問(wèn)管理員接口:
http://localhost:8081/api/admin/test,請(qǐng)求頭添加相同的access_token,可正常訪問(wèn)(admin角色擁有權(quán)限);
- 測(cè)試單點(diǎn)登錄:新增另一個(gè)資源服務(wù)器(如訂單服務(wù),端口8082),配置與8081端口資源服務(wù)器一致,使用相同的access_token訪問(wèn)其受保護(hù)接口,無(wú)需重新登錄即可正常訪問(wèn),實(shí)現(xiàn)單點(diǎn)登錄。
6.2 常見(jiàn)問(wèn)題排查
- 令牌驗(yàn)證失敗:檢查資源服務(wù)器與授權(quán)服務(wù)器的JWT_SECRET是否一致,access_token是否過(guò)期;
- 權(quán)限不足(403):檢查用戶角色是否與接口要求的角色匹配,User實(shí)體類中角色轉(zhuǎn)換是否添加ROLE_前綴;
- 授權(quán)碼無(wú)效:授權(quán)碼僅5分鐘有效,且一次使用后失效,需重新獲取授權(quán)碼。
七、生產(chǎn)環(huán)境適配
上述實(shí)操為測(cè)試環(huán)境配置,生產(chǎn)環(huán)境需進(jìn)行以下優(yōu)化,提升安全性和可維護(hù)性:
7.1 安全優(yōu)化
- 密鑰管理:JWT_SECRET、client_secret等敏感信息,不硬編碼,配置在Nacos、Apollo等配置中心,定期更換;
- 令牌安全:縮短access_token有效期(如30分鐘),refresh_token添加黑名單機(jī)制(結(jié)合Redis),支持主動(dòng)注銷令牌;
- 加密傳輸:所有接口使用HTTPS協(xié)議,避免令牌在傳輸過(guò)程中被竊??;
- 權(quán)限細(xì)化:基于資源的細(xì)粒度權(quán)限管控(如用戶只能訪問(wèn)自己的訂單),結(jié)合Spring Security的方法級(jí)權(quán)限注解(@PreAuthorize)。
7.2 架構(gòu)優(yōu)化
- 授權(quán)服務(wù)器集群部署:避免單點(diǎn)故障,使用Redis共享令牌黑名單;
- 資源服務(wù)器統(tǒng)一配置:將OAuth2.0和JWT配置抽取為公共依賴,所有微服務(wù)引入,減少重復(fù)開(kāi)發(fā);
- 日志與監(jiān)控:添加令牌生成、驗(yàn)證、失效的日志記錄,監(jiān)控令牌使用情況,及時(shí)發(fā)現(xiàn)異常訪問(wèn)。
八、總結(jié)
- 授權(quán)服務(wù)器:獨(dú)立部署,負(fù)責(zé)用戶認(rèn)證、頒發(fā)授權(quán)碼和JWT令牌,統(tǒng)一管理客戶端和用戶信息;
- 資源服務(wù)器:各個(gè)微服務(wù),配置令牌驗(yàn)證規(guī)則,實(shí)現(xiàn)權(quán)限管控,僅允許攜帶合法令牌的請(qǐng)求訪問(wèn);
- 單點(diǎn)登錄:用戶通過(guò)授權(quán)服務(wù)器一次登錄,獲取JWT令牌,即可訪問(wèn)所有授權(quán)的資源服務(wù)器,無(wú)需重復(fù)登錄。
到此這篇關(guān)于JWT + Spring Security / OAuth2.0:微服務(wù)統(tǒng)一登錄、鑒權(quán)、單點(diǎn)登錄全解析的文章就介紹到這了,更多相關(guān)Spring Security 微服務(wù)統(tǒng)一登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring?Security?Oauth2整合JWT的詳細(xì)步驟和核心配置
- Spring?Clou整合?Security?+?Oauth2?+?jwt實(shí)現(xiàn)權(quán)限認(rèn)證的詳細(xì)過(guò)程
- 使用JWT作為Spring?Security?OAuth2的token存儲(chǔ)問(wèn)題
- Spring Security OAuth2實(shí)現(xiàn)使用JWT的示例代碼
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過(guò)程
- SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
- SpringSecurity動(dòng)態(tài)加載用戶角色權(quán)限實(shí)現(xiàn)登錄及鑒權(quán)功能
相關(guān)文章
java根據(jù)模板動(dòng)態(tài)生成PDF實(shí)例
本篇文章主要介紹了java根據(jù)模板動(dòng)態(tài)生成PDF實(shí)例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
java中創(chuàng)建寫(xiě)入文件的6種方式詳解與源碼實(shí)例
這篇文章主要介紹了java中創(chuàng)建寫(xiě)入文件的6種方式詳解與源碼實(shí)例,Files.newBufferedWriter(Java 8),Files.write(Java 7 推薦),PrintWriter,File.createNewFile,FileOutputStream.write(byte[] b) 管道流,需要的朋友可以參考下2022-12-12
mybatisplus根據(jù)條件只更新一個(gè)字段的實(shí)現(xiàn)
MyBatis-Plus提供使用update方法結(jié)合Wrapper來(lái)指定更新條件和要更新的字段,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12
SpringBoot設(shè)置接口超時(shí)時(shí)間的方法
這篇文章主要介紹了SpringBoot設(shè)置接口超時(shí)時(shí)間的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08

