微服務架構中整合網關、權限服務

前言:以前的文章有講過微服務的權限系列和網關實現,都是孤立存在,本文將整合後端服務與網關、權限系統。安全權限部分的實現還講解了基於前置驗證的方式實現,可是因爲與業務聯繫比較緊密,沒有具體的示例。業務權限與業務聯繫很是密切,本次的整合項目將會把這部分的操做權限校驗實現基於具體的業務服務。web

1. 前文回顧與整合設計

認證鑑權與API權限控制在微服務架構中的設計與實現系列文章中,講解了在微服務架構中Auth系統的受權認證和鑑權。在微服務網關中,講解了基於netflix-zuul組件實現的微服務網關。下面咱們看一下此次整合的架構圖。redis

ms

整個流程分爲兩類:spring

  • 用戶還沒有登陸。客戶端(web和移動端)發起登陸請求,網關對於登陸請求直接轉發到auth服務,auth服務對用戶身份信息進行校驗(整合項目省略用戶系統,讀者可自行實現,直接硬編碼返回用戶信息),最終將身份合法的token返回給客戶端。
  • 用戶已登陸,請求其餘服務。這種狀況,客戶端的請求到達網關,網關會調用auth系統進行請求身份合法性的驗證,驗證不通則直接拒絕,並返回401;若是經過驗證,則轉發到具體服務,服務通過過濾器,根據請求頭部中的userId,獲取該user的安全權限信息。利用切面,對該接口須要的權限進行校驗,經過則proceed,不然返回403。

第一類其實比較簡單,在講解認證鑑權與API權限控制在微服務架構中的設計與實現就基本實現,如今要作的是與網關進行結合;第二類中,咱們新建了一個後端服務,與網關、auth系統整合。sql

下面對整合項目涉及到的三個服務分別介紹。網關和auth服務的實現已經講過,本文主要講下這兩個服務進行整合須要的改動,還有就是對於後端服務的主要實現進行講解。express

2. gateway實現

微服務網關已經基本介紹完了網關的實現,包括服務路由、幾種過濾方式等。這一節將重點介紹實際應用時的整合。對於須要修改加強的地方以下:後端

  • 區分暴露接口(即對外直接訪問)和須要合法身份登陸以後才能訪問的接口
  • 暴露接口直接放行,轉發到具體服務,如登陸、刷新token等
  • 須要合法身份登陸以後才能訪問的接口,根據傳入的Access token進行構造頭部,頭部主要包括userId等信息,可根據本身的實際業務在auth服務中進行設置。
  • 最後,比較重要的一點,引入Spring Security的資源服務器配置,對於暴露接口設置permitAll(),其他接口進入身份合法性校驗的流程,調用auth服務,若是經過則正常繼續轉發,不然拋出異常,返回401。

繪製的流程圖以下:api

gwflow

2.1 permitAll實現

對外暴露的接口能夠直接訪問,這能夠依賴配置文件,而配置文件又能夠經過配置中心進行動態更新,因此不用擔憂有hard-code的問題。
在配置文件中定義須要permitall的路徑。緩存

