認證鑑權與API權限控制在微服務架構中的設計與實現:受權碼模式

引言: 以前系列文章《認證鑑權與API權限控制在微服務架構中的設計與實現》,前面文章已經將認證鑑權與API權限控制的流程和主要細節講解完。因爲有些同窗想了解下受權碼模式,本文特意補充講解。html

受權碼類型介紹

受權碼類型(authorization code)經過重定向的方式讓資源全部者直接與受權服務器進行交互來進行受權,避免了資源全部者信息泄漏給客戶端,是功能最完整、流程最嚴密的受權類型,可是須要客戶端必須能與資源全部者的代理(一般是Web瀏覽器)進行交互,和可從受權服務器中接受請求(重定向給予受權碼),受權流程以下:java

+----------+
 | 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                  |
 |         |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (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

在原來的基礎上,進行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中,註釋掉ResourceServerConfigHttpSecurity配置:json

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客戶端的認證和處理。api

@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微信

重定向到登陸界面,引導用戶登陸:session

登陸成功,受權客戶端獲取受權碼。

受權以後,從回調地址中獲取到受權碼:

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端點時,並無對客戶端進行驗證,可是必需要通過用戶認證的請求才能被接受。

訂閱最新文章,歡迎關注個人公衆號

微信公衆號

推薦閱讀

系列文章:認證鑑權與API權限控制在微服務架構中的設計與實現

參考

spring-security

相關文章
相關標籤/搜索