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

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

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

在微服務(wù)架構(gòu)中,服務(wù)被拆分為多個獨立部署的節(jié)點,跨服務(wù)訪問、用戶身份統(tǒng)一管理成為核心痛點——用戶在每個服務(wù)都需單獨登錄、權(quán)限無法統(tǒng)一管控、多系統(tǒng)切換頻繁登錄,這些問題不僅影響用戶體驗,更會帶來嚴重的安全隱患。

一、微服務(wù)身份認證與鑒權(quán)的核心痛點

在單體應(yīng)用中,我們通常通過Session存儲用戶身份信息,實現(xiàn)登錄與鑒權(quán),但這種方式在微服務(wù)架構(gòu)中完全失效,核心痛點集中在3點:

1.1 Session共享問題

單體應(yīng)用中,Session存儲在服務(wù)器內(nèi)存,微服務(wù)中多個服務(wù)部署在不同節(jié)點,Session無法跨服務(wù)共享,導致用戶在A服務(wù)登錄后,訪問B服務(wù)仍需重新登錄,體驗極差。

1.2 權(quán)限管控分散

每個微服務(wù)單獨維護一套權(quán)限規(guī)則,無法實現(xiàn)統(tǒng)一的角色、資源管控,不僅開發(fā)冗余,更易出現(xiàn)權(quán)限漏洞(如某服務(wù)遺漏權(quán)限校驗、權(quán)限規(guī)則不一致)。

1.3 多系統(tǒng)單點登錄需求

企業(yè)通常有多個關(guān)聯(lián)系統(tǒng)(如電商系統(tǒng)、后臺管理系統(tǒng)、APP接口),用戶希望一次登錄,即可訪問所有授權(quán)系統(tǒng),無需重復輸入賬號密碼,這就需要單點登錄(SSO)能力。

而JWT + Spring Security + OAuth2.0的組合,正是解決上述痛點的最優(yōu)解:JWT實現(xiàn)無狀態(tài)令牌傳輸,Spring Security實現(xiàn)權(quán)限管控,OAuth2.0實現(xiàn)授權(quán)與單點登錄,三者協(xié)同,構(gòu)建微服務(wù)統(tǒng)一身份認證與鑒權(quán)體系。

二、JWT、Spring Security、OAuth2.0

2.1 JWT:無狀態(tài)令牌,解決Session共享難題

JWT(JSON Web Token)是一種輕量級的令牌規(guī)范,核心作用是在客戶端與服務(wù)器之間安全地傳輸用戶身份信息,采用無狀態(tài)設(shè)計,無需在服務(wù)器存儲Session,完美適配微服務(wù)架構(gòu)。

2.1.1 JWT核心結(jié)構(gòu)(3部分,用點號分隔)

JWT令牌由 Header(頭部)、Payload(載荷)Signature(簽名) 三部分組成,示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MTUyODc2MDAsImlhdCI6MTcxNTI4NDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header(頭部):指定JWT的簽名算法和令牌類型,默認算法為HS256(HMAC SHA256),示例:

{
??"alg": "HS256", // 簽名算法
??"typ": "JWT" // 令牌類型
}
  • Payload(載荷):存儲用戶核心信息(如用戶ID、用戶名、角色)和令牌過期時間,分為標準聲明自定義聲明
{ "userId": 1, // 自定義聲明:用戶ID "userName": "admin", // 自定義聲明:用戶名 "role": "ADMIN", // 自定義聲明:角色 "exp": 1715287600, // 標準聲明:過期時間 "iat": 1715284000 // 標準聲明:簽發(fā)時間 }
  • 標準聲明(可選,但推薦使用):
    • sub:令牌面向的用戶
    • iat:令牌簽發(fā)時間
    • exp:令牌過期時間(時間戳,單位毫秒)
    • iss:令牌簽發(fā)者
    • 自定義聲明:根據(jù)業(yè)務(wù)需求添加,如用戶ID(userId)、角色(role)、權(quán)限(permissions)等,注意:Payload不加密,不能存儲敏感信息(如密碼)。
  • Signature(簽名):核心安全保障,通過Header指定的算法,將Header(Base64編碼)、Payload(Base64編碼)和密鑰(secret)進行加密,生成簽名。服務(wù)器接收令牌后,會重新計算簽名,若與令牌中的簽名不一致,則說明令牌被篡改,直接拒絕訪問。 簽名計算公式:HMACSHA256( Base64Encode(Header) + "." + Base64Encode(Payload), secret )

2.1.2 JWT核心優(yōu)勢與注意事項

  • 優(yōu)勢
    • 無狀態(tài):服務(wù)器無需存儲Session,僅通過令牌即可驗證用戶身份,減輕服務(wù)器壓力,適配微服務(wù)集群部署;
    • 跨語言:基于JSON格式,支持所有語言(Java、Python、Go等),適配多端(Web、APP、小程序);
    • 自包含:Payload中包含用戶核心信息,無需頻繁查詢數(shù)據(jù)庫,提升接口響應(yīng)速度;
    • 可擴展:支持自定義聲明,適配不同業(yè)務(wù)場景的身份信息傳輸需求。
  • 注意事項
    • Payload不加密:嚴禁存儲敏感信息(如密碼、手機號),僅存儲非敏感的用戶標識和權(quán)限信息;
    • 密鑰安全:簽名密鑰(secret)必須妥善保管,一旦泄露,攻擊者可偽造令牌,引發(fā)安全風險;
    • 令牌過期:必須設(shè)置合理的過期時間(如1小時),過期后需重新登錄獲取新令牌;
    • 無法撤銷:JWT令牌一旦簽發(fā),在過期前無法主動撤銷(除非結(jié)合Redis黑名單機制)。

2.2 Spring Security:微服務(wù)權(quán)限管控核心框架

Spring Security是Spring生態(tài)中成熟的權(quán)限管理框架,核心作用是實現(xiàn)用戶認證(登錄校驗)和授權(quán)(權(quán)限管控),提供了完善的安全防護機制(如CSRF防護、XSS防護、會話管理),可無縫整合JWT和OAuth2.0,是微服務(wù)權(quán)限管控的首選框架。

2.2.1 Spring Security核心概念

  • 認證(Authentication):驗證用戶身份的合法性(如賬號密碼是否正確),認證通過后,生成認證信息(Authentication對象),存儲在SecurityContext中。
  • 授權(quán)(Authorization):驗證用戶是否擁有訪問某個資源的權(quán)限(如普通用戶能否訪問管理員接口),核心是“資源-角色-用戶”的關(guān)聯(lián)關(guān)系。
  • SecurityContext:存儲用戶認證信息的上下文,線程安全,可通過SecurityContextHolder.getContext()獲取當前登錄用戶信息。
  • UserDetailsService:核心接口,用于加載用戶信息(如從數(shù)據(jù)庫查詢用戶賬號、密碼、角色),是認證流程的核心組件。
  • PasswordEncoder:密碼加密器,用于對用戶密碼進行加密存儲(如BCrypt加密),避免明文存儲密碼,提升安全性。
  • FilterChain:安全過濾器鏈,Spring Security通過一系列過濾器(如UsernamePasswordAuthenticationFilter、JwtAuthenticationFilter)處理請求,完成認證和授權(quán)。

2.2.2 Spring Security核心流程(認證+授權(quán))

  • 用戶發(fā)起登錄請求(如POST /login),攜帶賬號密碼;
  • UsernamePasswordAuthenticationFilter攔截請求,將賬號密碼封裝為Authentication對象;
  • 調(diào)用AuthenticationManager(認證管理器),觸發(fā)認證流程;
  • AuthenticationManager調(diào)用UserDetailsService,加載數(shù)據(jù)庫中的用戶信息(UserDetails);
  • PasswordEncoder對比用戶提交的密碼與數(shù)據(jù)庫中加密后的密碼,驗證是否一致;
  • 認證通過:生成包含用戶信息和權(quán)限的Authentication對象,存入SecurityContext;
  • 認證失?。簰伋霎惓?,返回登錄失敗提示;
  • 用戶訪問受保護資源時,F(xiàn)ilterSecurityInterceptor攔截請求,校驗當前用戶是否擁有該資源的訪問權(quán)限;
  • 授權(quán)通過:允許訪問資源;授權(quán)失?。悍祷?03 Forbidden。

2.3 OAuth2.0:授權(quán)協(xié)議,實現(xiàn)單點登錄與第三方授權(quán)

OAuth2.0是一種開放的授權(quán)協(xié)議,核心作用是實現(xiàn)“第三方授權(quán)”和“單點登錄(SSO)”,允許用戶通過一個賬號(如微信、QQ)登錄多個關(guān)聯(lián)系統(tǒng),無需重復注冊和登錄,同時避免用戶將核心賬號密碼泄露給第三方系統(tǒng)。

注意:OAuth2.0是授權(quán)協(xié)議,不是認證協(xié)議,它的核心是“授權(quán)”——用戶授權(quán)第三方系統(tǒng)訪問自己的資源(如微信授權(quán)某APP獲取用戶昵稱、頭像),而認證是驗證用戶身份的過程(如微信登錄時驗證賬號密碼)。

2.3.1 OAuth2.0核心角色

  • 資源所有者(Resource Owner):用戶,擁有資源的所有權(quán)(如微信用戶擁有自己的昵稱、頭像等資源)。
  • 客戶端(Client):需要獲取用戶資源的應(yīng)用(如某APP、某網(wǎng)站),需提前在授權(quán)服務(wù)器注冊,獲取客戶端ID(client_id)和客戶端密鑰(client_secret)。
  • 授權(quán)服務(wù)器(Authorization Server):負責驗證用戶身份、頒發(fā)授權(quán)令牌(如access_token),是OAuth2.0的核心組件(如微信授權(quán)服務(wù)器)。
  • 資源服務(wù)器(Resource Server):存儲用戶資源的服務(wù)器(如微信的用戶信息服務(wù)器),客戶端通過授權(quán)令牌訪問資源服務(wù)器,獲取用戶資源。

2.3.2 OAuth2.0核心授權(quán)流程(通用流程)

  • 客戶端(APP)引導用戶跳轉(zhuǎn)到授權(quán)服務(wù)器,請求用戶授權(quán);
  • 用戶驗證身份(如登錄微信),并同意授權(quán)客戶端訪問自己的資源;
  • 授權(quán)服務(wù)器頒發(fā)授權(quán)碼(code)給客戶端;
  • 客戶端攜帶授權(quán)碼(code)、客戶端ID、客戶端密鑰,向授權(quán)服務(wù)器請求訪問令牌(access_token)
  • 授權(quán)服務(wù)器驗證信息無誤后,頒發(fā)access_token(訪問令牌)和refresh_token(刷新令牌)給客戶端;
  • 客戶端攜帶access_token,向資源服務(wù)器請求訪問用戶資源;
  • 資源服務(wù)器驗證access_token的合法性,驗證通過后,返回用戶資源給客戶端。

2.3.3 OAuth2.0 4種授權(quán)模式(重點掌握2種)

OAuth2.0提供4種授權(quán)模式,適配不同的業(yè)務(wù)場景,其中授權(quán)碼模式密碼模式是微服務(wù)中最常用的兩種。

  • 授權(quán)碼模式(Authorization Code)
    • 特點:最安全、最常用的模式,通過授權(quán)碼獲取access_token,避免直接傳遞賬號密碼,適合Web應(yīng)用、APP等場景;
    • 適用場景:單點登錄(SSO)、第三方授權(quán)(如APP用微信登錄);
    • 核心優(yōu)勢:安全性高,授權(quán)碼僅短期有效,且客戶端無需存儲用戶賬號密碼。
  • 密碼模式(Password)
    • 特點:用戶直接向客戶端提供賬號密碼,客戶端攜帶賬號密碼向授權(quán)服務(wù)器請求access_token;
    • 適用場景:微服務(wù)內(nèi)部系統(tǒng)(如后臺管理系統(tǒng)),客戶端與授權(quán)服務(wù)器屬于同一信任體系,且用戶信任客戶端;
    • 注意:安全性較低,僅適用于內(nèi)部信任場景,嚴禁用于第三方授權(quán)。
  • 簡化模式(Implicit):無需授權(quán)碼,直接頒發(fā)access_token,安全性低,僅適用于純前端應(yīng)用(如Vue、React),不推薦生產(chǎn)使用。
  • 客戶端憑證模式(Client Credentials):客戶端通過自身的client_id和client_secret獲取access_token,無需用戶參與,適用于服務(wù)間通信(如微服務(wù)A調(diào)用微服務(wù)B)。

2.3.4 OAuth2.0核心令牌

  • access_token(訪問令牌):用于訪問資源服務(wù)器的令牌,短期有效(如1小時),過期后需重新獲取;
  • refresh_token(刷新令牌):用于在access_token過期后,無需重新登錄,直接獲取新的access_token,長期有效(如7天);
  • 授權(quán)碼(code):用于獲取access_token的臨時憑證,短期有效(如5分鐘),一次使用后失效。

三、實操落地:Spring Security + JWT 實現(xiàn)微服務(wù)統(tǒng)一登錄與鑒權(quán)

先實現(xiàn)最基礎(chǔ)的“統(tǒng)一登錄與鑒權(quán)”:基于Spring Security + JWT,實現(xiàn)用戶登錄生成JWT令牌,后續(xù)請求攜帶令牌完成身份驗證和權(quán)限管控,適配微服務(wù)架構(gòu)(無狀態(tài))。

3.1 第一步:導入依賴(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ù)庫依賴(模擬用戶數(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工具類(生成令牌、驗證令牌)

核心工具類,負責JWT令牌的生成、解析、驗證,封裝通用方法,便于后續(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,嚴禁硬編碼)
    @Value("${jwt.secret}")
    private String secret;
    // JWT過期時間(單位:毫秒,此處配置1小時)
    @Value("${jwt.expiration}")
    private long expiration;
    // 從令牌中獲取用戶名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    // 從令牌中獲取過期時間
    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);
    }
    // 解析令牌,獲取所有聲明(需驗證簽名)
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secret.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    // 判斷令牌是否過期
    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) // 用戶名(唯一標識)
                .setIssuedAt(new Date(System.currentTimeMillis())) // 簽發(fā)時間
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 過期時間
                .signWith(SignatureAlgorithm.HS256, secret.getBytes()) // 簽名算法+密鑰
                .compact();
    }
    // 驗證令牌(驗證簽名、過期時間、用戶名匹配)
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