1
2
3
4
5
6
auth:
permitall:
-
pattern: /login/**
-
pattern: /web/public/**

服務啓動時,讀入相應的Configuration,下面的配置屬性讀取以auth開頭的配置。安全

1
2
3
4
5
@Bean
@ConfigurationProperties(prefix = "auth")
public PermitAllUrlProperties getPermitAllUrlProperties() {
return new PermitAllUrlProperties();
}

固然還須要有PermitAllUrlProperties對應的實體類,比較簡單,不列出來了。服務器

2.2 增強頭部

Filter過濾器,它是Servlet技術中最實用的技術,Web開發人員經過Filter技術,對web服務器管理的全部web資源進行攔截。這邊使用Filter進行頭部加強,解析請求中的token,構造統一的頭部信息,到了具體服務,能夠利用頭部中的userId進行操做權限獲取與判斷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class HeaderEnhanceFilter implements Filter {

//...

@Autowired
private PermitAllUrlProperties permitAllUrlProperties;

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

//主要的過濾方法
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String authorization = ((HttpServletRequest) servletRequest).getHeader("Authorization");
String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
// test if request url is permit all , then remove authorization from header
LOGGER.info(String.format("Enhance request URI : %s.", requestURI));
//將isPermitAllUrl的請求進行傳遞
if(isPermitAllUrl(requestURI) && isNotOAuthEndpoint(requestURI)) {
//移除頭部,但不包括登陸端點的頭部
HttpServletRequest resetRequest = removeValueFromRequestHeader((HttpServletRequest) servletRequest);
filterChain.doFilter(resetRequest, servletResponse);
return;
}
//判斷是否是符合規範的頭部
if (StringUtils.isNotEmpty(authorization)) {
if (isJwtBearerToken(authorization)) {
try {
authorization = StringUtils.substringBetween(authorization, ".");
String decoded = new String(Base64.decodeBase64(authorization));

Map properties = new ObjectMapper().readValue(decoded, Map.class);
//解析authorization中的token,構造USER_ID_IN_HEADER
String userId = (String) properties.get(SecurityConstants.USER_ID_IN_HEADER);

RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, userId);
} catch (Exception e) {
LOGGER.error("Failed to customize header for the request", e);
}
}
} else {
//爲了適配,設置匿名頭部
RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, ANONYMOUS_USER_ID);
}

filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}

//...

}

上面代碼列出了頭部加強的基本處理流程,將isPermitAllUrl的請求進行直接傳遞,不然判斷是否是符合規範的頭部,而後解析authorization中的token,構造USER_ID_IN_HEADER。最後爲了適配,設置匿名頭部。
須要注意的是,HeaderEnhanceFilter也要進行註冊。Spring 提供了FilterRegistrationBean類,此類提供setOrder方法,能夠爲filter設置排序值,讓spring在註冊web filter以前排序後再依次註冊。

2.3 資源服務器配置

利用資源服務器的配置,控制哪些是暴露端點不須要進行身份合法性的校驗,直接路由轉發,哪些是須要進行身份loadAuthentication,調用auth服務。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

//...
//配置permitAll的請求pattern,依賴於permitAllUrlProperties對象
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests()
.antMatchers(permitAllUrlProperties.getPermitallPatterns()).permitAll()
.anyRequest().authenticated();
}

//經過自定義的CustomRemoteTokenServices,植入身份合法性的相關驗證
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
CustomRemoteTokenServices resourceServerTokenServices = new CustomRemoteTokenServices();
//...
resources.tokenServices(resourceServerTokenServices);
}
}

資源服務器的配置你們看了筆者以前的文章應該很熟悉,此處不過多重複講了。關於ResourceServerSecurityConfigurer配置類,以前的安全系列文章已經講過,ResourceServerTokenServices接口,當時咱們也用到了,只不過用的是默認的DefaultTokenServices。這邊經過自定義的CustomRemoteTokenServices,植入身份合法性的相關驗證。

固然這個配置還要引入Spring Cloud Security oauth2的相應依賴。

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

2.4 自定義RemoteTokenServices實現

ResourceServerTokenServices接口其中的一個實現是RemoteTokenServices

Queries the /check_token endpoint to obtain the contents of an access token.
If the endpoint returns a 400 response, this indicates that the token is invalid.

RemoteTokenServices主要是查詢auth服務的/check_token端點以獲取一個token的校驗結果。若是有錯誤,則說明token是不合法的。筆者這邊的的CustomRemoteTokenServices實現就是沿用該思路。須要注意的是,筆者的項目基於Spring cloud,auth服務是多實例的,因此這邊使用了Netflix Ribbon獲取auth服務進行負載均衡。Spring Cloud Security添加以下默認配置,對應auth服務中的相應端點。

1
2
3
4
5
6
7
8
9
security:
oauth2:
client:
accessTokenUri: /oauth/token
clientId: gateway
clientSecret: gateway
resource:
userInfoUri: /user
token-info-uri: /oauth/check_token

至於具體的CustomRemoteTokenServices實現,能夠參考上面講的思路以及RemoteTokenServices,很簡單,此處略去。

至此,網關服務的加強完成,下面看一下咱們對auth服務和後端backend服務的實現。
強調一下,爲何頭部傳遞的userId等信息須要在網關構造?讀者能夠本身思考一下,結合安全等方面,😆筆者暫時不給出答案。

3. auth整合

auth服務的整合修改,其實沒那麼多,以前對於user、role以及permission之間的定義和關係沒有給出實現,這部分的sql語句已經在auth.sql中。因此爲了能給出一個完整的實例,筆者把這部分實現給補充了,主要就是user-role,role、role-permission的相應接口定義與實現,實現增刪改查。

讀者要是想參考整合項目進行實際應用,這部分徹底能夠根據本身的業務進行加強,包括token的建立,其自定義的信息還能夠在網關中進行統一處理,構造好以後傳遞給後端服務。

這邊的接口只是列出了須要的幾個,其餘接口沒寫(由於懶。。)

這兩個接口也是給backend項目用來獲取相應的userId權限。

1
2
3
4
5
6
7
8
9
//根據userId獲取用戶對應的權限
@RequestMapping(method = RequestMethod.GET, value = "/api/userPermissions?userId={userId}",
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
List<Permission> getUserPermissions(@RequestParam("userId") String userId);

//根據userId獲取用戶對應的accessLevel(好像暫時沒用到。。)
@RequestMapping(method = RequestMethod.GET, value = "/api/userAccesses?userId={userId}",
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
List<UserAccess> getUserAccessList(@RequestParam("userId") String userId);

好了,這邊的實現已經講完了,具體見項目中的實現。

4. backend項目實現

本節是進行實現一個backend的實例,後端項目主要實現哪些功能呢?咱們考慮一下,以前網關服務和auth服務所作的準備:

  • 網關構造的頭部userId(可能還有其餘信息,這邊只是示例),能夠在backend得到
  • 轉發到backend服務的請求,都是通過身份合法性校驗,或者是直接對外暴露的接口
  • auth服務,提供根據userId進行獲取相應的權限的接口

根據這些,筆者繪製了一個backend的通用流程圖:

bf

上面的流程圖其實已經很是清晰了,首先通過filter過濾器,填充SecurityContextHolder的上下文。其次,經過切面來實現註解,是否須要進入切面表達式處理。不須要的話,直接執行接口內的方法;不然解析註解中須要的權限,判斷是否有權限執行,有的話繼續執行,不然返回403 forbidden。

4.1 filter過濾器

Filter過濾器,和上面網關使用同樣,攔截客戶的HttpServletRequest。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class AuthorizationFilter implements Filter {

@Autowired
private FeignAuthClient feignAuthClient;

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("過濾器正在執行...");
// pass the request along the filter chain
String userId = ((HttpServletRequest) servletRequest).getHeader(SecurityConstants.USER_ID_IN_HEADER);

if (StringUtils.isNotEmpty(userId)) {
UserContext userContext = new UserContext(UUID.fromString(userId));
userContext.setAccessType(AccessType.ACCESS_TYPE_NORMAL);

List<Permission> permissionList = feignAuthClient.getUserPermissions(userId);
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (Permission permission : permissionList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority();
authority.setAuthority(permission.getPermission());
authorityList.add(authority);
}

CustomAuthentication userAuth = new CustomAuthentication();
userAuth.setAuthorities(authorityList);
userContext.setAuthorities(authorityList);
userContext.setAuthentication(userAuth);
SecurityContextHolder.setContext(userContext);
}
filterChain.doFilter(servletRequest, servletResponse);
}

//...
}

上述代碼主要實現了,根據請求頭中的userId,利用feign client獲取auth服務中的該user所具備的權限集合。以後構造了一個UserContext,UserContext是自定義的,實現了Spring Security的UserDetails, SecurityContext接口。

4.2 經過切面來實現@PreAuth註解

基於Spring的項目,使用Spring的AOP切面實現註解是比較方便的一件事,這邊咱們使用了自定義的註解@PreAuth

1
2
3
4
5
6
7
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
String value();
}

Target用於描述註解的使用範圍,超出範圍時編譯失敗,能夠用在方法或者類上面。在運行時生效。不瞭解註解相關知識的,能夠自行Google。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
@Aspect
public class AuthAspect {


@Pointcut("@annotation(com.blueskykong.auth.demo.annotation.PreAuth)")
private void cut() {
}

/**
* 定製一個環繞通知,當想得到註解裏面的屬性,能夠直接注入該註解
*
* @param joinPoint
* @param preAuth
*/
@Around("cut()&&@annotation(preAuth)")
public Object record(ProceedingJoinPoint joinPoint, PreAuth preAuth) throws Throwable {
//取出註解中的表達式
String value = preAuth.value();
//Spring EL 對value進行解析
SecurityExpressionOperations operations = new CustomerSecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication());
StandardEvaluationContext operationContext = new StandardEvaluationContext(operations);
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(value);
//獲取表達式判斷的結果
boolean result = expression.getValue(operationContext, boolean.class);
if (result) {
//繼續執行接口內的方法
return joinPoint.proceed();
}
return "Forbidden";
}
}

