spring security oauth2搭建resource-server demo及token改造成JWT令牌

尋找的路上發表於2021-12-18

我們在上文講了如何在spring security的環境中搭建基於oauth2協議的認證中心demo:https://www.cnblogs.com/process-h/p/15688971.html, 對應的還應該要有一個resource server。本章主要就是講resource server的demo搭建,並且再將普通的token改造成JWT令牌的形式以及為什麼要改成JWT令牌格式。

自定義resource-server實現類

​ 在搭建oauth2認證中心時,我們需要再自定義一個繼承AuthorizationServerConfigurerAdapter的實現類,同時在該實現類上新增@EnableAuthorizationServer註解。同樣的,我們也需要建立一個繼承ResourceServerConfigurerAdapter的實現類,並在該實現類上新增@EnableResourceServer註解來宣告這是一個資源服務。

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
}

​ 我們來看看ResourceServerConfigurerAdapter父類,發現它重寫了2個方法。以下是這2個方法中的引數物件可以配置的屬性:

ResourceServerSecurityConfigurer中主要包括:

  • tokenServices:ResourceServerTokenServices 類的例項,用來實現令牌服務。
  • tokenStore:TokenStore類的例項,指定令牌如何訪問。
  • resourceId:這個資源服務的ID,這個屬性是可選的,但是推薦設定並在授權服務中進行驗證。
  • 其他的擴充屬性例如 tokenExtractor 令牌提取器用來提取請求中的令牌。

HttpSecurity配置這個與Spring Security類似:

  • 請求匹配器,用來設定需要進行保護的資源路徑,預設的情況下是保護資源服務的全部路徑。
  • 通過http.authorizeRequests()來設定受保護資源的訪問規則。
  • 其他的自定義許可權保護規則通過 HttpSecurity 來進行配置。
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
   @Override
   public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
   }
   @Override
   public void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests().anyRequest().authenticated();
   }

}

編寫ResourceServerConfig

package com.peter.security.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

/**
 * @author huang
 * @description
 * @date 2021/12/17
 **/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "res1";

    //資源服務令牌解析服務
    @Bean
    public ResourceServerTokenServices tokenService() {
        //使用遠端服務請求授權伺服器校驗token,必須指定校驗token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
       service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        // 宣告只有該client 的接入方才能訪問該資源服務
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 宣告該資源服務id,以及認證的tokenSerivce物件
        resources.resourceId(RESOURCE_ID)
                .tokenServices(tokenService());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
// 訪問該資源服務的client的scope要有all許可權                
            	.antMatchers("/**").access("#oauth2.hasScope('all')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

定義資源服務介面

​ 有了配置,還需要暴露出一些資源介面給外部呼叫,我們這裡簡單定義一個介面。

package com.peter.security.order.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author huangyizeng
 * @description
 * @date 2021/12/17
 **/
@RestController
public class OrderController {

    // 訪問該資源的使用者要有P1許可權
    @GetMapping(value = "/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1(){
        return "訪問資源1";
    }

}

示例

1、申請token

​ 我們知道Oauth2協議有4中授權方式,分別是:

http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=hyz&password=hyz

2、檢視token代表了哪些資訊

​ spring security oauth2 中有個用於資源服務訪問的令牌解析端點(/oauth/check_token )。我們先來看看上面返回的token中都有哪些許可權,訪問

http://localhost:53020/uaa/oauth/check_token?token=c46ffcf5-3043-4513-9405-808f89b02f30

​ 可以看到返回以下屬性資訊:

  • 第三方client的基本資訊:user_name\client_Id\scope
  • 第三方client的授權列表:authorities
  • 第三方client的資源服務列表:res1

如果我們傳入一個不存在的token,那麼將會返回

{
    "error": "invalid_token",
    "error_description": "Token was not recognised"
}

3、使用token訪問資源服務

​ 最後我們再來使用該token訪問資源服務。oauth2.0 要求將token根據具體的格式放在請求頭中:Authorization: Bearer token

​ 如上圖,資源服務在接收到token引數後,會到授權服務的/oauth/check_token端點拿到該token的許可權,然後與資源服務定義的授權進行比對,如果符合就繼續執行,否則返回

{
    "error": "invalid_token",
    "error_description": "c46ffcf5-3043-4513-9405-808f89b02f301"
}

JWT令牌

​ 通過上面的測試,當資源服務和授權服務不在一起時,資源服務需要通過網路請求去授權服務的/token/check_token端點請求驗證token,如果訪問量較大將會影響系統的效能。

​ 為了解決效能問題,可以將token令牌採用jwt格式,這樣使用者授權後通過後將會拿到一個JWT令牌,JWT令牌中已經包含了使用者相關的資訊,包括許可權,基本資訊等,接著資源服務根據事先跟認證服務約定好的演算法自行進行令牌校驗,無需每次都想認證服務請求驗證。

​ JWT具體是什麼,相信童鞋們也有一定的認識了,這裡不過多說明。主要說明一點,一般JWT是是用非對稱加密使用的,在這裡我們使用對稱加密的方式,省去非對稱加密金鑰生成步驟。

1、配置認證服務JWT令牌

​ 在認證服務中配置JWT令牌,即可實現生成JWT格式的令牌。

TokenConfig ,修改tokenService的實現類。

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //對稱祕鑰,資源伺服器使用該祕鑰來驗證
        return converter;
    }
}

