前言
《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;
- 客戶端根據認證令牌向資源伺服器申請相關資源;
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
- 考慮到篇幅有限,JWT 相關令牌儲存將在《微服務架構 | 7.2 構建使用 JWT 令牌儲存的 OAuth2 安全認證》中講解;
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 伺服器進行了驗證,呼叫 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();
}
}