OAuth2授權概述
在Spring Cloud Security 中,認證和授權都是通過FilterChainProxy(Servlet Filter過濾器)攔截然後進行操作的。在Spring Security中FilterSecurityInterceptor 過濾器會對資源受保護的Http請求進行攔截,然後進行授權處理。其部分源碼如下:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
/**
* 授權過濾器攔截邏輯
*/
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
/**
* 設定權限資訊擷取服務FilterInvocationSecurityMetadataSource
*/
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 授權邏輯校驗
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 調用下一個過濾器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
// 資源伺服器的通路
super.finallyInvocation(token);
}
// 調用結束後的處理
super.afterInvocation(token, null);
}
}
/**
* Indicates whether once-per-request handling will be observed. By default this is
* <code>true</code>, meaning the <code>FilterSecurityInterceptor</code> will only
* execute once-per-request. Sometimes users may wish it to execute more than once per
* request, such as when JSP forwards are being used and filter security is desired on
* each included fragment of the HTTP request.
*
* @return <code>true</code> (the default) if once-per-request is honoured, otherwise
* <code>false</code> if <code>FilterSecurityInterceptor</code> will enforce
* authorizations for each and every fragment of the HTTP request.
*/
public boolean isObserveOncePerRequest() {
return observeOncePerRequest;
}
public void setObserveOncePerRequest(boolean observeOncePerRequest) {
this.observeOncePerRequest = observeOncePerRequest;
}
}
FilterSecurityInterceptor 攔截處理的大緻流程如下:
- 處理授權邏輯校驗
- 調用餘下的過濾器
- 授權成功後,通路真正的資源伺服器請求。
在第一步授權邏輯的校驗邏輯中,調用的是在父類的AbstractSecurityInterceptor的beforeInvocation方法實作的,大緻流程如下:
- 使用SecurityMetadataSource根據http請求擷取對應擁有的權限。
- 使用Spring Security授權子產品對使用者通路的資源進行授權驗證。
AbstractSecurityInterceptor的部分源碼如下:
// AbstractSecurityInterceptor.java
protected InterceptorStatusToken beforeInvocation(Object object) {
......
// 根據http請求擷取對應的配置的權限資訊
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
......
// 對使用者認證進行校驗
Authentication authenticated = authenticateIfRequired();
try {
// 對使用者的權限與通路資源擁有的權限進行校驗
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
......
}
資源伺服器配置權限擷取邏輯
在FilterSecurityInterceptor中FilterInvocationSecurityMetadataSource,用于擷取資源擁有的授權資訊。在其預設子類DefaultFilterInvocationSecurityMetadataSource 實作類中的源碼如下:
public class DefaultFilterInvocationSecurityMetadataSource implements
FilterInvocationSecurityMetadataSource {
/**
* 請求與擁有權限的映射
*/
private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
/**
* 擷取資源伺服器擁有的全部權限
*/
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<>();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
allAttributes.addAll(entry.getValue());
}
return allAttributes;
}
/**
* 根據請求擷取資源伺服器擁有的權限
*/
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
授權處理邏輯
在Spring Security中,對于授權處理的邏輯,通過AccessDecisionManager接口實作的,源碼如下:
public interface AccessDecisionManager {
/**
* authentication 認證以後擁有的權限
* object 授權的Url資訊
* configAttributes url路徑權限屬性
*/
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AccessDecisionManager的實作類自定義授權邏輯的具體實作,其常見的實作類如下:
- AffirmativeBased:隻要有一個授權處理通過則可以進行通路(預設使用的類)。
- ConsensusBased:根據少數服務多數的原則進行判斷。
- UnanimousBased:隻要有一個授權不通過,則不能通路。
AccessDecisionManager的公共子類AbstractAccessDecisionManager中包含一個AccessDecisionVoter清單,用于組合處理授權邏輯,AccessDecisionVoter接口定義如下:
public interface AccessDecisionVoter<S> {
// 授權通過
int ACCESS_GRANTED = 1;
// 授權忽略
int ACCESS_ABSTAIN = 0;
// 授權拒絕
int ACCESS_DENIED = -1;
/**
* 支援的路徑授權屬性
*/
boolean supports(ConfigAttribute attribute);
/**
*支援的類
*/
boolean supports(Class<?> clazz);
/**
* 授權方法,authentication為使用者認證過後的認證資訊,object為url路徑資訊,attributes為路徑配置的授權資訊
*/
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
AccessDecisionVoter的常見的實作類如下:
- RoleVoter(根據角色授權處理)
- AuthenticatedVoter(認證授權處理)
- webExpressionVoter(描述語言的授權處理)
資源伺服器的搭建
在Spring Cloud Security資源伺服器的搭建中,通過注解@EnableResourceServer開啟資源伺服器的預設配置,可以繼承ResourceServerConfigurerAdapter自定義資源伺服器的邏輯。主要有兩方面的配置:
- ResourceServerSecurityConfigurer,用于配置資源伺服器的安全配置,例如,通路令牌的校驗。
- HttpSecurity,用于配置資源伺服器授權邏輯。例如,擁有的權限配置,授權邏輯的自定義。
個人demo實作中,使用redis存儲token,RemoteTokenServices遠端服務調用方式通路認證伺服器的check_token方法,jwt方式進行token轉換,靜态的配置了通路資源的權限,源碼如下:
public class Oauth2ResourcesConfig extends ResourceServerConfigurerAdapter {
/**
* redis連接配接工廠
*/
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* jwt
*/
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
*
*/
@Autowired
private RestTemplate restTemplate;
/**
* 資源伺服器安全配置
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setAccessTokenConverter(jwtAccessTokenConverter);
tokenServices.setRestTemplate(restTemplate);
tokenServices.setClientId("meituan");
tokenServices.setClientSecret("123456");
// TODO 本地啟動使用 RestTemplate通過服務名會通路80端口
// tokenServices.setCheckTokenEndpointUrl("http://oauth2-server/oauth/check_token");
tokenServices.setCheckTokenEndpointUrl("http://localhost:8766/oauth/check_token");
resources
.resourceId("coupon")
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.tokenServices(tokenServices)
// 通路無狀态
.stateless(true);
}
@Autowired
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* 資源伺服器内的資源通路控制
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// session配置,微服務中配置為無狀态
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 授權配置
.and().authorizeRequests()
// 無需認證授權即可通路
.antMatchers("/coupon/demo2", "/coupon/demo3").permitAll()
// 角色設定
.antMatchers("/user/**").hasAnyRole("user")
// 權限設定
.antMatchers("/coupon/demo").hasAuthority("couponDemo")
// 剩餘所有請求都需要身份認證才能通路
.anyRequest().authenticated();
}
/**
* jwt token 配置
*/
@Configuration
public static class JwtTokenConfig {
/**
* JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("kuqi-mall");
return converter;
}
}
}
授權測試
測試無需授權的api "/coupon/demo2"和 "/coupon/demo3",直接通路"/coupon/demo2"和 "/coupon/demo3"位址,無需token,可以直接放回結果:
測試通路"/coupon/demo",需要進行身份認證,并且帶有couponDemo權限才可以通路。在db中配置user(使用者),role(角色),permission(權限)的關系資料,配置了使用者username為admin,password為123456,擁有couponDemo的權限。首先擷取使用者的通路授權碼access token,流程如下:
根據傳回的通路授權碼,通路"/coupon/demo",成功傳回測試資料,流程如下:
不足與優化之處
Spring Cloud Security資源伺服器的授權處理,在以上的示例中屬于在靜态加載,在啟動資源服務時,會全部加到記憶體。在資源伺服器運作期間,如果需要修改資源擁有的權限,該如何處理呢?請關注後續的Spring Cloud Security動态權限配置章節。