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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
简介: 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个注解用于控制权限,如下:

  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){
....
}

总结

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

相关文章
|
5月前
|
存储 缓存 数据库
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
介绍如何整合Spring Boot、Shiro和Jwt,以实现一个支持RBAC的无状态认证系统。通过生成JWT token,实现用户无状态登录,并能根据用户角色动态鉴权,而非使用Shiro提供的注解,将角色和权限信息硬编码。此外,文章还探讨了如何对Shiro的异常进行统一捕获和处理。作为应届生,笔者在学习Shiro的过程中进行了一些源码分析,尽管可能存在不足和Bug,但希望能为同样需要实现权限管理的开发者提供参考,并欢迎各位大佬指正完善。
327 65
【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战
|
3月前
|
JSON 算法 Java
微服务Token鉴权设计的几种方案
【8月更文挑战第18天】在微服务架构中,Token鉴权是确保服务安全性的重要环节。本文将详细介绍几种常见的微服务Token鉴权设计方案,旨在帮助大家在工作和学习中更好地理解和应用这些技术。
121 2
|
3月前
|
安全 数据安全/隐私保护 微服务
微服务 Token 鉴权设计:一场守护系统安全的惊心动魄之战,你敢应战吗?
【8月更文挑战第29天】在微服务架构中,Token鉴权设计至关重要,它通过在客户端与服务器间传递包含用户身份和权限信息的Token来确保系统安全。合理的Token鉴权能有效防止非法访问,保护数据安全。设计时需考虑Token的有效期、刷新机制及加密算法等,以提升安全性。随着技术发展,持续优化鉴权机制对于满足复杂的安全需求至关重要。
52 0
|
3月前
|
Kubernetes 前端开发 应用服务中间件
微服务从代码到k8s部署应有尽有系列(三、鉴权)
微服务从代码到k8s部署应有尽有系列(三、鉴权)
|
6月前
|
JSON 安全 Java
微服务Token鉴权设计:概念与实战
【4月更文挑战第29天】在微服务架构中,鉴权是确保服务安全的重要环节。由于微服务往往由多个独立的服务组成,这些服务之间的通信需要一种高效、安全的鉴权机制。Token鉴权作为一种常用的鉴权方式,为微服务架构提供了简洁而有效的解决方案。
171 0
|
6月前
|
安全 Java 数据安全/隐私保护
深入探索 Java 微服务鉴权的奥秘与实践
【4月更文挑战第17天】
188 0
|
存储 JSON 算法
微服务网关限流&鉴权3
微服务网关限流&鉴权
98 0
|
算法 NoSQL Redis
微服务网关限流&鉴权2
微服务网关限流&鉴权
138 0
|
监控 安全 Java
微服务网关限流&鉴权1
微服务网关限流&鉴权
100 0
|
安全 Java 微服务
十四.SpringCloud+Security+Oauth2实现微服务授权 - 网关统一鉴权
SpringCloud+Security+Oauth2实现微服务授权 - 网关统一鉴权