前言:之前的文章有講過微服務(wù)的權(quán)限系列和網(wǎng)關(guān)實現(xiàn),都是孤立存在,,本文將整合后端服務(wù)與網(wǎng)關(guān),、權(quán)限系統(tǒng)。安全權(quán)限部分的實現(xiàn)還講解了基于前置驗證的方式實現(xiàn),,但是由于與業(yè)務(wù)聯(lián)系比較緊密,,沒有具體的示例。業(yè)務(wù)權(quán)限與業(yè)務(wù)聯(lián)系非常密切,,本次的整合項目將會把這部分的操作權(quán)限校驗實現(xiàn)基于具體的業(yè)務(wù)服務(wù),。
1. 前文回顧與整合設(shè)計 在認證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計與實現(xiàn) 系列文章中,講解了在微服務(wù)架構(gòu)中Auth系統(tǒng)的授權(quán)認證和鑒權(quán),。在微服務(wù)網(wǎng)關(guān) 中,,講解了基于netflix-zuul組件實現(xiàn)的微服務(wù)網(wǎng)關(guān)。下面我們看一下這次整合的架構(gòu)圖,。
微服務(wù)架構(gòu)權(quán)限
整個流程分為兩類:
用戶尚未登錄,??蛻舳耍╳eb和移動端)發(fā)起登錄請求,,網(wǎng)關(guān)對于登錄請求直接轉(zhuǎn)發(fā)到auth服務(wù),,auth服務(wù)對用戶身份信息進行校驗(整合項目省略用戶系統(tǒng),讀者可自行實現(xiàn),,直接硬編碼返回用戶信息),,最終將身份合法的token返回給客戶端。 用戶已登錄,,請求其他服務(wù),。這種情況,客戶端的請求到達網(wǎng)關(guān),,網(wǎng)關(guān)會調(diào)用auth系統(tǒng)進行請求身份合法性的驗證,,驗證不通則直接拒絕,并返回401,;如果通過驗證,,則轉(zhuǎn)發(fā)到具體服務(wù),服務(wù)經(jīng)過過濾器,,根據(jù)請求頭部中的userId,,獲取該user的安全權(quán)限信息。利用切面,,對該接口需要的權(quán)限進行校驗,,通過則proceed,否則返回403,。
第一類其實比較簡單,,在講解認證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計與實現(xiàn) 就基本實現(xiàn),現(xiàn)在要做的是與網(wǎng)關(guān)進行結(jié)合,;第二類中,,我們新建了一個后端服務(wù),與網(wǎng)關(guān),、auth系統(tǒng)整合,。
下面對整合項目涉及到的三個服務(wù)分別介紹。網(wǎng)關(guān)和auth服務(wù)的實現(xiàn)已經(jīng)講過,,本文主要講下這兩個服務(wù)進行整合需要的改動,,還有就是對于后端服務(wù)的主要實現(xiàn)進行講解。
2. gateway實現(xiàn) 微服務(wù)網(wǎng)關(guān) 已經(jīng)基本介紹完了網(wǎng)關(guān)的實現(xiàn),,包括服務(wù)路由,、幾種過濾方式等。這一節(jié)將重點介紹實際應(yīng)用時的整合,。對于需要修改增強的地方如下:
區(qū)分暴露接口(即對外直接訪問)和需要合法身份登錄之后才能訪問的接口 暴露接口直接放行,,轉(zhuǎn)發(fā)到具體服務(wù),,如登錄、刷新token等 需要合法身份登錄之后才能訪問的接口,,根據(jù)傳入的Access token進行構(gòu)造頭部,,頭部主要包括userId等信息,可根據(jù)自己的實際業(yè)務(wù)在auth服務(wù)中進行設(shè)置,。 最后,,比較重要的一點,引入Spring Security的資源服務(wù)器配置,,對于暴露接口設(shè)置permitAll(),,其余接口進入身份合法性校驗的流程,調(diào)用auth服務(wù),,如果通過則正常繼續(xù)轉(zhuǎn)發(fā),,否則拋出異常,返回401,。
繪制的流程圖如下:
網(wǎng)關(guān)路由流程圖
2.1 permitAll實現(xiàn) 對外暴露的接口可以直接訪問,,這可以依賴配置文件,而配置文件又可以通過配置中心進行動態(tài)更新,,所以不用擔心有hard-code的問題,。 在配置文件中定義需要permitall的路徑。
1
2
3
4
5
6
auth:
permitall:
-
pattern: /login/**
-
pattern: /web/public/**
服務(wù)啟動時,,讀入相應(yīng)的Configuration,,下面的配置屬性讀取以auth開頭的配置。
1
2
3
4
5
@Bean
@ConfigurationProperties (prefix = "auth" )
public PermitAllUrlProperties getPermitAllUrlProperties () {
return new PermitAllUrlProperties();
}
當然還需要有PermitAllUrlProperties對應(yīng)的實體類,,比較簡單,,不列出來了。
2.2 加強頭部 Filter過濾器,,它是Servlet技術(shù)中最實用的技術(shù),,Web開發(fā)人員通過Filter技術(shù),對web服務(wù)器管理的所有web資源進行攔截,。這邊使用Filter進行頭部增強,,解析請求中的token,構(gòu)造統(tǒng)一的頭部信息,,到了具體服務(wù),,可以利用頭部中的userId進行操作權(quán)限獲取與判斷。
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 ;
}
//判斷是不是符合規(guī)范的頭部
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,,構(gòu)造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 {
//為了適配,設(shè)置匿名頭部
RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, ANONYMOUS_USER_ID);
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy () {
}
//...
}
上面代碼列出了頭部增強的基本處理流程,,將isPermitAllUrl的請求進行直接傳遞,,否則判斷是不是符合規(guī)范的頭部,,然后解析authorization中的token,構(gòu)造USER_ID_IN_HEADER,。最后為了適配,,設(shè)置匿名頭部。 需要注意的是,,HeaderEnhanceFilter也要進行注冊,。Spring 提供了FilterRegistrationBean類,此類提供setOrder方法,,可以為filter設(shè)置排序值,讓spring在注冊web filter之前排序后再依次注冊,。
2.3 資源服務(wù)器配置 利用資源服務(wù)器的配置,,控制哪些是暴露端點不需要進行身份合法性的校驗,直接路由轉(zhuǎn)發(fā),,哪些是需要進行身份loadAuthentication,,調(diào)用auth服務(wù)。
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,,植入身份合法性的相關(guān)驗證
@Override
public void configure (ResourceServerSecurityConfigurer resources) throws Exception {
CustomRemoteTokenServices resourceServerTokenServices = new CustomRemoteTokenServices();
//...
resources.tokenServices(resourceServerTokenServices);
}
}
資源服務(wù)器的配置大家看了筆者之前的文章應(yīng)該很熟悉,此處不過多重復(fù)講了,。關(guān)于ResourceServerSecurityConfigurer
配置類,,之前的安全系列文章已經(jīng)講過,ResourceServerTokenServices
接口,,當時我們也用到了,,只不過用的是默認的DefaultTokenServices
。這邊通過自定義的CustomRemoteTokenServices
,,植入身份合法性的相關(guān)驗證,。
當然這個配置還要引入Spring Cloud Security oauth2的相應(yīng)依賴。
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實現(xiàn) ResourceServerTokenServices
接口其中的一個實現(xiàn)是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服務(wù)的/check_token
端點以獲取一個token的校驗結(jié)果,。如果有錯誤,則說明token是不合法的,。筆者這邊的的CustomRemoteTokenServices
實現(xiàn)就是沿用該思路,。需要注意的是,筆者的項目基于Spring cloud,,auth服務(wù)是多實例的,,所以這邊使用了Netflix Ribbon獲取auth服務(wù)進行負載均衡。Spring Cloud Security添加如下默認配置,,對應(yīng)auth服務(wù)中的相應(yīng)端點,。
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
實現(xiàn),,可以參考上面講的思路以及RemoteTokenServices
,很簡單,,此處略去,。
至此,網(wǎng)關(guān)服務(wù)的增強完成,,下面看一下我們對auth服務(wù)和后端backend服務(wù)的實現(xiàn),。強調(diào)一下,為什么頭部傳遞的userId等信息需要在網(wǎng)關(guān)構(gòu)造,?讀者可以自己思考一下,,結(jié)合安全等方面,??筆者暫時不給出答案,。
3. auth整合 auth服務(wù)的整合修改,,其實沒那么多,之前對于user,、role以及permission之間的定義和關(guān)系沒有給出實現(xiàn),,這部分的sql語句已經(jīng)在auth.sql中。所以為了能給出一個完整的實例,,筆者把這部分實現(xiàn)給補充了,,主要就是user-role,role,、role-permission的相應(yīng)接口定義與實現(xiàn),,實現(xiàn)增刪改查。
讀者要是想?yún)⒖颊享椖窟M行實際應(yīng)用,,這部分完全可以根據(jù)自己的業(yè)務(wù)進行增強,,包括token的創(chuàng)建,其自定義的信息還可以在網(wǎng)關(guān)中進行統(tǒng)一處理,,構(gòu)造好之后傳遞給后端服務(wù),。
這邊的接口只是列出了需要的幾個,其他接口沒寫(因為懶,。,。)
這兩個接口也是給backend項目用來獲取相應(yīng)的userId權(quán)限。
1
2
3
4
5
6
7
8
9
//根據(jù)userId獲取用戶對應(yīng)的權(quán)限
@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) ;
//根據(jù)userId獲取用戶對應(yīng)的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) ;
好了,這邊的實現(xiàn)已經(jīng)講完了,,具體見項目中的實現(xiàn),。
4. backend項目實現(xiàn) 本節(jié)是進行實現(xiàn)一個backend的實例,后端項目主要實現(xiàn)哪些功能呢?我們考慮一下,,之前網(wǎng)關(guān)服務(wù)和auth服務(wù)所做的準備:
網(wǎng)關(guān)構(gòu)造的頭部userId(可能還有其他信息,,這邊只是示例),可以在backend獲得 轉(zhuǎn)發(fā)到backend服務(wù)的請求,,都是經(jīng)過身份合法性校驗,,或者是直接對外暴露的接口 auth服務(wù),提供根據(jù)userId進行獲取相應(yīng)的權(quán)限的接口
根據(jù)這些,,筆者繪制了一個backend的通用流程圖:
backend流程圖
上面的流程圖其實已經(jīng)非常清晰了,,首先經(jīng)過filter過濾器,填充SecurityContextHolder
的上下文,。其次,,通過切面來實現(xiàn)注解,是否需要進入切面表達式處理,。不需要的話,,直接執(zhí)行接口內(nèi)的方法;否則解析注解中需要的權(quán)限,,判斷是否有權(quán)限執(zhí)行,有的話繼續(xù)執(zhí)行,,否則返回403 forbidden,。
4.1 filter過濾器 Filter過濾器,和上面網(wǎng)關(guān)使用一樣,,攔截客戶的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("過濾器正在執(zhí)行..." );
// 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);
}
//...
}
上述代碼主要實現(xiàn)了,根據(jù)請求頭中的userId,,利用feign client獲取auth服務(wù)中的該user所具有的權(quán)限集合,。之后構(gòu)造了一個UserContext,UserContext是自定義的,,實現(xiàn)了Spring Security的UserDetails, SecurityContext
接口,。
4.2 通過切面來實現(xiàn)@PreAuth注解 基于Spring的項目,使用Spring的AOP切面實現(xiàn)注解是比較方便的一件事,,這邊我們使用了自定義的注解@PreAuth
1
2
3
4
5
6
7
@Target ({ElementType.METHOD, ElementType.TYPE})
@Retention (RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
String value () ;
}
Target用于描述注解的使用范圍,,超出范圍時編譯失敗,可以用在方法或者類上面。在運行時生效,。不了解注解相關(guān)知識的,,可以自行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 () {
}
/**
* 定制一個環(huán)繞通知,,當想獲得注解里面的屬性,,可以直接注入該注解
*
* @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);
//獲取表達式判斷的結(jié)果
boolean result = expression.getValue(operationContext, boolean .class);
if (result) {
//繼續(xù)執(zhí)行接口內(nèi)的方法
return joinPoint.proceed();
}
return "Forbidden" ;
}
}
因為Aspect作用在bean上,所以先用Component把這個類添加到容器中。@Pointcut
定義要攔截的注解,。@Around
定制一個環(huán)繞通知,,當想獲得注解里面的屬性,可以直接注入該注解,。切面表達式內(nèi)主要實現(xiàn)了,,利用Spring EL對value進行解析,將SecurityContextHolder.getContext()
轉(zhuǎn)換成標準的操作上下文,,然后解析注解中的表達式,,最后獲取對表達式判斷的結(jié)果。
1
2
3
4
5
6
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {
public CustomerSecurityExpressionRoot (Authentication authentication) {
super (authentication);
}
}
CustomerSecurityExpressionRoot
繼承的是抽象類SecurityExpressionRoot
,,而我們用到的實際表達式是定義在SecurityExpressionOperations
接口,,SecurityExpressionRoot
又實現(xiàn)了SecurityExpressionOperations
接口。不過這里面的具體判斷實現(xiàn),,Spring Security 調(diào)用的也是Spring EL,。
4.3 controller接口 下面我們看看最終接口是怎么用上面實現(xiàn)的注解。
1
2
3
4
5
@RequestMapping (value = "/test" , method = RequestMethod.GET)
@PreAuth ("hasAuthority('CREATE_COMPANY')" ) // 還可以定義很多表達式,,如hasRole('Admin')
public String test () {
return "ok" ;
}
@PreAuth
中,,可以定義的表達式很多,可以看SecurityExpressionOperations
接口中的方法,。目前筆者只是實現(xiàn)了hasAuthority()
表達式,,如果你想支持其他所有表達式,只需要構(gòu)造相應(yīng)的SecurityContextHolder
即可,。
4.4 為什么這樣設(shè)計,? 有些讀者看了上面的設(shè)計,既然好多用到了Spring Security的工具類,,肯定會問,,為什么要引入這么復(fù)雜的工具類?
其實很簡單,,首先因為SecurityExpressionOperations
接口中定義的表達式足夠多,,且較為合理,能夠覆蓋我們在平時用到的大部分場景,;其次,,筆者之前的設(shè)計是直接在注解中指定所需權(quán)限,沒有擴展性,,且可讀性差,;最后,Spring Security 4 確實引入了@PreAuthorize,@PostAuthorize
等注解,,本來想用來著,,自己嘗試了一下,發(fā)現(xiàn)對于微服務(wù)架構(gòu)這樣的接口級別的操作權(quán)限校驗不是很適合,十多個過濾器太過復(fù)雜,,而且還涉及到的Principal,、Credentials等信息,這些已經(jīng)在auth系統(tǒng)實現(xiàn)了身份合法性校驗,。筆者認為這邊的功能實現(xiàn)并不是很復(fù)雜,,需要很輕量的實現(xiàn),讀者有興趣可以試著這部分的實現(xiàn)封裝成jar包或者Spring Boot的starter,。
4.5 后期優(yōu)化 優(yōu)化的地方主要有兩點:
現(xiàn)在的設(shè)計是,,每次請求過來都會去調(diào)用auth服務(wù)獲取該user相應(yīng)的權(quán)限信息。而后端微服務(wù)數(shù)量有很多,,沒必要每個服務(wù),,或者說一個服務(wù)的多個服務(wù)實例,每次都去調(diào)用auth服務(wù),,筆者認為完全可以引入redis集群的緩存機制,,在請求到達一個服務(wù)的某個實例時,首先去查詢對應(yīng)的user的緩存中的權(quán)限,,如果沒有再調(diào)用auth服務(wù),,最后寫入redis緩存。當然,,如果權(quán)限更新了,,在auth服務(wù)肯定要delete相應(yīng)的user權(quán)限緩存。 關(guān)于被拒絕的請求,,在切面表達式中,直接返回了對象,,筆者認為可以和response status 403進行綁定,,定制返回對象的內(nèi)容,返回的response更加友好,。
5. 總結(jié) 如上,,首先講了整合的設(shè)計思路,主要包含三個服務(wù):gateway,、auth和backend demo,。整合的項目,總體比較復(fù)雜,,其中g(shù)ateway服務(wù)擴充了好多內(nèi)容,,對于暴露的接口進行路由轉(zhuǎn)發(fā),這邊引入了Spring Security 的starter,,配置資源服務(wù)器對暴露的路徑進行放行,;對于其他接口需要調(diào)用auth服務(wù)進行身份合法性校驗,保證到達backend的請求都是合法的或者公開的接口;auth服務(wù)在之前的基礎(chǔ)上,,補充了role,、permission、user相應(yīng)的接口,,供外部調(diào)用,;backend demo是新起的服務(wù),實現(xiàn)了接口級別的操作權(quán)限的校驗,,主要用到了自定義注解和Spring AOP切面,。
由于實現(xiàn)的細節(jié)實在有點多,本文限于篇幅,,只對部分重要的實現(xiàn)進行列出與講解,。如果讀者有興趣實際的應(yīng)用,可以根據(jù)實際的業(yè)務(wù)進行擴增一些信息,,如auth授權(quán)的token,、網(wǎng)關(guān)攔截請求構(gòu)造的頭部信息、注解支持的表達式等等,。
可以優(yōu)化的地方當然還有很多,,整合項目中設(shè)計不合理的地方,各位同學(xué)可以多多提意見,。
推薦閱讀 系列文章:認證鑒權(quán)與API權(quán)限控制在微服務(wù)架構(gòu)中的設(shè)計與實現(xiàn)
源碼 網(wǎng)關(guān),、auth權(quán)限服務(wù)和backend服務(wù)的整合項目地址為: GitHub:https://github.com/keets2012/microservice-integration 或者 碼云:https:///keets/microservice-integration