由於Aspect做用在bean上,因此先用Component把這個類添加到容器中。@Pointcut定義要攔截的註解。@Around定製一個環繞通知,當想得到註解裏面的屬性,能夠直接注入該註解。切面表達式內主要實現了,利用Spring EL對value進行解析,將SecurityContextHolder.getContext()轉換成標準的操做上下文,而後解析註解中的表達式,最後獲取對錶達式判斷的結果。

1
2
3
4
5
6
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {

public CustomerSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
}

CustomerSecurityExpressionRoot繼承的是抽象類SecurityExpressionRoot,而咱們用到的實際表達式是定義在SecurityExpressionOperations接口,SecurityExpressionRoot又實現了SecurityExpressionOperations接口。不過這裏面的具體判斷實現,Spring Security 調用的也是Spring EL。

4.3 controller接口

下面咱們看看最終接口是怎麼用上面實現的註解。

1
2
3
4
5
@RequestMapping(value = "/test", method = RequestMethod.GET)
@PreAuth("hasAuthority('CREATE_COMPANY')") // 還能夠定義不少表達式,如hasRole('Admin')
public String test() {
return "ok";
}

@PreAuth中,能夠定義的表達式不少,能夠看SecurityExpressionOperations接口中的方法。目前筆者只是實現了hasAuthority()表達式,若是你想支持其餘全部表達式,只須要構造相應的SecurityContextHolder便可。

