微服務架構 | 7.1 基於 OAuth2 的安全認證

多氯環己烷發表於2022-02-02


前言

《Spring Microservices in Action》
《Spring Cloud Alibaba 微服務原理與實戰》
《B站 尚矽谷 SpringCloud 框架開發教程 周陽》

OAuth2 是一個基於令牌的安全驗證和授權框架。他允許使用者使用第三方驗證服務進行驗證。 如果使用者成功進行了驗證, 則會出示一個令牌,該令牌必須與每個請求一起傳送。然後,驗證服務可以對令牌進行確認;


1. OAuth2 基礎知識

1.1 安全性的 4 個組成部分

  • 受保護資源:Resource Server,開發人員想要保護的資源(如一個微服務),需要確保只有已通過驗證並且具有適當授權的使用者才能訪問它;
  • 資源所有者:Resource Owner,資源所有者定義哪些應用程式可以呼叫其服務,哪些使用者可以訪問該服務,以及他們可以使用該服務完成哪些事情。 資源所有者註冊的每個應用程式都將獲得一個應用程式名稱,該應用程式名稱與應用程式金鑰一起標識應用程式。 應用程式名稱和金鑰的組合是在驗證 OAuth2 令牌時傳遞的憑據的一部分;
  • 應用程式:Client,這是代表使用者呼叫服務的應用程式。畢竟,使用者很少直接呼叫服務 。相反,他們依賴應用程式為他們工作。
  • OAuth2 驗證伺服器:Authorization Server,OAuth2 驗證伺服器是應用程式和正在使用的服務之間的中間人。 OAuth2 驗證伺服器允許使用者對自己進行驗證,而不必將使用者憑據傳遞給由應用程式代表使用者呼叫的每個服務;

1.2 OAuth2 的工作原理

  • 第三方客戶端資源所有者(使用者)申請認證請求;
  • 【關鍵】使用者同意請求,返回一個許可;
  • 客戶端根據許可向認證伺服器申請認證令牌 Token;
  • 客戶端根據認證令牌向資源伺服器申請相關資源;

Oauth執行流程

1.3 OAuth2 規範的 4 種型別的授權

  • 密碼( password ) ;
  • 客戶端憑據( client credential ) ;;
  • 授權碼( authorization code) ;
  • 隱式( imp licit );

1.4 OAuth2 的優勢

  • 允許開發人員輕鬆與第三方雲服務提供商整合,並使用這些服務進行使用者驗證和授權,而無須不斷地將使用者的憑據傳遞給第三方服務;

1.5 OAuth2 核心原理

  • 先有一個 OAuth2 認證伺服器,用來建立和管理 OAuth2 訪問令牌;
  • 接著在受保護資源主程式類上新增一個註解:@EnableResourceServer,該註解會強制執行一個過濾器,該過濾器會攔截對服務的所有傳入呼叫,檢查傳入呼叫的 HTTP 首部中是否存在 OAuth2 訪問令牌,然後呼叫 security.oauth2.resource.userInfoUri 中定義的回撥 URL 告訴客戶端與 OAuth2 認證伺服器互動,檢視令牌是否有效;
  • 一旦獲悉令牌是有效的,@EnableResourceServer 註解也會應用任何訪問控制規則,以控制什麼人可以訪問服務;

1.6 JSON Web Token


2. 建立 OAuth2 伺服器

  • 驗證服務將驗證使用者憑據並頒發令牌;
  • 每當使用者嘗試訪問由,如正服務保護的服務時,驗證服務將確認 OAuth2 令牌是否已由其頒發並且尚未過期;

2.1 引入 pom.xml 依賴檔案

<!--security 通用安全庫-->
<dependency> 
	<groupid>org.springframework.cloud</groupid> 
	<artifactid>spring-cloud-security</artifactid> 
</dependency> 
<!--oauth2.0-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

2.2 主程式類上新增註解

  • @EnableAuthorizationServer:該服務將作為 OAuth2 服務;
  • @EnableResourceServer:表示該服務是受保護資源;(該註解在 3.3 詳解)

2.3 新增受保護物件的端點

在 controller 包下;

  • 該端點將對映到 /auth/user 端點,當受保護的服務呼叫 /auth/user 時,將會確認 OAuth2 訪問令牌,並檢索發文手背歐虎服務所分配的角色;
/**
 * 使用者資訊校驗
 * 由受保護服務呼叫,確認 OAuth2 訪問令牌,並檢索訪問受保護服務的使用者所分配的角色
 * @param OAuth2Authentication 資訊
 * @return 使用者資訊
 */
