前言:以前的文章有講過微服務的權限系列和網關實現,都是孤立存在,本文將整合後端服務與網關、權限系統。安全權限部分的實現還講解了基於前置驗證的方式實現,可是因爲與業務聯繫比較緊密,沒有具體的示例。業務權限與業務聯繫很是密切,本次的整合項目將會把這部分的操做權限校驗實現基於具體的業務服務。java
在認證鑑權與API權限控制在微服務架構中的設計與實現系列文章中,講解了在微服務架構中Auth系統的受權認證和鑑權。在微服務網關中,講解了基於netflix-zuul組件實現的微服務網關。下面咱們看一下此次整合的架構圖。git
整個流程分爲兩類:github
第一類其實比較簡單,在講解認證鑑權與API權限控制在微服務架構中的設計與實現就基本實現,如今要作的是與網關進行結合;第二類中,咱們新建了一個後端服務,與網關、auth系統整合。web
下面對整合項目涉及到的三個服務分別介紹。網關和auth服務的實現已經講過,本文主要講下這兩個服務進行整合須要的改動,還有就是對於後端服務的主要實現進行講解。spring
微服務網關已經基本介紹完了網關的實現,包括服務路由、幾種過濾方式等。這一節將重點介紹實際應用時的整合。對於須要修改加強的地方以下:sql
繪製的流程圖以下:express
對外暴露的接口能夠直接訪問,這能夠依賴配置文件,而配置文件又能夠經過配置中心進行動態更新,因此不用擔憂有hard-code的問題。 在配置文件中定義須要permitall的路徑。後端
auth:
permitall:
-
pattern: /login/**
-
pattern: /web/public/**
複製代碼
服務啓動時,讀入相應的Configuration,下面的配置屬性讀取以auth開頭的配置。api
@Bean
@ConfigurationProperties(prefix = "auth")
public PermitAllUrlProperties getPermitAllUrlProperties() {
return new PermitAllUrlProperties();
}
複製代碼
固然還須要有PermitAllUrlProperties對應的實體類,比較簡單,不列出來了。安全
Filter過濾器,它是Servlet技術中最實用的技術,Web開發人員經過Filter技術,對web服務器管理的全部web資源進行攔截。這邊使用Filter進行頭部加強,解析請求中的token,構造統一的頭部信息,到了具體服務,能夠利用頭部中的userId進行操做權限獲取與判斷。
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以前排序後再依次註冊。
利用資源服務器的配置,控制哪些是暴露端點不須要進行身份合法性的校驗,直接路由轉發,哪些是須要進行身份loadAuthentication,調用auth服務。
@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的相應依賴。
<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>
複製代碼
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服務中的相應端點。
security:
oauth2:
client:
accessTokenUri: /oauth/token
clientId: gateway
clientSecret: gateway
resource:
userInfoUri: /user
token-info-uri: /oauth/check_token
複製代碼
至於具體的CustomRemoteTokenServices
實現,能夠參考上面講的思路以及RemoteTokenServices
,很簡單,此處略去。
至此,網關服務的加強完成,下面看一下咱們對auth服務和後端backend服務的實現。
強調一下,爲何頭部傳遞的userId等信息須要在網關構造?讀者能夠本身思考一下,結合安全等方面,😆筆者暫時不給出答案。
auth服務的整合修改,其實沒那麼多,以前對於user、role以及permission之間的定義和關係沒有給出實現,這部分的sql語句已經在auth.sql中。因此爲了能給出一個完整的實例,筆者把這部分實現給補充了,主要就是user-role,role、role-permission的相應接口定義與實現,實現增刪改查。
讀者要是想參考整合項目進行實際應用,這部分徹底能夠根據本身的業務進行加強,包括token的建立,其自定義的信息還能夠在網關中進行統一處理,構造好以後傳遞給後端服務。
這邊的接口只是列出了須要的幾個,其餘接口沒寫(由於懶。。)
這兩個接口也是給backend項目用來獲取相應的userId權限。
//根據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);
複製代碼
好了,這邊的實現已經講完了,具體見項目中的實現。
本節是進行實現一個backend的實例,後端項目主要實現哪些功能呢?咱們考慮一下,以前網關服務和auth服務所作的準備:
根據這些,筆者繪製了一個backend的通用流程圖:
上面的流程圖其實已經很是清晰了,首先通過filter過濾器,填充SecurityContextHolder
的上下文。其次,經過切面來實現註解,是否須要進入切面表達式處理。不須要的話,直接執行接口內的方法;不然解析註解中須要的權限,判斷是否有權限執行,有的話繼續執行,不然返回403 forbidden。
Filter過濾器,和上面網關使用同樣,攔截客戶的HttpServletRequest。
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
接口。
基於Spring的項目,使用Spring的AOP切面實現註解是比較方便的一件事,這邊咱們使用了自定義的註解@PreAuth
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
String value();
}
複製代碼
Target用於描述註解的使用範圍,超出範圍時編譯失敗,能夠用在方法或者類上面。在運行時生效。不瞭解註解相關知識的,能夠自行Google。
@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()
轉換成標準的操做上下文,而後解析註解中的表達式,最後獲取對錶達式判斷的結果。
public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {
public CustomerSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
}
複製代碼
CustomerSecurityExpressionRoot
繼承的是抽象類SecurityExpressionRoot
,而咱們用到的實際表達式是定義在SecurityExpressionOperations
接口,SecurityExpressionRoot
又實現了SecurityExpressionOperations
接口。不過這裏面的具體判斷實現,Spring Security 調用的也是Spring EL。
下面咱們看看最終接口是怎麼用上面實現的註解。
@RequestMapping(value = "/test", method = RequestMethod.GET)
@PreAuth("hasAuthority('CREATE_COMPANY')") // 還能夠定義不少表達式,如hasRole('Admin')
public String test() {
return "ok";
}
複製代碼
@PreAuth
中,能夠定義的表達式不少,能夠看SecurityExpressionOperations
接口中的方法。目前筆者只是實現了hasAuthority()
表達式,若是你想支持其餘全部表達式,只須要構造相應的SecurityContextHolder
便可。
有些讀者看了上面的設計,既然好多用到了Spring Security的工具類,確定會問,爲何要引入這麼複雜的工具類?
其實很簡單,首先由於SecurityExpressionOperations
接口中定義的表達式足夠多,且較爲合理,可以覆蓋咱們在平時用到的大部分場景;其次,筆者以前的設計是直接在註解中指定所需權限,沒有擴展性,且可讀性查;最後,Spring Security 4 確實引入了@PreAuthorize,@PostAuthorize
等註解,原本想用來着,本身嘗試了一下,發現對於微服務架構這樣的接口級別的操做權限校驗不是很適合,十多個過濾器太過複雜,並且還涉及到的Principal、Credentials等信息,這些已經在auth系統實現了身份合法性校驗。筆者認爲這邊的功能實現並非很複雜,須要很輕量的實現,讀者有興趣能夠試着這部分的實現封裝成jar包或者Spring Boot的starter。
如上,首先講了整合的設計思路,主要包含三個服務:gateway、auth和backend demo。整合的項目,整體比較複雜,其中gateway服務擴充了好多內容,對於暴露的接口進行路由轉發,這邊引入了Spring Security 的starter,配置資源服務器對暴露的路徑進行放行;對於其餘接口須要調用auth服務進行身份合法性校驗,保證到達backend的請求都是合法的或者公開的接口;auth服務在以前的基礎上,補充了role、permission、user相應的接口,供外部調用;backend demo是新起的服務,實現了接口級別的操做權限的校驗,主要用到了自定義註解和Spring AOP切面。
因爲實現的細節實在有點多,本文限於篇幅,只對部分重要的實現進行列出與講解。若是讀者有興趣實際的應用,能夠根據實際的業務進行擴增一些信息,如auth受權的token、網關攔截請求構造的頭部信息、註解支持的表達式等等。
能夠優化的地方固然還有不少,整合項目中設計不合理的地方,各位同窗能夠多多提意見。
網關、auth權限服務和backend服務的整合項目地址爲:
GitHub:https://github.com/keets2012/microservice-integration
或者 碼雲:https://gitee.com/keets/microservice-integration