最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時(shí)間:2026年04月10日 10:34:44   作者:鬼先生_sir  
本文介紹了微服務(wù)架構(gòu)中基于JWT、SpringSecurity和OAuth2.0實(shí)現(xiàn)統(tǒng)一身份認(rèn)證與鑒權(quán)的方法,接著詳細(xì)介紹了JWT、SpringSecurity和OAuth2.0的工作原理和核心概念,然后通過(guò)實(shí)操步驟展示了如何在Spring?Boot中實(shí)現(xiàn)微服務(wù)統(tǒng)一登錄與鑒權(quá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: 8080

3.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: 8080

4.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: all

5.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)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JCommander解析命令行參數(shù)使用詳解

    JCommander解析命令行參數(shù)使用詳解

    這篇文章主要為大家介紹了JCommander解析命令行參數(shù)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Java?Stream?流基本概念和使用方式詳解

    Java?Stream?流基本概念和使用方式詳解

    Stream是Java 8 引入的全新API,它基于函數(shù)式編程思想,為集合操作提供了一種聲明式的處理方式,本文給大家介紹Java Stream流基本概念和核心特性,感興趣的朋友一起看看吧
    2025-09-09
  • java并發(fā)編程之同步器代碼示例

    java并發(fā)編程之同步器代碼示例

    這篇文章主要介紹了java并發(fā)編程之同步器代碼示例,分享了相關(guān)代碼,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • java根據(jù)模板動(dòng)態(tài)生成PDF實(shí)例

    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搜索與圖論之DFS和BFS算法詳解

    Java搜索與圖論之DFS和BFS算法詳解

    DFS指在進(jìn)行算法運(yùn)算時(shí),優(yōu)先將該路徑的當(dāng)前路徑執(zhí)行完畢,執(zhí)行完畢或失敗后向上回溯嘗試其他途徑。BFS指在進(jìn)行算法運(yùn)算時(shí),優(yōu)先將當(dāng)前路徑點(diǎn)的所有情況羅列出來(lái),然后根據(jù)羅列出來(lái)的情況羅列下一層。本文介紹了二者的實(shí)現(xiàn)與應(yīng)用,需要的可以參考一下
    2022-11-11
  • Java多線程之顯示鎖和內(nèi)置鎖總結(jié)詳解

    Java多線程之顯示鎖和內(nèi)置鎖總結(jié)詳解

    這篇文章主要介紹了Java多線程之顯示鎖和內(nèi)置鎖總結(jié)詳解,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • java中創(chuàng)建寫(xiě)入文件的6種方式詳解與源碼實(shí)例

    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)

    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í)間的方法

    這篇文章主要介紹了SpringBoot設(shè)置接口超時(shí)時(shí)間的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • javafx實(shí)現(xiàn)時(shí)鐘效果

    javafx實(shí)現(xiàn)時(shí)鐘效果

    這篇文章主要為大家詳細(xì)介紹了javafx實(shí)現(xiàn)時(shí)鐘效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-11-11

最新評(píng)論

瓦房店市| 大埔区| 石家庄市| 石渠县| 白河县| 墨玉县| 延安市| 临湘市| 凌海市| 南宫市| 宜昌市| 克拉玛依市| 碌曲县| 余干县| 金山区| 焦作市| 大姚县| 丰县| 封丘县| 新营市| 五台县| 枣庄市| 珠海市| 天门市| 新兴县| 随州市| 克东县| 丹阳市| 沁水县| 长白| 景洪市| 唐海县| 酒泉市| 江陵县| 福海县| 潼关县| 鄂温| 青冈县| 驻马店市| 安远县| 泾川县|