定義使用JWT令牌

public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
     @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true);
        // 定義使用JWT令牌
        service.setTokenStore(tokenStore);
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);
        service.setAccessTokenValiditySeconds(7200); // 令牌預設有效期2小時
        service.setRefreshTokenValiditySeconds(259200); // 重新整理令牌預設有效期3天
        return service;
    }
}

檢驗令牌,根據密碼授權模式來獲取令牌:

​ 由上圖可見,生成的令牌確實已經是JWT的格式了。

​ 再檢視該令牌中代表了哪些資訊。訪問

http://localhost:53020/uaa/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ7XCJmdWxsbmFtZVwiOlwiaHl6XCIsXCJpZFwiOlwiMVwiLFwicGFzc3dvcmRcIjpcIiQyYSQxMCRJdm94aUN3eEdXWkdFemxkcVY2bktPcUxuY05zMmNVdkdsQ2ZmZ1V0a2hZaGc4dUo0akd5eVwiLFwidXNlcm5hbWVcIjpcImh5elwifSIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2Mzk4MTk4ODgsImF1dGhvcml0aWVzIjpbInAxIiwicDMiXSwianRpIjoiNDAzNWU3NTItNmYzNS00Y2RhLTg0ZTgtNjEyZmFhZWI2OWFkIiwiY2xpZW50X2lkIjoiYzEifQ.VIzqACiYJ-ZCTrq1BkOM_HeXkTwE9tAZ3TSiy7B9pJ4

確實訪問了在上面例項中的client相關資訊:

{
    "aud": [
        "res1"
    ],
    "user_name": "{\"fullname\":\"hyz\",\"id\":\"1\",\"password\":\"$2a$10$IvoxiCwxGWZGEzldqV6nKOqLncNs2cUvGlCffgUtkhYhg8uJ4jGyy\",\"username\":\"hyz\"}",
    "scope": [
        "all"
    ],
    "exp": 1639819888,
    "authorities": [
        "p1",
        "p3"
    ],
    "jti": "4035e752-6f35-4cda-84e8-612faaeb69ad",
    "client_id": "c1"
}

​ 由此可見,基於JWT的改造已經完成。

2、修改資源服務檢驗令牌配置

資源服務需要和授權服務擁有一致的簽字、令牌服務等:

  1. 將認證服務中的TokenConfig類拷貝到資源 服務中
  2. 將授權服務中的TokenConfig類拷貝到資源 服務中
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "res1";

    @Autowired
    private TokenStore tokenStore;

    //資源服務令牌解析服務
//    @Bean
//    public ResourceServerTokenServices tokenService() {
//        //使用遠端服務請求授權伺服器校驗token,必須指定校驗token 的url、client_id,client_secret
//        RemoteTokenServices service=new RemoteTokenServices();
//        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
//        service.setClientId("c1");
//        service.setClientSecret("secret");
//        return service;
//    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore);
    }
}

驗證令牌是否有效

​ 我們再次使用上面基於JWT生成的令牌去訪問資源服務:

​ 由上圖可知是可以正常訪問的,如果該令牌有誤,則會返回

{
    "error": "invalid_token",
    "error_description": "Cannot convert access token to JSON"
}

​ 到這裡,本篇就結束了,我們總結一下:

  1. 使用基本的token,需要去認證中心驗證token是否有效等,在高併發時會有效能問題
  2. 令牌改成JWT格式,解決了效能問題,當然也可以考慮將令牌存放在redis中也可以解決效能問題
  3. 本文栗子中使用對稱加密的JWT令牌,生產環境肯定是使用非對稱加密的金鑰對(如何配置,童鞋們可以實踐一下)

​ spring security相關的內容就寫到這裡,在spring security 及Oauth2的原始碼中發現了許多不明白的地方,之前spring的原始碼由粗略看到一部分,現在覺得還是需要再回頭看看spring原始碼,所以後續會分享spring原始碼的章節。

相關文章