1、背景
通常在微服務架構中咱們都會使用spring security oauth2來進行權限控制,咱們將資源服務所有放在內網環境中,將API網關暴露在公網上,公網若是想要訪問咱們的資源必須通過API網關進行鑑權,鑑權經過後再訪問咱們的資源服務。咱們根據以下圖片來分析一下問題。web
如今咱們有三個服務:分別是用戶服務、訂單服務和產品服務。用戶若是購買產品,則須要調用產品服務生成訂單,那麼咱們在這個調用過程當中有必要鑑權嗎?答案是否認的,由於這些資源服務放在內網環境中,徹底不用考慮安全問題。
spring
2、思路
若是要想實現這個功能,咱們則須要來區分這兩種請求,來自網關的請求進行鑑權,而服務間的請求則直接調用。安全
是否能夠給接口增長一個參數來標記它是服務間調用的請求?服務器
這樣雖然能夠實現兩種請求的區分,可是實際中不會這麼作。在 Spring Cloud Alibaba系列(三)使用feign進行服務調用 中曾提到了實現feign的兩種方式,通常狀況下服務間調用和網關請求的數據接口是同一個接口,若是寫成兩個接口來分別給兩種請求調用,這樣無疑增長了大量重複代碼。也就是說咱們通常不會經過改變請求參數的個數來實現這兩種服務的區分。微信
雖然不能增長請求的參數個數來區分,可是咱們能夠給請求的header中添加一個參數用來區分。這樣徹底能夠避免上面提到的問題。架構
3、實現
3.1 自定義註解
咱們自定義一個Inner的註解,而後利用aop對這個註解進行處理app
1@Target(ElementType.METHOD)
2@Retention(RetentionPolicy.RUNTIME)
3@Documented
4public @interface Inner {
5 /**
6 * 是否AOP統一處理
7 */
8 boolean value() default true;
9}
1@Aspect
2@Component
3public class InnerAspect implements Ordered {
4
5 private final Logger log = LoggerFactory.getLogger(InnerAspect.class);
6
7 @Around("@annotation(inner)")
8 public Object around(ProceedingJoinPoint point, Inner inner) throws Throwable {
9 String header = ServletUtils.getRequest().getHeader(SecurityConstants.FROM);
10 if (inner.value() && !StringUtils.equals(SecurityConstants.FROM_IN, header)){
11 log.warn("訪問接口 {} 沒有權限", point.getSignature().getName());
12 throw new AccessDeniedException("Access is denied");
13 }
14 return point.proceed();
15 }
16
17 @Override
18 public int getOrder() {
19 return Ordered.HIGHEST_PRECEDENCE + 1;
20 }
21}
上面這段代碼就是獲取全部加了@Inner註解的方法或類,判斷請求頭中是否有咱們規定的參數,若是沒有,則不容許訪問接口。ide
3.2 暴露url
將全部註解了@Inner的方法和類暴露出來,容許不鑑權能夠方法,這裏須要注意的點是若是方法使用pathVariable 傳參的,則須要將這個參數轉換爲*。若是不轉換,當成接口的訪問路徑,則找不到此接口。微服務
1@Configuration
2public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware{
3
4 private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
5 private ApplicationContext applicationContext;
6 private List<String> urls = new ArrayList<>();
7 public static final String ASTERISK = "*";
8
9 @Override
10 public void afterPropertiesSet() {
11 RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
12 Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
13 map.keySet().forEach(info -> {
14 HandlerMethod handlerMethod = map.get(info);
15 // 獲取方法上邊的註解 替代path variable 爲 *
16 Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
17 Optional.ofNullable(method).ifPresent(inner -> info.getPatternsCondition().getPatterns()
18 .forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, ASTERISK))));
19 // 獲取類上邊的註解, 替代path variable 爲 *
20 Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
21 Optional.ofNullable(controller).ifPresent(inner -> info.getPatternsCondition().getPatterns()
22 .forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, ASTERISK))));
23 });
24 }
25
26 @Override
27 public void setApplicationContext(ApplicationContext context) {
28 this.applicationContext = context;
29 }
30
31 public List<String> getUrls() {
32 return urls;
33 }
34
35 public void setUrls(List<String> urls) {
36 this.urls = urls;
37 }
38}
在資源服務器中,將請求暴露出來ui
1public void configure(HttpSecurity httpSecurity) throws Exception {
2 //容許使用iframe 嵌套,避免swagger-ui 不被加載的問題
3 httpSecurity.headers().frameOptions().disable();
4 ExpressionUrlAuthorizationConfigurer<HttpSecurity>
5 .ExpressionInterceptUrlRegistry registry = httpSecurity
6 .authorizeRequests();
7 // 將上面獲取到的請求,暴露出來
8 permitAllUrl.getUrls()
9 .forEach(url -> registry.antMatchers(url).permitAll());
10 registry.anyRequest().authenticated()
11 .and().csrf().disable();
12}
3.3 如何去請求
定義一個接口:
1@PostMapping("test")
2@Inner
3public String test(@RequestParam String id){
4 return id;
5}
定義feign遠程調用接口
1@PostMapping("test")
2MediaFodderBean test(@RequestParam("id") String id,@RequestHeader(SecurityConstants.FROM) String from);
服務間進行調用,傳請求頭
1 String id = testService.test(id, SecurityConstants.FROM_IN);
4、思考
4.1 安全性
上面雖然實現了服務間調用,可是咱們將@Inner的請求暴露出去了,也就是說不用鑑權既能夠訪問到,那麼咱們是否是能夠模擬一個請求頭,而後在其餘地方經過網關來調用呢?
答案是能夠,那麼,這時候咱們就須要對網關中分發的請求進行處理,在網關中寫一個全局攔截器,將請求頭的form參數清洗。
1@Component
2public class RequestGlobalFilter implements GlobalFilter, Ordered {
3
4 @Override
5 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
6 // 清洗請求頭中from 參數
7 ServerHttpRequest request = exchange.getRequest().mutate()
8 .headers(httpHeaders -> httpHeaders.remove(SecurityConstants.FROM))
9 .build();
10 addOriginalRequestUrl(exchange, request.getURI());
11 String rawPath = request.getURI().getRawPath();
12 ServerHttpRequest newRequest = request.mutate()
13 .path(rawPath)
14 .build();
15 exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
16 return chain.filter(exchange.mutate()
17 .request(newRequest.mutate()
18 .build()).build());
19 }
20
21 @Override
22 public int getOrder() {
23 return -1000;
24 }
25}
4.2 擴展性
咱們自定義@Inner註解的時候,放了一個boolean類型的value(),默認爲true。若是咱們想讓這個請求能夠經過網關訪問的話,將value賦值爲false便可。
1@PostMapping("test")
2@Inner(value=false)
3public String test(@RequestParam String id){
4 return id;
5}
5、總結
這樣咱們總共實現瞭如下幾個功能:
服務間訪問能夠不鑑權,添加註解@Inner便可。
網關訪問不須要鑑權的資源,添加註解@Inner(value=false)便可。固然,這樣服務間不鑑權也能夠訪問。
爲了安全性考慮,將網關中的請求頭form參數清洗,以防有人模擬請求,來訪問資源。
因爲各個服務都是在內網環境中,只有網關會暴露公網,所以服務間調用是不必鑑權的。
本文分享自微信公衆號 - Java旅途(Javatrip)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。