歡迎光臨
每天分享高質量文章

認證鑒權與API許可權控制在微服務架構中的設計與實現:授權碼樣式

之前系列文章《認證鑒權與API許可權控制在微服務架構中的設計與實現》,前面文章已經將認證鑒權與API許可權控制的流程和主要細節講解完。由於有些同學想瞭解下授權碼樣式,本文特地補充講解。

授權碼型別介紹

授權碼型別(authorization code)透過重定向的方式讓資源所有者直接與授權伺服器進行互動來進行授權,避免了資源所有者資訊洩漏給客戶端,是功能最完整、流程最嚴密的授權型別,但是需要客戶端必須能與資源所有者的代理(通常是Web瀏覽器)進行互動,和可從授權伺服器中接受請求(重定向給予授權碼),授權流程如下:
+----------+
| Resource |
|   Owner  |
|          |
+----------+
    ^
    |
   (B)
+----|
-----+          Client Identifier      +---------------+
|         -+----(A)-- & Redirection URI ---->|               |
|
 User-   |                                 | Authorization |
|
 Agent  -+----(B)-- User authenticates --->|     Server    |
|          |                                 |               |
|         -+----(C)-- Authorization Code ---               |
+-|
----|---+                                 +---------------+
 |
   |                                         ^      v
(A)  (C)                                        |
     |
 |
   |                                         |      |
 ^    v                                         |
     |
+---------+                                      |
     |
|
        |>---(D)-- Authorization Code ---------'      |
|  Client |          & Redirection URI                  |
|
        |                                             |
|         |'
+---------+       (w/ Optional Refresh Token)

  1. 客戶端引導資源所有者的使用者代理到授權伺服器的endpoint,一般透過重定向的方式。客戶端提交的資訊應包含客戶端標識(client identifier)、請求範圍(requested scope)、本地狀態(local state)和用於傳回授權碼的重定向地址(redirection URI)

  2. 授權伺服器認證資源所有者(透過使用者代理),並確認資源所有者允許還是拒絕客戶端的訪問請求

  3. 如果資源所有者授予客戶端訪問許可權,授權伺服器透過重定向使用者代理的方式回呼客戶端提供的重定向地址,併在重定向地址中新增授權碼和客戶端先前提供的任何本地狀態

  4. 客戶端攜帶上一步獲得的授權碼向授權伺服器請求訪問令牌。在這一步中授權碼和客戶端都要被授權伺服器進行認證。客戶端需要提交用於獲取授權碼的重定向地址

  5. 授權伺服器對客戶端進行身份驗證,和認證授權碼,確保接收到的重定向地址與第三步中用於的獲取授權碼的重定向地址相匹配。如果有效,傳回訪問令牌,可能會有掃清令牌(Refresh Token)

快速入門

Spring-Securiy 配置
由於授權碼樣式需要登入使用者給請求access_token的客戶端授權,所以auth-server需要新增Spring-Security的相關配置用於引導使用者進行登入。
在原來的基礎上,進行Spring-Securiy相關配置,允許使用者進行表單登入:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   CustomLogoutHandler customLogoutHandler;
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.csrf().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
               .and()
               .requestMatchers().antMatchers("/**")
               .and().authorizeRequests()
               .antMatchers("/**").permitAll()
               .anyRequest().authenticated()
               .and().formLogin()
               .permitAll()
               .and().logout()
               .logoutUrl("/logout")
               .clearAuthentication(true)
               .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
               .addLogoutHandler(customLogoutHandler);
   }
}
同時需要把ResourceServerConfig中的資源伺服器中的對於登出埠的處理遷移到WebSecurityConfig中,註釋掉ResourceServerConfig的HttpSecurity配置:
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//    @Override
//    public void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable()
//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                .and()
//                .requestMatchers().antMatchers("/**")
//                .and().authorizeRequests()
//                .antMatchers("/**").permitAll()
//                .anyRequest().authenticated()
//                .and().logout()
//                .logoutUrl("/logout")
//                .clearAuthentication(true)
//                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
//                .addLogoutHandler(customLogoutHandler());
//
//        //http.antMatcher("/api/**").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);
//
//    }
/*   @Bean
   public CustomSecurityFilter customSecurityFilter() {
       return new CustomSecurityFilter();
   }
*/

.....
}
AuthenticationProvider
由於使用者表單登入的認證過程可能有所不同,為此再新增一個CustomSecurityAuthenticationProvider,基本上與CustomAuthenticationProvider一致,只是忽略對client客戶端的認證和處理。
@Component
public class CustomSecurityAuthenticationProvider implements AuthenticationProvider {
   @Autowired
   private UserClient userClient;
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       String username = authentication.getName();
       String password;
       Map map;
       password = (String) authentication.getCredentials();
       //如果你是呼叫user服務,這邊不用註掉
       //map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
       map = checkUsernameAndPassword(getUserServicePostObject(username, password));
       String userId = (String) map.get("userId");
       if (StringUtils.isBlank(userId)) {
           String errorCode = (String) map.get("code");
           throw new BadCredentialsException(errorCode);
       }
       CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId);
       return new CustomAuthenticationToken(customUserDetails);
   }
   private CustomUserDetails buildCustomUserDetails(String username, String password, String userId) {
       CustomUserDetails customUserDetails = new CustomUserDetails.CustomUserDetailsBuilder()
               .withUserId(userId)
               .withPassword(password)
               .withUsername(username)
               .withClientId("for Security")
               .build();
       return customUserDetails;
   }
   private Map<String, String> getUserServicePostObject(String username, String password) {
       Map<String, String> requestParam = new HashMap<String, String>();
       requestParam.put("userName", username);
       requestParam.put("password", password);
       return requestParam;
   }
   //模擬呼叫user服務的方法
   private Map checkUsernameAndPassword(Map map) {
       //checkUsernameAndPassword
       Map ret = new HashMap();
       ret.put("userId", UUID.randomUUID().toString());
       return ret;
   }
   @Override
   public boolean supports(Class> aClass) {
       return true;
   }
}
在AuthenticationManagerConfig新增CustomSecurityAuthenticationProvider配置:
@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
   @Autowired
   CustomAuthenticationProvider customAuthenticationProvider;
   @Autowired
   CustomSecurityAuthenticationProvider securityAuthenticationProvider;
   @Override
   public void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.authenticationProvider(customAuthenticationProvider)
               .authenticationProvider(securityAuthenticationProvider);
   }
}
保證資料庫中的請求客戶端存在授權碼的請求授權和具備回呼地址,回呼地址是用來接受授權碼的。

