前面的文章中介紹了網(wǎng)關(guān)集成Spring Security實(shí)現(xiàn)網(wǎng)關(guān)層面的統(tǒng)一的認(rèn)證鑒權(quán)。
- 鑒權(quán)放在各個(gè)微服務(wù)中如何做?
- feign的調(diào)用如何做到的鑒權(quán)?
今天針對(duì)以上兩個(gè)問題深入聊聊如何通過三個(gè)注解解決。
前面的幾篇文章陳某都是將鑒權(quán)和認(rèn)證統(tǒng)一的放在了網(wǎng)關(guān)層面,架構(gòu)如下:

微服務(wù)中的鑒權(quán)還有另外一種思路:將鑒權(quán)交給下游的各個(gè)微服務(wù),網(wǎng)關(guān)層面只做路由轉(zhuǎn)發(fā)。
這種思路其實(shí)實(shí)現(xiàn)起來也是很簡(jiǎn)單,下面針對(duì)網(wǎng)關(guān)層面鑒權(quán)的代碼改造一下即可完成:實(shí)戰(zhàn)干貨!Spring Cloud Gateway 整合 OAuth2.0 實(shí)現(xiàn)分布式統(tǒng)一認(rèn)證授權(quán)!
1. 干掉鑒權(quán)管理器
在網(wǎng)關(guān)統(tǒng)一鑒權(quán)實(shí)際是依賴的鑒權(quán)管理器ReactiveAuthorizationManager,所有的請(qǐng)求都需要經(jīng)過鑒權(quán)管理器的去對(duì)登錄用戶的權(quán)限進(jìn)行鑒權(quán)。
這個(gè)鑒權(quán)管理器在網(wǎng)關(guān)鑒權(quán)的文章中也有介紹,在陳某的《Spring Cloud Alibaba 實(shí)戰(zhàn)》中配置攔截也很簡(jiǎn)單,如下:

除了配置的白名單,其他的請(qǐng)求一律都要被網(wǎng)關(guān)的鑒權(quán)管理器攔截鑒權(quán),只有鑒權(quán)通過才能放行路由轉(zhuǎn)發(fā)給下游服務(wù)。
看到這里思路是不是很清楚了,想要將鑒權(quán)交給下游服務(wù),只需要在網(wǎng)關(guān)層面直接放行,不走鑒權(quán)管理器,代碼如下:
http
....
//白名單直接放行
.pathMatchers(ArrayUtil.toArray(whiteUrls.getUrls(), String.class)).permitAll()
//其他的任何請(qǐng)求直接放行
.anyExchange().permitAll()
.....
2. 定義三個(gè)注解
經(jīng)過第①步,鑒權(quán)已經(jīng)下放給下游服務(wù)了,那么下游服務(wù)如何進(jìn)行攔截鑒權(quán)呢?
其實(shí)Spring Security 提供了3個(gè)注解用于控制權(quán)限,如下:
- @Secured
- @PreAuthorize
- @PostAuthorize
關(guān)于這三個(gè)注解就不再詳細(xì)介紹了,有興趣的可以去查閱官方文檔。
陳某這里并不打算使用的內(nèi)置的三個(gè)注解實(shí)現(xiàn),而是自定義了三個(gè)注解,如下:
1).@RequiresLogin
見名知意,只有用戶登錄才能放行,代碼如下:
/**
* @author 公眾號(hào):碼猿技術(shù)專欄
* @url: www.java-family.cn
* @description 登錄認(rèn)證的注解,標(biāo)注在controller方法上,一定要是登錄才能的訪問的接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresLogin {
}
2).@RequiresPermissions
見名知意,只有擁有指定權(quán)限才能放行,代碼如下:
/**
* @author 公眾號(hào):碼猿技術(shù)專欄
* @url: www.java-family.cn
* @description 標(biāo)注在controller方法上,確保擁有指定權(quán)限才能訪問該接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresPermissions {
/**
* 需要校驗(yàn)的權(quán)限碼
*/
String[] value() default {};
/**
* 驗(yàn)證模式:AND | OR,默認(rèn)AND
*/
Logical logical() default Logical.AND;
}
3).@RequiresRoles
見名知意,只有擁有指定角色才能放行,代碼如下:
/**
* @author 公眾號(hào):碼猿技術(shù)專欄
* @url: www.java-family.cn
* @description 標(biāo)注在controller方法上,確保擁有指定的角色才能訪問該接口
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresRoles {
/**
* 需要校驗(yàn)的角色標(biāo)識(shí),默認(rèn)超管和管理員
*/
String[] value() default {OAuthConstant.ROLE_ROOT_CODE,OAuthConstant.ROLE_ADMIN_CODE};
/**
* 驗(yàn)證邏輯:AND | OR,默認(rèn)AND
*/
Logical logical() default Logical.AND;
}
以上三個(gè)注解的含義想必都很好理解,這里就不再解釋了....
3. 注解切面定義
注解有了,那么如何去攔截呢?這里陳某定義了一個(gè)切面進(jìn)行攔截,關(guān)鍵代碼如下:
/**
* @author 公眾號(hào):碼猿技術(shù)專欄
* @url: www.java-family.cn
* @description @RequiresLogin,@RequiresPermissions,@RequiresRoles 注解的切面
*/
@Aspect
@Component
public class PreAuthorizeAspect {
/**
* 構(gòu)建
*/
public PreAuthorizeAspect() {
}
/**
* 定義AOP簽名 (切入所有使用鑒權(quán)注解的方法)
*/
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() {
}
/**
* 環(huán)繞切入
*
* @param joinPoint 切面對(duì)象
* @return 底層方法執(zhí)行后的返回值
* @throws Throwable 底層方法拋出的異常
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 注解鑒權(quán)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
checkMethodAnnotation(signature.getMethod());
try {
// 執(zhí)行原有邏輯
Object obj = joinPoint.proceed();
return obj;
} catch (Throwable e) {
throw e;
}
}
/**
* 對(duì)一個(gè)Method對(duì)象進(jìn)行注解檢查
*/
public void checkMethodAnnotation(Method method) {
// 校驗(yàn) @RequiresLogin 注解
RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
if (requiresLogin != null) {
doCheckLogin();
}
// 校驗(yàn) @RequiresRoles 注解
RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
if (requiresRoles != null) {
doCheckRole(requiresRoles);
}
// 校驗(yàn) @RequiresPermissions 注解
RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
if (requiresPermissions != null) {
doCheckPermissions(requiresPermissions);
}
}
/**
* 校驗(yàn)有無(wú)登錄
*/
private void doCheckLogin() {
LoginVal loginVal = SecurityContextHolder.get();
if (Objects.isNull(loginVal))
throw new ServiceException(ResultCode.INVALID_TOKEN.getCode(), ResultCode.INVALID_TOKEN.getMsg());
}
/**
* 校驗(yàn)有無(wú)對(duì)應(yīng)的角色
*/
private void doCheckRole(RequiresRoles requiresRoles){
String[] roles = requiresRoles.value();
LoginVal loginVal = OauthUtils.getCurrentUser();
//該登錄用戶對(duì)應(yīng)的角色
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 自己實(shí)現(xiàn),由于并未集成前端的菜單權(quán)限,根據(jù)業(yè)務(wù)需求自己實(shí)現(xiàn)
*/
private void doCheckPermissions(RequiresPermissions requiresPermissions){
}
}
其實(shí)這中間的邏輯非常簡(jiǎn)單,就是解析的Token中的權(quán)限、角色然后和注解中的指定的進(jìn)行比對(duì)。
@RequiresPermissions這個(gè)注解的邏輯陳某并未實(shí)現(xiàn),自己根據(jù)業(yè)務(wù)模仿著完成,算是一道思考題了....
4. 注解使用
比如《Spring Cloud Alibaba 實(shí)戰(zhàn)》項(xiàng)目中有一個(gè)添加文章的接口,只有超管和管理員的角色才能添加,那么可以使用@RequiresRoles注解進(jìn)行標(biāo)注,如下:
@RequiresRoles
@AvoidRepeatableCommit
@ApiOperation("添加文章")
@PostMapping("/add")
public ResultMsg<Void> add(@RequestBody @Valid ArticleAddReq req){
.......
}
效果這里就不演示了,實(shí)際的效果:非超管和管理員角色用戶登錄訪問,將會(huì)直接被攔截,返回?zé)o權(quán)限。
注意:這里僅僅解決了下游服務(wù)鑒權(quán)的問題,那么feign調(diào)用是否也適用?
當(dāng)然適用,這里使用的是切面方式,feign內(nèi)部其實(shí)使用的是http方式調(diào)用,對(duì)于接口來說一樣適用。
比如《Spring Cloud Alibaba 實(shí)戰(zhàn)》項(xiàng)目中獲取文章列表的接口,其中會(huì)通過feign的方式調(diào)用評(píng)論服務(wù)中的接口獲取文章評(píng)論總數(shù),這里一旦加上了@RequiresRoles,那么調(diào)用將會(huì)失敗,代碼如下:
@RequiresRoles
@ApiOperation(value = "批量獲取文章總數(shù)")
@PostMapping(value = "/list/total")
public ResultMsg<List<TotalVo>> listTotal(@RequestBody @Valid List<CommentListReq> param){
....
}
總結(jié)
本文主要介紹了微服務(wù)中如何將鑒權(quán)下放到微服務(wù)中,也是為了解決讀者的疑惑,實(shí)際生產(chǎn)中除非業(yè)務(wù)需要,陳某還是建議將鑒權(quán)統(tǒng)一放到網(wǎng)關(guān)中。