@RequestMapping(value = { "/user" }, produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
    Map<String, Object> userInfo = new HashMap<>();
    userInfo.put("user", user.getUserAuthentication().getPrincipal());
    userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
    return userInfo;
}

2.4 定義哪些應用程式可以使用服務

在 config 包下;

  • ClientDetailsServiceConfigurer 支援兩種型別的儲存:記憶體儲存和JDBC儲存,如下分點所示:

2.4.1 使用 JDBC 儲存

  • OAuth2Config 類
@Configuration
//繼承 AuthorizationServerConfigurerAdapter 類
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    //定義哪些客戶端將註冊到服務
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //JDBC儲存:
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT); //設定我們的自定義的sql查詢語句
        clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT); //設定我們的自定義的sql查詢語句
        clients.withClientDetails(clientDetailsService); //從 jdbc 查出資料來儲存
    }
    
    @Override
    //使用 Spring 提供的預設驗證管理器和使用者詳細資訊服務
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      endpoints
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService);
    }
}
  • SecurityConstants 類:裡面存放上述提到的 SQL 查詢語句;
public interface SecurityConstants {
    /**
     * sys_oauth_client_details 表的欄位,不包括client_id、client_secret
     */
    String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
            + "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
            + "refresh_token_validity, additional_information, autoapprove";

    /**
     *JdbcClientDetailsService 查詢語句
     */
    String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS + " from sys_oauth_client_details";

    /**
     * 預設的查詢語句
     */
    String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";

    /**
     * 按條件client_id 查詢
     */
    String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";
}

2.4.2 使用記憶體儲存

@Configuration
//繼承 AuthorizationServerConfigurerAdapter 類
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    //定義哪些客戶端將註冊到服務
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("eagleeye")  //名稱
                .secret("thisissecret")  //金鑰
                .authorizedGrantTypes("refresh_token", "password", "client_credentials")  //授權型別列表
                .scopes("webclient", "mobileclient");  //獲取訪問令牌時可以操作的範圍
    }

    @Override
    //使用 Spring 提供的預設驗證管理器和使用者詳細資訊服務
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      endpoints
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService);
    }
}

2.5 為應用程式定義使用者 ID、密碼和角色

在 config 包下:

  • 可以從記憶體資料儲存、支援 JDBC 的關聯式資料庫或 LDAP 伺服器中儲存和檢索使用者資訊;
@Configuration
@EnableWebSecurity
//擴充套件核心 Spring Security 的 WebSecurityConfigurerAdapter
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    //用來處理驗證
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //處理返回使用者資訊
    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
    
    //configure() 方法定義使用者、密碼與角色
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("john.carnell").password("password1").roles("USER")
                .and()
                .withUser("william.woodward").password("password2").roles("USER", "ADMIN");
    }
}
  • 上述例子中 john.carnell 使用者擁有 USER 使用者;
  • william.woodward 擁有 ADMIN 使用者;

2.6 通過傳送 POST 請求驗證使用者

  • 傳送:POST http://localhost:8901/auth/oauth/token
  • 並在 POST 的請求體裡帶上應用程式名稱、金鑰、使用者 ID 和密碼,可以模擬使用者獲取 OAuth2 令牌;

3. 使用 OAuth2 建立並保護服務資源

  • 建立和管理 OAuth2 訪問令牌是 OAuth2 伺服器的職責;
  • 定義哪些使用者角色有權執行哪些操作在單個服務級別上的;

3.1 引入 pom.xml 依賴檔案

<!--security 通用安全庫-->
<dependency> 
	<groupid>org.springframework.cloud</groupid> 
	<artifactid>spring-cloud-security</artifactid> 
</dependency> 
<!--oauth2.0-->
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>

3.2 新增 bootstrap.yml 配置檔案

security:
  oauth2:
   resource:
      userInfoUri: http://localhost:8901/auth/user
  • 這裡新增回撥 URL,客戶端訪問受保護服務時,受保護服務將呼叫 /auth/user 端點,向 OAuth2 伺服器檢查訪問令牌是否生效;