測試使用
啟動服務,瀏覽器訪問地址http://localhost:9091/oauth/authorize?response_type=code&client;_id=frontend& scope=all&redirect;_uri=http://localhost:8080。
重定向到登入介面,引導使用者登入:

登入成功,授權客戶端獲取授權碼。

授權之後,從回呼地址中獲取到授權碼:
http://localhost:8080/?code=7OglOJ
攜帶授權碼獲取對應的token:

原始碼詳解

AuthorizationServerTokenServices是授權伺服器中進行token操作的介面,提供了以下的三個介面:
public interface AuthorizationServerTokenServices {
   // 生成與OAuth2認證系結的access_token
   OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
   // 根據refresh_token掃清access_token
   OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
           throws AuthenticationException
;
   // 獲取OAuth2認證的access_token,如果access_token存在的話
   OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}
請註意,生成的token都是與授權的使用者進行系結的。
AuthorizationServerTokenServices介面的預設實現是DefaultTokenServices,註意token透過TokenStore進行儲存管理。
生成token:
//DefaultTokenServices
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
   // 從TokenStore獲取access_token
   OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
   OAuth2RefreshToken refreshToken = null;
   if (existingAccessToken != null) {
       if (existingAccessToken.isExpired()) {
           // 如果access_token已經存在但是過期了
           // 刪除對應的access_token和refresh_token
           if (existingAccessToken.getRefreshToken() != null) {
               refreshToken = existingAccessToken.getRefreshToken();
               tokenStore.removeRefreshToken(refreshToken);
           }
           tokenStore.removeAccessToken(existingAccessToken);
       }
       else {
           // 如果access_token已經存在並且沒有過期
           // 重新儲存一下防止authentication改變,並且傳回該access_token
           tokenStore.storeAccessToken(existingAccessToken, authentication);
           return existingAccessToken;
       }
   }
   // 只有當refresh_token為null時,才重新建立一個新的refresh_token
   // 這樣可以使持有過期access_token的客戶端可以根據以前拿到refresh_token拿到重新建立的access_token
   // 因為建立的access_token需要系結refresh_token
   if (refreshToken == null) {
       refreshToken = createRefreshToken(authentication);
   }else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
       // 如果refresh_token也有期限並且過期,重新建立
       ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
       if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
           refreshToken = createRefreshToken(authentication);
       }
   }
   // 系結授權使用者和refresh_token建立新的access_token
   OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
   // 將access_token與授權使用者對應儲存
   tokenStore.storeAccessToken(accessToken, authentication);
   // In case it was modified
   refreshToken = accessToken.getRefreshToken();
   if (refreshToken != null) {
       // 將refresh_token與授權使用者對應儲存
       tokenStore.storeRefreshToken(refreshToken, authentication);
   }
   return accessToken;
}
需要註意到,在建立token的過程中,會根據該授權使用者去查詢是否存在未過期的access_token,有就直接傳回,沒有的話才會重新建立新的access_token,同時也應該註意到是先建立refresh_token,再去建立access_token,這是為了防止持有過期的access_token能夠透過refresh_token重新獲得access_token,因為前後建立access_token系結了同一個refresh_token。
DefaultTokenServices中掃清token的refreshAccessToken()以及獲取token的getAccessToken()方法就留給讀者們自己去檢視,在此不介紹。

小結

本文主要講了授權碼樣式,在授權碼樣式需要使用者登入之後進行授權才獲取獲取授權碼,再攜帶授權碼去向TokenEndpoint請求訪問令牌,當然也可以在請求中設定response_token=token透過隱式型別直接獲取到access_token。這裡需要註意一個問題,在到達AuthorizationEndpoint端點時,並沒有對客戶端進行驗證,但是必須要經過使用者認證的請求才能被接受。
參考
Spring Security Reference:https://docs.spring.io/spring-security/site/docs/5.0.3.RELEASE/reference/htmlsingle/
原文連結:http://blueskykong.com/2018/04/02/security6/

Kubernetes零基礎進階培訓

本次培訓內容包括:容器原理、Docker架構及工作原理、Docker網路與儲存方案、Harbor、Kubernetes架構、元件、核心機制、外掛、核心模組、Kubernetes網路與儲存、監控、日誌、二次開發以及實踐經驗等,點選瞭解具體培訓內容

4月20日正式上課,點選閱讀原文連結即可報名。
贊(0)

分享創造快樂