3.3 第三步:配置Spring Security核心配置

核心配置類,用于配置認證流程、授權(quán)規(guī)則、JWT過濾器等,替代默認的Session認證。

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) // 啟用方法級別的權(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();
    }
    // 認證提供者(關(guān)聯(lián)UserDetailsService和PasswordEncoder)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    // 認證管理器(核心認證組件)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    // 核心安全配置(配置授權(quán)規(guī)則、過濾器、會話管理等)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 關(guān)閉CSRF防護(微服務(wù)中JWT無狀態(tài),無需CSRF)
                .csrf().disable()
                // 配置未認證請求的處理方式(返回401)
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                // 配置會話管理:無狀態(tài)(不創(chuàng)建Session)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 配置授權(quán)規(guī)則
                .authorizeRequests()
                // 登錄接口、注冊接口允許匿名訪問
                .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
                // 靜態(tài)資源允許匿名訪問
                .antMatchers("/static/**", "/swagger-ui/**").permitAll()
                // 管理員接口僅允許ADMIN角色訪問
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                // 普通用戶接口允許USER或ADMIN角色訪問
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                // 其他所有請求都需要認證
                .anyRequest().authenticated();
        // 注冊認證提供者
        http.authenticationProvider(authenticationProvider());
        // 添加JWT過濾器(在用戶名密碼過濾器之前執(zhí)行,先驗證令牌)
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

