您现在的位置是:亿华云 > 应用开发

三个注解,优雅的实现微服务鉴权

亿华云2025-10-09 15:08:25【应用开发】2人已围观

简介实现思路前面的几篇文章陈某都是将鉴权和认证统一的放在了网关层面,架构如下:微服务中的鉴权还有另外一种思路:将鉴权交给下游的各个微服务,网关层面只做路由转发。这种思路其实实现起来也是很简单,下面针对网关

实现思路

前面的三个注解几篇文章陈某都是将鉴权和认证统一的放在了网关层面,架构如下:

微服务中的优雅鉴权还有另外一种思路:将鉴权交给下游的各个微服务,网关层面只做路由转发。现微

这种思路其实实现起来也是服务很简单,下面针对网关层面鉴权的鉴权代码改造一下即可完成:实战干货!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 ResultMsgadd(@RequestBody @Valid ArticleAddReq req){

.......

}

效果这里就不演示了,站群服务器实际的效果:非超管和管理员角色用户登录访问,将会直接被拦截,返回无权限。

注意:这里仅仅解决了下游服务鉴权的问题,那么feign调用是否也适用?

当然适用,这里使用的是切面方式,feign内部其实使用的是http方式调用,对于接口来说一样适用。

比如《Spring Cloud Alibaba 实战》项目中获取文章列表的接口,其中会通过feign的方式调用评论服务中的接口获取文章评论总数,这里一旦加上了@RequiresRoles,那么调用将会失败,代码如下:

@RequiresRoles

@ApiOperation(value = "批量获取文章总数")

@PostMapping(value = "/list/total")

public ResultMsg> listTotal(@RequestBody @Valid Listparam){

....

}

总结

本文主要介绍了微服务中如何将鉴权下放到微服务中,也是为了解决读者的疑惑,实际生产中除非业务需要,陈某还是建议将鉴权统一放到网关中。

很赞哦!(1713)