Spring cloud OAuth2 and JWT

參考:

  1. Spring cloud oauth2.0學(xué)習(xí)總結(jié)
  2. spring-security-oauth2
  3. 官方spring_cloud_security
  4. 理解OAuth 2.0
  5. where-to-store-your-jwts
  6. JWT RFC標(biāo)準(zhǔn)
  7. 理解JWT

官方文檔有句話,可以看出官方基本上也是推薦使用OAuth2進(jìn)行授權(quán)管理、JWT作為令牌管理:

基于Spring BootSpring Security OAuth2可以很快速創(chuàng)建單點(diǎn)登錄、令牌relay和令牌交換。

OAuth 2.0 簡(jiǎn)單介紹

角色

先區(qū)分下OAuth 2.0 中有哪些角色,注意這里根據(jù)自己理解來(lái)寫(xiě)的,阮一峰博客里寫(xiě)的更精確:

  1. Client: 客戶端,也就是Third-party application - 第三方應(yīng)用程序
  2. Service:服務(wù)端,也就是服務(wù)的提供者
  3. User: 用戶,也就是Resource Owner - 資源所有者
  4. User Agent:用戶代理,如瀏覽器,下文中將其與Client合并考慮。
  5. Authorization Server:認(rèn)證服務(wù)器,即服務(wù)提供商專(zhuān)門(mén)用來(lái)處理認(rèn)證的服務(wù)器。
  6. Resource Server:資源服務(wù)器,即服務(wù)提供商存放用戶生成的資源的服務(wù)器。

模式

在不需要第三方認(rèn)證支持時(shí),我們常用的就是簡(jiǎn)化模式:

image

步驟如下:

(A)客戶端將用戶導(dǎo)向認(rèn)證服務(wù)器。
(B)用戶決定是否給于客戶端授權(quán)。
(C)假設(shè)用戶給予授權(quán),認(rèn)證服務(wù)器將用戶導(dǎo)向客戶端指定的"重定向URI",并在URI的Hash部分包含了訪問(wèn)令牌。
(D)瀏覽器向資源服務(wù)器發(fā)出請(qǐng)求,其中不包括上一步收到的Hash值。
(E)資源服務(wù)器返回一個(gè)網(wǎng)頁(yè),其中包含的代碼可以獲取Hash值中的令牌。
(F)瀏覽器執(zhí)行上一步獲得的腳本,提取出令牌。
(G)瀏覽器將令牌發(fā)給客戶端。

上面都是非常嚴(yán)謹(jǐn)?shù)拿枋?,大家可以詳?xì)看阮一峰的博客,或者RFC標(biāo)準(zhǔn), 下面主要分析下微服務(wù)下如何

簡(jiǎn)單對(duì)比下簡(jiǎn)單模式和授權(quán)模式的差別:

  1. 簡(jiǎn)化模式主要用于用戶直接登錄,至于是通過(guò)web、App還是其它方式是無(wú)所謂的,用戶將給客戶端完全的授權(quán);這里只涉及到用戶 -> 客戶端 -> 服務(wù)提供者
  2. 授權(quán)模式是給第三方部分/全部權(quán)限以便于其給用戶提供服務(wù),這里涉及到 用戶 -> 客戶端 -> 第三方服務(wù)提供者 -> 服務(wù)提供者

重點(diǎn)是授權(quán)模式給 第三方部分用戶權(quán)限,而且這時(shí)候客戶端大部分也是第三方提供的。

團(tuán)隊(duì)學(xué)習(xí)時(shí),很多人困惑: 為何在授權(quán)模式下需要將code給客戶端, 然后再讓第三方去獲取Token?

  1. 授權(quán)模式下,數(shù)據(jù)是在第三方和后臺(tái)直接交互,用戶只是給了授權(quán),所以Token肯定要給第三方
  2. 如果直接將Token給客戶端,但客戶端本身也可能是偽造的,但它拿了code是沒(méi)有用的,無(wú)法訪問(wèn)數(shù)據(jù)

Spring Cloud 微服務(wù)下

