@Inner使用及原理

huayang183發表於2021-01-02
  1. 外部從Gateway訪問,需要鑑權(eg.CURD操作)。這種是最常使用的,使用者登入後正常訪問介面,不需要我們做什麼處理(可能有的介面需要加許可權欄位)。
  2. 外部從Gateway訪問,不需要鑑權(eg.簡訊驗證碼)。需要我們將uri加入到security.oauth2.client.ignore-urls配置中,可以不需要鑑權訪問
  3. 內部服務間用Feign訪問,不需要鑑權(eg.Auth查詢使用者資訊)。也是需要我們將uri加入到security.oauth2.client.ignore-urls配置中,那與第二種的區別就是這種情況下大多數都是服務可以請求另一個服務的所有資料,不受約束,那我們如果僅僅只配置ignore-url的話,外部所有人都可以通過url請求到我們內部的連結,安全達不到保障。
  • 鑑於上述第三種情況,配置了ignore-url和Feign,此時該介面不需要鑑權,服務內部通過Feign訪問,服務外部通過url也可以訪問,所以Pigx中,加入了一種@RequestHeader(SecurityConstants.FROM)的處理方式。即在介面方法中,對頭部進行判斷,只有請求帶上相應的Header引數時,才允許通過訪問,否則丟擲異常。那這時候其實我們在外網通過Gateway訪問的時候,也可以手動帶上這個Header引數,來達到這個目的。所以我們便在Gateway中設定了一個GlobalFilter過濾器:

  • 這個過濾器在處理HttpRequest的時候,會刪除從外部請求頭裡的SecurityConstants.FROM這個引數。此時的效果就是,這個URL從外部訪問不需要鑑權,但由於Gateway的過濾,最終到達我們介面方法時,由於缺少頭部資訊,被拒絕訪問;而服務間通過Feign訪問,不經過Gateway,則可以正常訪問。
  • 那原始的處理方法和處理邏輯就是這樣:首先將uri加入ingore-url,然後在介面的方法和Feign的介面引數中寫上RequestHeader引數,最後在Feign-Client中帶上這個SecurityConstants.FROM引數。既然這種邏輯都是相同的,那後面的pigx版本發行後,就使用AOP將此步驟抽離出來,成為了Inner。

# Inner的處理流程

# 統一的ignore-url處理

首先我們來看看這個註解的程式碼

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inner {
	/**
	 * 是否AOP統一處理(可以理解為是否僅允許Feign之間呼叫)
	 *
	 * @return false, true
	 */
	boolean value() default true;
 
	/**
	 * 需要特殊判空的欄位(預留)
	 *
	 * @return {}
	 */
	String[] field() default {};
}
  • 首先,在我們專案載入階段,我們獲取有Inner註解的類和方法,然後獲取我們配置的uri,經過正則替換後面的可變引數為*,然後將此uri加入到ignore-url中。此時我們就能達到所有Inner配置的方法/類上的介面地址,都統一在專案載入階段自動幫我們加到ignore-url中,不需要我們手動配置,免去了很多開發工作,同時也能避免我們忘記配置,而浪費開發時間。核心程式碼如下:
@Slf4j
@Configuration
@ConditionalOnExpression("!'${security.oauth2.client.ignore-urls}'.isEmpty()")
@ConfigurationProperties(prefix = "security.oauth2.client")
public class PermitAllUrlProperties implements InitializingBean {
	private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
	@Autowired
	private WebApplicationContext applicationContext;
	@Getter
	@Setter
	private List<String> ignoreUrls = new ArrayList<>();
	@Override
	public void afterPropertiesSet() {
		RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
		Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
		map.keySet().forEach(info -> {
			HandlerMethod handlerMethod = map.get(info);
			// 獲取方法上邊的註解 替代path variable 為 *
			Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
			Optional.ofNullable(method)
					.ifPresent(inner -> info.getPatternsCondition().getPatterns()
							.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, StringPool.ASTERISK))));
			// 獲取類上邊的註解, 替代path variable 為 *
			Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
			Optional.ofNullable(controller)
					.ifPresent(inner -> info.getPatternsCondition().getPatterns()
							.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, StringPool.ASTERISK))));
		});
	}
}

# 統一的安全性處理

那上面講到的,如果我們不希望這個url可以直接被外網呼叫,僅能在Feign服務中呼叫,改如何統一處理呢?

我們使用一個Spring-AOP,在對所有Inner註解的方法做一個環繞增強的切點,進行統一的處理。在上面我們提到的Inner的value引數,當該引數為true時,我們對方法的入參進行判斷,僅當符合我們定製的入參規則時(Pigx這裡是用的@RequestHeader(SecurityConstants.FROM) 與SecurityConstants.FROM_IN做比較),我們對它進行放行,不符合時,丟擲異常;當value為false時,我們不做任何處理,此時Inner僅起到了一個ignore-url的作用。

@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class PigxSecurityInnerAspect {
	private final HttpServletRequest request;
	@SneakyThrows
	@Around("@annotation(inner)")
	public Object around(ProceedingJoinPoint point, Inner inner) {
		String header = request.getHeader(SecurityConstants.FROM);
		if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
			log.warn("訪問介面 {} 沒有許可權", point.getSignature().getName());
			throw new AccessDeniedException("Access is denied");
		}
		return point.proceed();
	}
}

通過這兩步呢,我們首先是在載入時通過找到Inner註解,將相應的uri加入到ignore-url中,達到自動化配置的目的;之後我們又使用切面對Inner的方法進行環繞處理,達到安全控制。對比之前的處理方式,現在我們使用一個@Inner註解,就能很快的滿足上面說的兩種場景,大大節省了我們的開發時間。

# 合理的使用@Inner註解

上面提到的兩種應用場景,在我們的程式碼中,其實都是可以使用Inner註解的,下面結合Feign做一個簡單的示例,示例場景就是我們的使用者密碼登入中的一環:

  1. 在介面上使用@Inner註解,使得url無需鑑權

    	/**
    	 * 獲取指定使用者全部資訊
    	 *
    	 * @return 使用者資訊
    	 */
    	@Inner
    	@GetMapping("/info/{username}")
    	public R info(@PathVariable String username) {
    		SysUser user = userService.getOne(Wrappers.<SysUser>query()
    				.lambda().eq(SysUser::getUsername, username));
    		if (user == null) {
    			return R.failed(null, String.format("使用者資訊為空 %s", username));
    		}
    		return R.ok(userService.findUserInfo(user));
    	}

     

  2. 編寫Feign介面

    @FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UMPS_SERVICE)
    public interface RemoteUserService {
    	/**
    	 * 通過使用者名稱查詢使用者、角色資訊
    	 *
    	 * @param username 使用者名稱
    	 * @param from     呼叫標誌
    	 * @return R
    	 */
    	@GetMapping("/user/info/{username}")
    	R<UserInfo> info(@PathVariable("username") String username
    			, @RequestHeader(SecurityConstants.FROM) String from);
    }

     

  3. Feign-Client中呼叫介面,帶上SecurityConstants.FROM_IN引數為內部識別

    	/**
    	 * 使用者密碼登入
    	 *
    	 * @param username 使用者名稱
    	 * @return
    	 * @throws UsernameNotFoundException
    	 */
    	@Override
    	@SneakyThrows
    	public UserDetails loadUserByUsername(String username) {
    		Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
    		if (cache != null && cache.get(username) != null) {
    			return (PigxUser) cache.get(username).get();
    		}
    		R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);
    		UserDetails userDetails = getUserDetails(result);
    		cache.put(username, userDetails);
    		return userDetails;
    	}

     

相關文章