3.4 第四步:實現(xiàn)JWT過濾器與認證異常處理

JWT過濾器負責攔截所有請求,提取請求頭中的JWT令牌,驗證令牌合法性,若驗證通過,將用戶信息存入SecurityContext,實現(xiàn)無狀態(tài)認證。

4.1 JWT過濾器(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;
    // 攔截請求,驗證JWT令牌
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            // 1. 從請求頭中提取JWT令牌(請求頭格式:Authorization: Bearer <token>)
            String jwt = getJwtFromRequest(request);
            // 2. 驗證令牌是否存在且有效
            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)建認證對象,存入SecurityContext
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("無法設(shè)置用戶認證信息: {}", e);
        }
        // 繼續(xù)執(zhí)行過濾器鏈
        filterChain.doFilter(request, response);
    }
    // 從請求頭中提取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 認證異常處理(JwtAuthenticationEntryPoint)

當令牌無效、過期或未攜帶令牌時,返回統(tǒng)一的401響應(yīng),替代默認的登錄頁面。

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 {
    // 未認證請求的處理邏輯
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        // 設(shè)置響應(yīng)狀態(tài)碼401,返回JSON格式的錯誤信息
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":401,\"message\":\"未認證,請先登錄獲取令牌\"}");
    }
}

3.5 第五步:實現(xiàn)UserDetailsService(加載用戶信息)

自定義UserDetailsService,從數(shù)據(jù)庫中加載用戶賬號、密碼、角色信息,適配Spring Security的認證流程。

5.1 實體類(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)
    // 實現(xiàn)UserDetails接口方法:獲取用戶權(quán)限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 將角色轉(zhuǎn)換為Spring Security認可的權(quán)限格式(ROLE_前綴)
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        return authorities;
    }
    // 實現(xiàn)UserDetails接口方法:賬號是否未過期(默認true)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 實現(xiàn)UserDetails接口方法:賬號是否未鎖定(默認true)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 實現(xiàn)UserDetails接口方法:憑證是否未過期(默認true)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 實現(xiàn)UserDetails接口方法:賬號是否啟用(默認true)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

5.2 UserRepository(數(shù)據(jù)庫操作)

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認證核心方法)
    Optional<User> findByUsername(String username);
}

5.3 自定義UserDetailsService實現(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ù)庫查詢用戶,若不存在則拋出異常
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用戶不存在:" + username));
        // 返回User對象(已實現(xiàn)UserDetails接口)
        return user;
    }
}

3.6 第六步:實現(xiàn)登錄接口(生成JWT令牌)

自定義登錄接口,接收用戶賬號密碼,完成認證后,生成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;
    // 登錄接口:接收賬號密碼,返回JWT令牌
    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest loginRequest) {
        // 1. 執(zhí)行認證(驗證賬號密碼)
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        // 2. 認證通過,將認證信息存入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);
    }
    // 注冊接口(可選,用于測試)
    @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("注冊成功");
    }
    // 登錄請求參數(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; }
    }
    // 注冊請求參數(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ù)庫配置(H2內(nèi)存數(shù)據(jù)庫,用于測試)
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update # 自動創(chuàng)建表結(jié)構(gòu)
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  # H2控制臺配置(訪問:http://localhost:8080/h2-console)
  h2:
    console:
      enabled: true
      path: /h2-console