Spring Cloud 下我們這里使用簡(jiǎn)化模式,主要是登錄、授權(quán)、Token管理,角色大體如下:

  1. User: 也就是用戶,用戶一般直接與Client交互,REST API后臺(tái)一般不需要考慮。
  2. Gateway + Resource Server :資源服務(wù)器對(duì)請(qǐng)求進(jìn)行認(rèn)證,一般整合在網(wǎng)關(guān)中,這樣可以很方便的統(tǒng)一處理所有請(qǐng)求。
  3. Authorization Server: 授權(quán)服務(wù)器,進(jìn)行授權(quán)和Token管理。
  4. Client: 調(diào)用API的應(yīng)用,一般是前端、移動(dòng)App或者第三方應(yīng)用
  5. Token Store: 令牌存儲(chǔ),多個(gè)服務(wù)如果每次請(qǐng)求都通過(guò)授權(quán)服務(wù)器進(jìn)行Token查詢,效率底下,所以需要統(tǒng)一存儲(chǔ)、交互令牌信息,常用Redis
  6. Services: 提供正在業(yè)務(wù)/功能/API的服務(wù)。

大概畫(huà)個(gè)圖,這里以Client為前端為例,注意不涉及用戶和前端的交互:


image.png
  1. 服務(wù)接收請(qǐng)求后,如果需要還會(huì)解析Token獲取用戶信息
  2. 實(shí)際過(guò)程中,還會(huì)有Token的刷新、刪除等操作
  3. 如果使用JWT,那么可以不保存token,當(dāng)然也可以保存,Spring中默認(rèn)JwtTokenStore實(shí)際上是沒(méi)有存儲(chǔ)??梢詤⒖?jwt-authentication-how-to-implement-logout

Spring Cloud OAuth將基本的功能都實(shí)現(xiàn)。

JWT 簡(jiǎn)介

規(guī)范

JWT -- Json Web Token, 如其名,使用Json方式保存Web Token的協(xié)議。網(wǎng)上有各種解讀,個(gè)人理解,這就是一個(gè) 客戶端Session - Session保存在客戶端,而不是通常的保存在服務(wù)端。

構(gòu)成

JWT三部分組成:

  1. Header 頭部:JSON方式描述JWT基本信息,如類(lèi)型和簽名算法。使用Base64編碼為字符串
  2. Payload 載荷:JSON方式描述JWT信息,除了標(biāo)準(zhǔn)定義的,還可以添加自定義的信息。同樣使用Base64編碼為字符串。
    • iss: 簽發(fā)者
    • sub: 用戶
    • aud: 接收方
    • exp(expires): unix時(shí)間戳描述的過(guò)期時(shí)間
    • iat(issued at): unix時(shí)間戳描述的簽發(fā)時(shí)間
  3. Signature 簽名:將前兩個(gè)字符串用 . 連接后,使用頭部定義的加密算法,利用密鑰進(jìn)行簽名,并將簽名信息附在最后。

注意: Payload 使用 Base64編碼,所以就是明文的,不要存放任何機(jī)密信息。

優(yōu)缺點(diǎn)

當(dāng)然帶來(lái)一些好處:

  1. 服務(wù)端內(nèi)存占用少了
  2. 不需要維護(hù)session狀態(tài)了,真正無(wú)狀態(tài)
  3. 單點(diǎn)登錄 so easy,只要后臺(tái)服務(wù)能解讀,Cookie 設(shè)置為頂級(jí)域名

有好處當(dāng)然就有不太好的:

  1. 每個(gè)請(qǐng)求就要對(duì)JWT進(jìn)行解密,驗(yàn)證
  2. Token有效期只有超時(shí),沒(méi)有退出。當(dāng)然有一些做法,上面也提到了,jwt-authentication-how-to-implement-logout
  3. XSS攻擊問(wèn)題,一個(gè)討論: Is it OK to store the JWT in local/session storage

我個(gè)人的看法是: 使用JWT,同時(shí)在Redis保存信息,在API網(wǎng)關(guān)進(jìn)行詳細(xì)的驗(yàn)證;各服務(wù)則只簡(jiǎn)單校驗(yàn)Token本身是否篡改。

Spring Cloud OAuth 解讀

角色

Spring Cloud OAuth中將角色為三個(gè),這點(diǎn)從源碼中包org.springframework.security.oauth2.config.annotation.web.configurers 中包含三個(gè)Enable注解就可以看出來(lái):

  1. EnableAuthorizationServer -- 使能授權(quán)服務(wù)器
  2. EnableResourceServer -- 使能資源服務(wù)器
  3. EnableOAuth2Client -- 使能客戶端,如需要第三方授權(quán)來(lái)調(diào)用,應(yīng)該使用此注解。

