关于微服务:3个注解优雅的实现微服务鉴权

30次阅读

共计 5479 个字符,预计需要花费 14 分钟才能阅读完成。

大家好,我是不才陈某~

这是《Spring Cloud 进阶》第 39 篇 文章,后面的文章中介绍了网关集成 Spring Security 实现网关层面的对立的认证鉴权。

有不分明的能够看之前的文章:实战干货!Spring Cloud Gateway 整合 OAuth2.0 实现分布式对立认证受权!

最近订阅了《Spring Cloud Alibaba 实战》视频专栏的读者常常问陈某两个问题,如下:

  1. 鉴权放在各个微服务中如何做?
  2. feign 的调用如何做到的鉴权?

明天针对以上两个问题深刻聊聊如何通过三个注解解决。

实现思路

后面的几篇文章陈某都是将鉴权和认证对立的放在了网关层面,架构如下:

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

这种思路其实实现起来也是很简略,上面针对网关层面鉴权的代码革新一下即可实现:实战干货!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 个注解用于管制权限,如下:

  1. @Secured
  2. @PreAuthorize
  3. @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){....}

总结

本文次要介绍了微服务中如何将鉴权下放到微服务中,也是为了解决读者的纳闷,理论生产中除非业务须要,陈某还是倡议将鉴权对立放到网关中。

最初说一句(别白嫖,求关注)

陈某每一篇文章都是精心输入,曾经写了 3 个专栏,整顿成PDF,获取形式如下:

  1. 《Spring Cloud 进阶》PDF:关注公众号:【码猿技术专栏】回复关键词 Spring Cloud 进阶 获取!
  2. 《Spring Boot 进阶》PDF:关注公众号:【码猿技术专栏】回复关键词 Spring Boot 进阶 获取!
  3. 《Mybatis 进阶》PDF:关注公众号:【码猿技术专栏】回复关键词 Mybatis 进阶 获取!

如果这篇文章对你有所帮忙,或者有所启发的话,帮忙 点赞 在看 转发 珍藏,你的反对就是我坚持下去的最大能源!

正文完
 0