# JWT配置
jwt:
  secret: abc1234567890abc1234567890abc1234 # 簽名密鑰(生產(chǎn)環(huán)境需修改,建議至少32位)
  expiration: 3600000 # 過期時間(1小時,單位:毫秒)
# 服務(wù)器端口
server:
  port: 8080

3.8 測試驗證

  • 啟動項目,訪問 http://localhost:8080/h2-console,登錄H2數(shù)據(jù)庫,插入測試用戶:
  •  INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密碼:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密碼:123456
  • 調(diào)用注冊接口(可選):POST http://localhost:8080/api/auth/register,請求體: { "username": "test", "password": "123456", "role": "USER" }
  • 調(diào)用登錄接口:POST  http://localhost:8080/api/auth/login,請求體: { "username": "admin", "password": "123456" }響應(yīng)結(jié)果(包含JWT令牌): { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9XSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", "username": "admin", "role": "ADMIN" }
  • 訪問受保護接口(如管理員接口):GET  http://localhost:8080/api/admin/test,請求頭添加Authorization: Bearer 令牌,即可正常訪問;若不攜帶令牌或令牌無效,返回401。

四、OAuth2.0 + JWT + Spring Security 實現(xiàn)單點登錄(SSO)

前面實現(xiàn)了單個微服務(wù)的登錄與鑒權(quán),而微服務(wù)架構(gòu)中通常有多個服務(wù)(如訂單服務(wù)、用戶服務(wù)、后臺管理服務(wù)),需要實現(xiàn)“單點登錄”——用戶一次登錄,即可訪問所有授權(quán)服務(wù)。

核心方案:基于 OAuth2.0 授權(quán)碼模式,搭建獨立的授權(quán)服務(wù)器(統(tǒng)一處理登錄、頒發(fā)令牌)和資源服務(wù)器(各微服務(wù)),結(jié)合JWT實現(xiàn)無狀態(tài)單點登錄。

4.1 架構(gòu)設(shè)計(核心組件)

  • 授權(quán)服務(wù)器(Authorization Server):獨立部署,負責用戶認證、頒發(fā)JWT令牌(access_token、refresh_token)、處理授權(quán)請求,是單點登錄的核心。
  • 資源服務(wù)器(Resource Server):各個微服務(wù)(如訂單服務(wù)、用戶服務(wù)),配置OAuth2.0和JWT,驗證令牌合法性,實現(xiàn)權(quán)限管控。
  • 客戶端(Client):需要接入單點登錄的應(yīng)用(如Web后臺、APP、小程序),提前在授權(quán)服務(wù)器注冊。

4.2 第一步:搭建授權(quán)服務(wù)器(Authorization Server)

基于Spring Security OAuth2.0,搭建獨立的授權(quán)服務(wù)器,實現(xiàn)用戶登錄、授權(quán)碼頒發(fā)、JWT令牌生成。

2.1 導入依賴(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ù)庫)同上 -->

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(提前注冊,用于客戶端身份驗證)
    private static final String CLIENT_ID = "client1";
    // 客戶端密鑰(加密存儲,密碼: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小時)
    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";
    // 配置令牌存儲(JWT),用于存儲和解析JWT令牌
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    // 配置JWT令牌轉(zhuǎn)換器(設(shè)置簽名密鑰,確保令牌生成和驗證的一致性)
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(JWT_SECRET); // 與JWT工具類的密鑰完全一致
        return converter;
    }
    // 配置客戶端信息(客戶端在授權(quán)服務(wù)器注冊的核心信息,用于客戶端身份校驗)
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // 客戶端ID(唯一標識,客戶端需攜帶此ID請求授權(quán))
                .withClient(CLIENT_ID)
                // 客戶端密鑰(加密存儲,客戶端請求時需攜帶加密前的密鑰進行校驗)
                .secret(CLIENT_SECRET)
                // 授權(quán)范圍,用于限制客戶端可訪問的資源范圍
                .scopes(SCOPE)
                // 支持的授權(quán)模式,此處兼容授權(quán)碼模式(SSO核心)、密碼模式(內(nèi)部系統(tǒng))、刷新令牌模式
                .authorizedGrantTypes(GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_PASSWORD, GRANT_TYPE_REFRESH_TOKEN)
                // 訪問令牌有效期(1小時,避免令牌長期有效帶來的安全風險)
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
                // 刷新令牌有效期(7天,用戶無需頻繁登錄,提升體驗)
                .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ù)器端點(核心組件關(guān)聯(lián),確保認證和令牌生成流程正常)
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // 關(guān)聯(lián)認證管理器(用于密碼模式,驗證用戶賬號密碼合法性)
                .authenticationManager(authenticationManager)
                // 關(guān)聯(lián)令牌存儲(JWT),用于存儲和讀取令牌信息
                .tokenStore(tokenStore())
                // 關(guān)聯(lián)令牌轉(zhuǎn)換器(JWT),用于生成和解析JWT令牌
                .accessTokenConverter(accessTokenConverter());
    }
    // 配置授權(quán)服務(wù)器安全規(guī)則(控制授權(quán)服務(wù)器端點的訪問權(quán)限)
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 允許所有客戶端訪問token_key端點(獲取JWT簽名公鑰,用于資源服務(wù)器驗證令牌)
                .tokenKeyAccess("permitAll()")
                // 允許所有客戶端訪問check_token端點(驗證令牌的合法性,資源服務(wù)器會調(diào)用此端點)
                .checkTokenAccess("permitAll()")
                // 允許客戶端通過表單認證(用于客戶端身份校驗,簡化客戶端請求流程)
                .allowFormAuthenticationForClients();
    }
}