AuthorizationServer 授權(quán)服務(wù)配置

一. 首先當(dāng)然需要使能,在配置類(lèi)或 Application 上類(lèi)添加注解: @EnableAuthorizationServer,添加該注解后會(huì)自動(dòng)添加OAuth2的多個(gè)endpoint, 相關(guān)實(shí)現(xiàn)代碼在包 org.springframework.security.oauth2.provider.endpoint:

  1. /oauth/authorize:驗(yàn)證接口, AuthorizationEndpoint
  2. /oauth/token:獲取token
  3. /oauth/confirm_access:用戶授權(quán)
  4. /oauth/error:認(rèn)證失敗
  5. /oauth/check_token:資源服務(wù)器用來(lái)校驗(yàn)token
  6. /oauth/token_key:jwt模式下獲取公鑰;位于:TokenKeyEndpoint ,通過(guò) JwtAccessTokenConverter 訪問(wèn)key

二. 配置入口為接口:AuthorizationServerConfigurer, 通過(guò)擴(kuò)展AuthorizationServerConfigurerAdapter 實(shí)現(xiàn)來(lái)進(jìn)行配置。

Spring Boot 2中很多 Adapter已經(jīng)取消,直接利用 Java8 Interface Default特性來(lái)實(shí)現(xiàn),不過(guò)到我寫(xiě)此文時(shí) security 還沒(méi)改,當(dāng)然也許是我沒(méi)注意到。

三. 簡(jiǎn)單看一下 AuthorizationServerConfigurer 接口的方法, 一共配置三個(gè)屬性:

  1. AuthorizationServerSecurityConfigurer :聲明安全約束,哪些允許訪問(wèn),哪些不允許訪問(wèn)。配置 AuthorizationServer 的安全屬性,也就是endpoint /oauth/token 。/oauth/authorize 則和其它用戶 REST 一樣保護(hù)??梢圆慌渲?。
  2. ClientDetailsServiceConfigurer : 配置 ClientDetailsService 獨(dú)立client客戶端的信息。包括權(quán)限范圍、授權(quán)方式、客戶端權(quán)限等配置。授權(quán)方式有4種:implicit, client_redentials, password , authorization_code, 其中密碼授權(quán)方式必須結(jié)合 AuthenticationManager 進(jìn)行配置。必須至少配置一個(gè)客戶端。
  3. AuthorizationServerEndpointsConfigurer : 配置AuthorizationServer 端點(diǎn)的非安全屬性,也就是 token 存儲(chǔ)方式、token 配置、用戶授權(quán)模式等。默認(rèn)不需做任何配置,除非使用 密碼授權(quán)方式, 這時(shí)候必須配置 AuthenticationManager。

四. 其中,Token管理:

  1. Token 生命周期管理接口 AuthorizationServerTokenServices, 默認(rèn)使用: DefaultTokenServices
  2. Token存儲(chǔ)通過(guò)配置 TokenStore,默認(rèn)使用內(nèi)存存儲(chǔ)。AuthorizationServerEndpointsConfigurerDefaultTokenServices 入口配置。配置方式有
    • InMemoryTokenStore 默認(rèn)方式,保存在本地內(nèi)存
    • JdbcTokenStore 存儲(chǔ)數(shù)據(jù)庫(kù)
    • RedisTokenStore 存儲(chǔ)Redis,這應(yīng)該是微服務(wù)下比較常用方式
    • JwtTokenStore
  3. AccessTokenConverter

五. 加密算法配置

在spring5之后,必須配置加密算法。

  1. 測(cè)試時(shí)候可以用無(wú)加密算法,參考:no-passwordencoder-mapped-id-null
    @SuppressWarnings("deprecation")
    @Bean
    public NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
  1. 配置加密算法,當(dāng)然也可以配置其它算法:
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

六. 實(shí)際例子:

代碼這里不貼了,可以參考上面的參考材料。

測(cè)試:

curl -X POST \
  http://127.0.0.1:<端口>/oauth/token \
  -H 'Authorization: Basic <xxxxxxx>' \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=admin&password=admin&grant_type=password'