3.3 在主程式類上新增註解

  • @EnableResourceServer:表示該服務是受保護資源;
  • 該註解會強制執行一個過濾器,該過濾器會攔截對服務的所有傳入呼叫,檢查傳入呼叫的 HTTP 首部中是否存在 OAuth2 訪問令牌,然後呼叫 security.oauth2.resource.userInfoUri 中定義的回撥 URL 來檢視令牌是否有效;
  • 一旦獲悉令牌是有效的,@EnableResourceServer 註解也會應用任何訪問控制規則,以控制什麼人可以訪問服務;
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //斷路器
@EnableResourceServer //表示受保護資源
public class Application {
    //注入一個過濾器,會攔截對服務的所有傳入呼叫
    @Bean
    public Filter userContextFilter() {
        UserContextFilter userContextFilter = new UserContextFilter();
        return userContextFilter;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3.4 定義訪問控制規則

在 config 包或 security 包下;

  • 要定義訪問控制規則,需要擴充套件 ResourceServerConfigurerAdapter 類井覆蓋 configure() 方法;
  • 有多種定義方法,這裡給出常見的兩種定義示例:

3.4.1 通過驗證使用者保護服務

  • 即:只由已通過身份驗證的使用者訪問;
//必須使用該註解,且需要擴充套件 ResourceServerConfigurerAdapter 類
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    //訪問規則在 configure() 方法中定義,並且通過傳入方法的 HttpSecurity 物件配置
    @Override
    public void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests().anyRequest().authenticated();
    }
}
  • anyRequest().authenticated() 表示需要由已通過驗證的使用者訪問;

3.4.2 通過特定角色保護服務

  • 限制只有 ADMIN 使用者才能呼叫該服務的 DELETE 方法;
@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception{
        http
        .authorizeRequests()
          .antMatchers(HttpMethod.DELETE, "/v1/xxxservices/**")  //執行部開發人員限制對受保護的 URL 和 HTTP DELETE 動詞的呼叫
          .hasRole("ADMIN")  //允許訪問的角色列表
          .anyRequest()
          .authenticated();
    }
}
  • anyRequest().authenticated() 表示仍需要由已通過驗證的使用者訪問;
  • 結合本篇《2.5 為應用程式定義使用者 ID、密碼和角色》的示例,這裡使用 john.carnell USER 使用者訪問資源將被拒絕,而使用 william.woodward ADMIN 使用者訪問資源將被通過;

4. 在上下游服務中傳播 OAuth2 訪問令牌

傳播 OAuth2 訪問令牌

  • 使用者已經向 OAuth2 伺服器進行了驗證,呼叫 EagleEye Web 客戶端;
  • EagleEye Web 應用程式( OAuth2 伺服器)將通過 HTTP 首都 Authorization 新增 OAuth2 訪問令牌;
  • Zuul 將查詢許可證服務端點,然後將呼叫轉發到其中一個許可證服務的伺服器;
  • 服務閘道器需要從傳入的呼叫中複製 HTTP 首部 Authorization
  • 受保護服務使用 OAuth2 伺服器確認令牌;

4.1 配置服務閘道器的黑名單

在 Zuul 的 application.yml 的配置檔案裡;

  • 因為在整個驗證流程中,我們需要將 HTTP 首部 Authorization 傳遞上下游進行許可權認證;

  • 但在預設情況下,Zuul 不會將敏感的 HTTP 首部(如 Cookie、Set-Cokkie 和 Authorization)轉發到下游服務;

  • 需要配置 Zuul 的黑名單放行 Authorization;

    zuul:
      sensitiveHeaders: Cookie , Set-Cookie
    
  • 上述配置表示攔截 Cookie , Set-Cookie 傳遞下游,而 Authorization 會放行;

4.2 修改上游服務業務程式碼

  • 業務程式碼需要保證將 HTTP 首部 Authorization 注入服務的上下游;

4.2.1 下游服務

  • 這裡的下游服務就是受保護的服務;
  • 其構建方法同本篇的《3. 使用 OAuth2 建立並保護服務資源》

4.2.2 在上游服務中公開 OAuth2RestTemplate 類

可以在主程式類上,也可以在主程式所在包及其子包裡建立類;

  • 使該類可以被自動裝配到呼叫另一個受 OAuth2 保護的服務;

    @Bean
    public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) {
        return new OAuth2RestTemplate(details, oauth2ClientContext);
    }
    

4.2.3 在上游服務中用 OAuth2RestTemplate 來傳播 OAuth2 訪問令牌

  • 自動裝配 OAuth2RestTemplate;
@Component
public class OrganizationRestTemplateClient {
    //OAuth2RestTemplate 是標準的 RestTemplate 的增強式替代品,可處理 OAuth2 訪問令牌
    @Autowired
    OAuth2RestTemplate restTemplate;

    public Organization getOrganization(String organizationId){
        //呼叫組織服務的方式與標準的 RestTemplate 完全相同
        ResponseEntity<Organization> restExchange =
                restTemplate.exchange(
                        "http://zuulserver:5555/api/organization/v1/organizations/{organizationId}",
                        HttpMethod.GET,
                        null, Organization.class, organizationId);
        return restExchange.getBody();
    }
}

最後

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!
微服務架構 | 7.1 基於 OAuth2 的安全認證

相關文章