4.3 授權(quán)服務(wù)器配套配置(完善認證流程)

授權(quán)服務(wù)器需依賴前文實現(xiàn)的UserDetailsService、PasswordEncoder等組件,同時補充Spring Security配置(避免默認登錄頁面干擾),確保認證流程正常。

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();
    }
    // 認證提供者(關(guān)聯(lián)UserDetailsService和PasswordEncoder,用于加載用戶信息并校驗密碼)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    // 認證管理器(核心認證組件,授權(quán)服務(wù)器密碼模式需依賴此組件)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    // 安全過濾器鏈配置(關(guān)閉Session,允許授權(quán)相關(guān)端點匿名訪問)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 關(guān)閉CSRF防護(授權(quán)服務(wù)器無狀態(tài),無需CSRF)
                .csrf().disable()
                // 關(guān)閉Session(授權(quán)服務(wù)器無需存儲用戶會話,適配微服務(wù)無狀態(tài)架構(gòu))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 授權(quán)規(guī)則配置
                .authorizeRequests()
                // 授權(quán)服務(wù)器核心端點允許匿名訪問(客戶端請求授權(quán)、獲取令牌需訪問這些端點)
                .antMatchers("/oauth/authorize", "/oauth/token", "/oauth/check_token", "/oauth/token_key").permitAll()
                // 登錄接口、注冊接口允許匿名訪問(用于用戶注冊和登錄驗證)
                .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
                // 其他所有請求需認證(避免未授權(quán)訪問)
                .anyRequest().authenticated();
        // 注冊認證提供者
        http.authenticationProvider(authenticationProvider());
        return http.build();
    }
}

4.3.2 授權(quán)服務(wù)器配置文件(application.yml)

配置端口、數(shù)據(jù)庫、JWT等信息,與前文保持一致,確保組件協(xié)同工作:

spring:
  # 數(shù)據(jù)庫配置(H2內(nèi)存數(shù)據(jù)庫,用于測試,生產(chǎn)環(huán)境替換為MySQL)
  datasource:
    url: jdbc:h2:mem:authdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456
  # JPA配置(自動創(chuàng)建表結(jié)構(gòu),簡化測試)
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  # H2控制臺配置(訪問:http://localhost:8080/h2-console,用于插入測試用戶)
  h2:
    console:
      enabled: true
      path: /h2-console
# JWT配置(與資源服務(wù)器、令牌轉(zhuǎn)換器一致)
jwt:
  secret: abc1234567890abc1234567890abc1234 # 簽名密鑰,生產(chǎn)環(huán)境需修改并配置在配置中心
  expiration: 3600000 # 訪問令牌有效期(1小時,與授權(quán)服務(wù)器配置一致)
# 服務(wù)器端口(授權(quán)服務(wù)器獨立部署,端口設(shè)為8080,避免與資源服務(wù)器沖突)
server:
  port: 8080

4.4 授權(quán)服務(wù)器測試驗證