4.4 爲何這樣設計?

有些讀者看了上面的設計,既然好多用到了Spring Security的工具類,確定會問,爲何要引入這麼複雜的工具類?

其實很簡單,首先由於SecurityExpressionOperations接口中定義的表達式足夠多,且較爲合理,可以覆蓋咱們在平時用到的大部分場景;其次,筆者以前的設計是直接在註解中指定所需權限,沒有擴展性,且可讀性差;最後,Spring Security 4 確實引入了@PreAuthorize,@PostAuthorize等註解,原本想用來着,本身嘗試了一下,發現對於微服務架構這樣的接口級別的操做權限校驗不是很適合,十多個過濾器太過複雜,並且還涉及到的Principal、Credentials等信息,這些已經在auth系統實現了身份合法性校驗。筆者認爲這邊的功能實現並非很複雜,須要很輕量的實現,讀者有興趣能夠試着這部分的實現封裝成jar包或者Spring Boot的starter。

4.5 後期優化

優化的地方主要有兩點:

  • 如今的設計是,每次請求過來都會去調用auth服務獲取該user相應的權限信息。然後端微服務數量有不少,不必每一個服務,或者說一個服務的多個服務實例,每次都去調用auth服務,筆者認爲徹底能夠引入redis集羣的緩存機制,在請求到達一個服務的某個實例時,首先去查詢對應的user的緩存中的權限,若是沒有再調用auth服務,最後寫入redis緩存。固然,若是權限更新了,在auth服務確定要delete相應的user權限緩存。
  • 關於被拒絕的請求,在切面表達式中,直接返回了對象,筆者認爲能夠和response status 403進行綁定,定製返回對象的內容,返回的response更加友好。

5. 總結

如上,首先講了整合的設計思路,主要包含三個服務:gateway、auth和backend demo。整合的項目,整體比較複雜,其中gateway服務擴充了好多內容,對於暴露的接口進行路由轉發,這邊引入了Spring Security 的starter,配置資源服務器對暴露的路徑進行放行;對於其餘接口須要調用auth服務進行身份合法性校驗,保證到達backend的請求都是合法的或者公開的接口;auth服務在以前的基礎上,補充了role、permission、user相應的接口,供外部調用;backend demo是新起的服務,實現了接口級別的操做權限的校驗,主要用到了自定義註解和Spring AOP切面。

因爲實現的細節實在有點多,本文限於篇幅,只對部分重要的實現進行列出與講解。若是讀者有興趣實際的應用,能夠根據實際的業務進行擴增一些信息,如auth受權的token、網關攔截請求構造的頭部信息、註解支持的表達式等等。

能夠優化的地方固然還有不少,整合項目中設計不合理的地方,各位同窗能夠多多提意見。

相關文章
相關標籤/搜索