引言: 以前系列文章《認證鑑權與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)
複製代碼
因爲受權碼模式須要登陸用戶給請求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
中,註釋掉ResourceServerConfig
的HttpSecurity
配置: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(); } */
.....
}
複製代碼
因爲用戶表單登陸的認證過程可能有所不一樣,爲此再添加一個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權限控制在微服務架構中的設計與實現