啟動授權(quán)服務(wù)器,完成以下測試,確保授權(quán)服務(wù)器可正常頒發(fā)授權(quán)碼和JWT令牌:

  • 啟動項目,訪問H2控制臺(http://localhost:8080/h2-console),登錄后插入測試用戶(與前文一致):
  • INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密碼:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密碼:123456
  • 請求授權(quán)碼(授權(quán)碼模式):訪問以下地址,引導用戶登錄并授權(quán),獲取授權(quán)碼(code): http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all
  • 訪問后會跳轉(zhuǎn)至登錄頁面,輸入用戶名(admin)和密碼(123456);
  • 登錄成功后,會跳轉(zhuǎn)至回調(diào)地址(http://localhost:8081/callback),地址欄會攜帶授權(quán)碼(code參數(shù)),例如:http://localhost:8081/callback?code=abc123(code為臨時憑證,5分鐘內(nèi)有效)。
  • 通過授權(quán)碼獲取訪問令牌(access_token):使用Postman發(fā)送POST請求,地址:http://localhost:8080/oauth/token,參數(shù)如下:
    • 請求頭:添加Authorization: Basic Y2xpZW50MToxMjM0NTY=(client1:123456的Base64編碼);
    • 請求體(form-data): grant_type=authorization_code、code=步驟2獲取的授權(quán)碼、redirect_uri=http://localhost:8081/callback、scope=all;
    • 響應(yīng)結(jié)果:會返回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ù)器即各個微服務(wù)(如訂單服務(wù)、用戶服務(wù)),需配置OAuth2.0和JWT,實現(xiàn)令牌驗證和權(quán)限管控,確保只有攜帶合法JWT令牌的請求才能訪問受保護資源。

5.1 導入資源服務(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)換器和令牌存儲,配置資源訪問權(quá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(唯一標識,與授權(quán)服務(wù)器客戶端配置的資源范圍對應(yīng))
    private static final String RESOURCE_ID = "all";
    // JWT簽名密鑰(與授權(quán)服務(wù)器完全一致,否則無法驗證令牌)
    private static final String JWT_SECRET = "abc1234567890abc1234567890abc1234";
    // 配置令牌存儲(JWT),與授權(quán)服務(wù)器一致
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    // 配置JWT令牌轉(zhuǎn)換器(設(shè)置簽名密鑰,用于驗證令牌簽名)
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(JWT_SECRET);
        return converter;
    }
    // 配置資源服務(wù)器核心信息(令牌存儲、資源ID)
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                // 關(guān)聯(lián)資源ID(與授權(quán)服務(wù)器客戶端的scope對應(yīng))
                .resourceId(RESOURCE_ID)
                // 關(guān)聯(lián)令牌存儲(用于驗證令牌合法性)
                .tokenStore(tokenStore())
                // 令牌驗證失敗時,返回401未授權(quán)響應(yīng)
                .stateless(true);
    }
    // 配置資源訪問權(quán)限規(guī)則(根據(jù)用戶角色控制資源訪問)
    @Override
    public void configure(org.springframework.security.config.annotation.web.builders.HttpSecurity http) throws Exception {
        http
                // 關(guān)閉CSRF防護(資源服務(wù)器無狀態(tài),無需CSRF)
                .csrf().disable()
                // 關(guān)閉Session(適配微服務(wù)無狀態(tài)架構(gòu))
                .sessionManagement().sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.STATELESS).and()
                // 授權(quán)規(guī)則配置
                .authorizeRequests()
                // 公開接口允許匿名訪問(如健康檢查接口)
                .antMatchers("/health/**").permitAll()
                // 管理員接口僅允許ADMIN角色訪問
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                // 普通用戶接口允許USER或ADMIN角色訪問
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                // 其他所有資源需攜帶合法令牌才能訪問
                .anyRequest().authenticated();
    }
}

5.3 資源服務(wù)器配置文件(application.yml)

配置端口(與授權(quán)服務(wù)器不同,避免端口沖突)、JWT等信息:

# 服務(wù)器端口(資源服務(wù)器獨立部署,設(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:
      # 令牌驗證端點(授權(quán)服務(wù)器的check_token端點,用于驗證令牌合法性)
      token-info-uri: http://localhost:8080/oauth/check_token
      # 資源ID(與資源服務(wù)器配置的RESOURCE_ID一致)
      id: all

5.4 實現(xiàn)資源服務(wù)器測試接口

創(chuàng)建測試接口,用于驗證單點登錄和權(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角色可訪問)
    @GetMapping("/user/test")
    public String userTest() {
        // 獲取當前登錄用戶信息
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return "普通用戶接口訪問成功!當前登錄用戶:" + userDetails.getUsername() + ",角色:" + userDetails.getAuthorities();
    }
    // 管理員接口(僅ADMIN角色可訪問)
    @GetMapping("/admin/test")
    public String adminTest() {
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return "管理員接口訪問成功!當前登錄用戶:" + 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(實際場景中由客戶端處理)
        return "授權(quán)碼接收成功!code:" + code;
    }
}

六、第三步:單點登錄(SSO)完整測試

啟動授權(quán)服務(wù)器(8080端口)和資源服務(wù)器(8081端口),完成以下測試,驗證單點登錄效果(一次登錄,多服務(wù)訪問):