其中:Basic <xxxxxxx> 根據(jù)client_id和secret計(jì)算,我用的postman測(cè)試,其中授權(quán)方式選擇 Basic Auth, Username就是client_id,secret 就是 password,postman 會(huì)自動(dòng)計(jì)算 Authorization 字段。

ResourceServer 資源服務(wù)器配置

一. 在配置類(lèi)或 Application 類(lèi)上添加注解:@EnableResourceServer

二. 配置接口為: ResourceServerConfigurer,繼承實(shí)現(xiàn)ResourceServerConfigurerAdapter 即可。接口有兩個(gè)配置:

  1. ResourceServerSecurityConfigurer : 配置資源服務(wù)器安全屬性,如Token的配置,這些是與 AuthorizationServer 授權(quán)服務(wù)器的配置是匹配的。
  2. HttpSecurity : 配置資源的保護(hù)

如何整合 ResourceServer 到 zuul,網(wǎng)上有很多教程,這里不多說(shuō)。但是到目前為止,spring cloud gateway 并沒(méi)有整合 OAuth2,需要自己實(shí)現(xiàn),且其與 ResourceServer 實(shí)現(xiàn)不能整合(也可能我沒(méi)找到)

https://github.com/spring-cloud/spring-cloud-gateway/issues/179

Spring Cloud Gateway 整合 Security

一些參考

https://github.com/spring-projects/spring-security/issues/4807
https://github.com/spring-cloud/spring-cloud-gateway/issues/144

https://stackoverflow.com/questions/46798705/is-there-working-example-of-oauth2-with-webflux

主要參考:
https://stackoverflow.com/questions/47354171/spring-webflux-custom-authentication-for-api

臨時(shí)的解決方案,后續(xù)Spring應(yīng)該會(huì)將其完全整合。
gradle 添加依賴(lài):

  compile 'org.springframework.boot:spring-boot-starter-security'
  compile 'io.jsonwebtoken:jjwt:0.9.0'

添加SecurityFilter

@Configuration
@EnableWebFluxSecurity
public class SecurityFilter {

    @Autowired
    private SecurityContextRepository securityContextRepository;

    @Bean
    SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
        return http
                // Disable default security.
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                // config auth
                .securityContextRepository(securityContextRepository)
                // Disable authentication for `/oauth/**` routes.
                .authorizeExchange()
                .pathMatchers("/oauth/**").permitAll()
                .anyExchange().authenticated()
                .and()
                .build();
    }
}

主要的實(shí)現(xiàn)在 SecurityContextRepository,下面代碼沒(méi)有完全實(shí)現(xiàn),大體流程:

  1. 獲取Token
  2. 解析Token,這里用的 jjwt
  3. 判斷Token信息,下面的代碼中并沒(méi)有實(shí)現(xiàn),大家可以自己實(shí)現(xiàn)
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {

    private static final Logger logger = LoggerFactory.getLogger(SecurityFilter.class);

    @Override
    public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
        return Mono.empty();
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
         // 獲取Token
        String authHeader = serverWebExchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null) {
            logger.warn("not find AUTHORIZATION");
            return Mono.empty();
        }
        String token = authHeader.replace(TOKEN_SCHEME, "").trim();

        try {
            Claims claims = Jwts.parser().setSigningKey("iotSignKey".getBytes()).parseClaimsJws(token).getBody();
            TokenInfo tokenInfo = new TokenInfo(claims);
            logger.info("token:{}  ", tokenInfo);

            // 獲取授權(quán)信息
            List<GrantedAuthority> authorities = tokenInfo.getAuthorities()
                    .stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(toList());

            Authentication authentication = new JwtAuthenticationToken(authorities, tokenInfo.getUserName());
            authentication.setAuthenticated(true);

            return Mono.justOrEmpty(new SecurityContextImpl(authentication));
        } catch (SignatureException e) {
            // 驗(yàn)證錯(cuò)誤
            logger.warn("jwt token parse error: {}", e.getCause());
        } catch (ExpiredJwtException e) {
            // token 超時(shí)
            logger.warn("jwt token is expired");
        } catch (MalformedJwtException e) {
            // token Malformed
            logger.warn("jwt token is malformed");
        }
        return Mono.empty();
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容