3 個註解,輕鬆的實現微服務鑑權
實現思路
前面的幾篇文章陳某都是將鑑權和認證統一的放在了網關層面,架構如下:
微服務中的鑑權還有另外一種思路:將鑑權交給下游的各個微服務,網關層面只做路由轉發。
這種思路其實實現起來也是很簡單,下面針對網關層面鑑權的代碼改造一下即可完成:實戰乾貨!Spring Cloud Gateway 整合 OAuth2.0 實現分佈式統一認證授權!
1. 幹掉鑑權管理器
在網關統一鑑權實際是依賴的鑑權管理器 ReactiveAuthorizationManager,所有的請求都需要經過鑑權管理器的去對登錄用戶的權限進行鑑權。
這個鑑權管理器在網關鑑權的文章中也有介紹,在陳某的《Spring Cloud Alibaba 實戰》中配置攔截也很簡單,如下:
除了配置的白名單,其他的請求一律都要被網關的鑑權管理器攔截鑑權,只有鑑權通過才能放行路由轉發給下游服務。
看到這裏思路是不是很清楚了,想要將鑑權交給下游服務,只需要在網關層面直接放行,不走鑑權管理器,代碼如下:
http
....
//白名單直接放行
.pathMatchers(ArrayUtil.toArray(whiteUrls.getUrls(), String.class)).permitAll()
//其他的任何請求直接放行
.anyExchange().permitAll()
.....
2. 定義三個註解
經過第①步,鑑權已經下放給下游服務了,那麼下游服務如何進行攔截鑑權呢?
其實 Spring Security 提供了 3 個註解用於控制權限,如下:
-
@Secured
-
@PreAuthorize
-
@PostAuthorize
關於這三個註解就不再詳細介紹了,有興趣的可以去查閱官方文檔。
陳某這裏並不打算使用的內置的三個註解實現,而是自定義了三個註解,如下:
1.@RequiresLogin
見名知意,只有用戶登錄才能放行,代碼如下:
/**
* @author 公衆號:碼猿技術專欄
* @url: www.java-family.cn
* @description 登錄認證的註解,標註在controller方法上,一定要是登錄才能的訪問的接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresLogin {
}
2.@RequiresPermissions
見名知意,只有擁有指定權限才能放行,代碼如下:
/**
* @author 公衆號:碼猿技術專欄
* @url: www.java-family.cn
* @description 標註在controller方法上,確保擁有指定權限才能訪問該接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresPermissions {
/**
* 需要校驗的權限碼
*/
String[] value() default {};
/**
* 驗證模式:AND | OR,默認AND
*/
Logical logical() default Logical.AND;
}
3.@RequiresRoles
見名知意,只有擁有指定角色才能放行,代碼如下:
/**
* @author 公衆號:碼猿技術專欄
* @url: www.java-family.cn
* @description 標註在controller方法上,確保擁有指定的角色才能訪問該接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresRoles {
/**
* 需要校驗的角色標識,默認超管和管理員
*/
String[] value() default {OAuthConstant.ROLE_ROOT_CODE,OAuthConstant.ROLE_ADMIN_CODE};
/**
* 驗證邏輯:AND | OR,默認AND
*/
Logical logical() default Logical.AND;
}
以上三個註解的含義想必都很好理解,這裏就不再解釋了....
3. 註解切面定義
註解有了,那麼如何去攔截呢?這裏陳某定義了一個切面進行攔截,關鍵代碼如下:
/**
* @author 公衆號:碼猿技術專欄
* @url: www.java-family.cn
* @description @RequiresLogin,@RequiresPermissions,@RequiresRoles 註解的切面
*/
@Aspect
@Component
public class PreAuthorizeAspect {
/**
* 構建
*/
public PreAuthorizeAspect() {
}
/**
* 定義AOP簽名 (切入所有使用鑑權註解的方法)
*/
public static final String POINTCUT_SIGN = " @annotation(com.mugu.blog.common.annotation.RequiresLogin) || "
+ "@annotation(com.mugu.blog.common.annotation.RequiresPermissions) || "
+ "@annotation(com.mugu.blog.common.annotation.RequiresRoles)";
/**
* 聲明AOP簽名
*/
@Pointcut(POINTCUT_SIGN)
public void pointcut() {
}
/**
* 環繞切入
*
* @param joinPoint 切面對象
* @return 底層方法執行後的返回值
* @throws Throwable 底層方法拋出的異常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 註解鑑權
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try {
// 執行原有邏輯
Object obj = joinPoint.proceed();
return obj;
} catch (Throwable e) {
throw e;
}
}
/**
* 對一個Method對象進行註解檢查
*/
public void checkMethodAnnotation(Method method) {
// 校驗 @RequiresLogin 註解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null) {
doCheckLogin();
}
// 校驗 @RequiresRoles 註解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null) {
doCheckRole(requiresRoles);
}
// 校驗 @RequiresPermissions 註解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null) {
doCheckPermissions(requiresPermissions);
}
}
/**
* 校驗有無登錄
*/
private void doCheckLogin() {
LoginVal loginVal = SecurityContextHolder.get();
if (Objects.isNull(loginVal))
throw new ServiceException(ResultCode.INVALID_TOKEN.getCode(), ResultCode.INVALID_TOKEN.getMsg());
}
/**
* 校驗有無對應的角色
*/
private void doCheckRole(RequiresRoles requiresRoles){
String[] roles = requiresRoles.value();
LoginVal loginVal = OauthUtils.getCurrentUser();
//該登錄用戶對應的角色
String[] authorities = loginVal.getAuthorities();
boolean match=false;
//and 邏輯
if (requiresRoles.logical()==Logical.AND){
match = Arrays.stream(authorities).filter(StrUtil::isNotBlank).allMatch(item -> CollectionUtil.contains(Arrays.asList(roles), item));
}else{ //OR 邏輯
match = Arrays.stream(authorities).filter(StrUtil::isNotBlank).anyMatch(item -> CollectionUtil.contains(Arrays.asList(roles), item));
}
if (!match)
throw new ServiceException(ResultCode.NO_PERMISSION.getCode(), ResultCode.NO_PERMISSION.getMsg());
}
/**
* TODO 自己實現,由於並未集成前端的菜單權限,根據業務需求自己實現
*/
private void doCheckPermissions(RequiresPermissions requiresPermissions){
}
}
其實這中間的邏輯非常簡單,就是解析的 Token 中的權限、角色然後和註解中的指定的進行比對。
“
@RequiresPermissions
這個註解的邏輯陳某並未實現,自己根據業務模仿着完成,算是一道思考題了....”
4. 註解使用
比如《Spring Cloud Alibaba 實戰》項目中有一個添加文章的接口,只有超管和管理員的角色才能添加,那麼可以使用@RequiresRoles
註解進行標註,如下:
@RequiresRoles
@AvoidRepeatableCommit
@ApiOperation("添加文章")
@PostMapping("/add")
public ResultMsg<Void> add(@RequestBody @Valid ArticleAddReq req){
.......
}
效果這裏就不演示了,實際的效果:非超管和管理員角色用戶登錄訪問,將會直接被攔截,返回無權限。
注意:這裏僅僅解決了下游服務鑑權的問題,那麼 feign 調用是否也適用?
當然適用,這裏使用的是切面方式,feign 內部其實使用的是 http 方式調用,對於接口來說一樣適用。
比如《Spring Cloud Alibaba 實戰》項目中獲取文章列表的接口,其中會通過 feign 的方式調用評論服務中的接口獲取文章評論總數,這裏一旦加上了@RequiresRoles
,那麼調用將會失敗,代碼如下:
@RequiresRoles
@ApiOperation(value = "批量獲取文章總數")
@PostMapping(value = "/list/total")
public ResultMsg<List<TotalVo>> listTotal(@RequestBody @Valid List<CommentListReq> param){
....
}
總結
本文主要介紹了微服務中如何將鑑權下放到微服務中,也是爲了解決讀者的疑惑,實際生產中除非業務需要,陳某還是建議將鑑權統一放到網關中。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-dQgxRumwrjVHY70ipOv5A