6.1 測試流程(授權(quán)碼模式,SSO核心流程)

  • 獲取授權(quá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:通過Postman發(fā)送POST請求到http://localhost:8080/oauth/token,攜帶code、client_id、client_secret等參數(shù),獲取access_token(JWT令牌);
    • 訪問資源服務(wù)器接口:
    • 訪問普通用戶接口:http://localhost:8081/api/user/test,請求頭添加Authorization: Bearer 第一步獲取的access_token,可正常訪問;
    • 訪問管理員接口:http://localhost:8081/api/admin/test,請求頭添加相同的access_token,可正常訪問(admin角色擁有權(quán)限);
  • 測試單點登錄:新增另一個資源服務(wù)器(如訂單服務(wù),端口8082),配置與8081端口資源服務(wù)器一致,使用相同的access_token訪問其受保護接口,無需重新登錄即可正常訪問,實現(xiàn)單點登錄。

6.2 常見問題排查

  • 令牌驗證失敗:檢查資源服務(wù)器與授權(quán)服務(wù)器的JWT_SECRET是否一致,access_token是否過期;
  • 權(quán)限不足(403):檢查用戶角色是否與接口要求的角色匹配,User實體類中角色轉(zhuǎn)換是否添加ROLE_前綴;
  • 授權(quán)碼無效:授權(quán)碼僅5分鐘有效,且一次使用后失效,需重新獲取授權(quán)碼。

七、生產(chǎn)環(huán)境適配

上述實操為測試環(huán)境配置,生產(chǎn)環(huán)境需進行以下優(yōu)化,提升安全性和可維護性:

7.1 安全優(yōu)化

  • 密鑰管理:JWT_SECRET、client_secret等敏感信息,不硬編碼,配置在Nacos、Apollo等配置中心,定期更換;
  • 令牌安全:縮短access_token有效期(如30分鐘),refresh_token添加黑名單機制(結(jié)合Redis),支持主動注銷令牌;
  • 加密傳輸:所有接口使用HTTPS協(xié)議,避免令牌在傳輸過程中被竊?。?/li>
  • 權(quán)限細化:基于資源的細粒度權(quán)限管控(如用戶只能訪問自己的訂單),結(jié)合Spring Security的方法級權(quán)限注解(@PreAuthorize)。

7.2 架構(gòu)優(yōu)化

  • 授權(quán)服務(wù)器集群部署:避免單點故障,使用Redis共享令牌黑名單;
  • 資源服務(wù)器統(tǒng)一配置:將OAuth2.0和JWT配置抽取為公共依賴,所有微服務(wù)引入,減少重復開發(fā);
  • 日志與監(jiān)控:添加令牌生成、驗證、失效的日志記錄,監(jiān)控令牌使用情況,及時發(fā)現(xiàn)異常訪問。

八、總結(jié)

  • 授權(quán)服務(wù)器:獨立部署,負責用戶認證、頒發(fā)授權(quán)碼和JWT令牌,統(tǒng)一管理客戶端和用戶信息;
  • 資源服務(wù)器:各個微服務(wù),配置令牌驗證規(guī)則,實現(xiàn)權(quán)限管控,僅允許攜帶合法令牌的請求訪問;
  • 單點登錄:用戶通過授權(quán)服務(wù)器一次登錄,獲取JWT令牌,即可訪問所有授權(quán)的資源服務(wù)器,無需重復登錄。

到此這篇關(guān)于JWT + Spring Security / OAuth2.0:微服務(wù)統(tǒng)一登錄、鑒權(quán)、單點登錄全解析的文章就介紹到這了,更多相關(guān)Spring Security 微服務(wù)統(tǒng)一登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

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

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

    這篇文章主要為大家介紹了JCommander解析命令行參數(shù)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    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)代碼,具有一定參考價值,需要的朋友可以了解下。
    2017-11-11
  • java根據(jù)模板動態(tài)生成PDF實例

    java根據(jù)模板動態(tài)生成PDF實例

    本篇文章主要介紹了java根據(jù)模板動態(tài)生成PDF實例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-04-04
  • Java搜索與圖論之DFS和BFS算法詳解

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

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

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

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

    java中創(chuàng)建寫入文件的6種方式詳解與源碼實例

    這篇文章主要介紹了java中創(chuàng)建寫入文件的6種方式詳解與源碼實例,Files.newBufferedWriter(Java 8),Files.write(Java 7 推薦),PrintWriter,File.createNewFile,FileOutputStream.write(byte[] b) 管道流,需要的朋友可以參考下
    2022-12-12
  • mybatisplus根據(jù)條件只更新一個字段的實現(xiàn)

    mybatisplus根據(jù)條件只更新一個字段的實現(xiàn)

    MyBatis-Plus提供使用update方法結(jié)合Wrapper來指定更新條件和要更新的字段,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2024-12-12
  • SpringBoot設(shè)置接口超時時間的方法

    SpringBoot設(shè)置接口超時時間的方法

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

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

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

最新評論

都安| 沁水县| 鄂州市| 永清县| 同心县| 宿迁市| 卢湾区| 肥城市| 华阴市| 昌宁县| 永平县| 西安市| 昂仁县| 都匀市| 梧州市| 乌拉特前旗| 通道| 南溪县| 吴江市| 青神县| 闸北区| 奉化市| 丰宁| 佛冈县| 青冈县| 宁远县| 白城市| 新余市| 裕民县| 灵石县| 鄂托克前旗| 荆门市| 广饶县| 永兴县| 林州市| 沙洋县| 营山县| 衡南县| 荥经县| 祁东县| 乐陵市|