上文我们讲到了 Spring Boot 的接口鉴权:将 token 放到 Http 请求的头部,服务端使用 Filter 拦截器,根据 token 值确定用户。

但是这种用法只限于同权的系统中,并不能区分开用户的权限。比如管理员拥有删除用户的权限,而普通用户没有。调用删除用户这个接口时如何区分不同权限的用户呢?(如果你准备在实现类中手写鉴权方法就当我没说,因为无权的账户已经进入该方法了,不安全)

网上对于 Spring 权限安全的解决方案主要是使用 Shiro 或 SpringSecurity 框架。当然我也建议大家使用这两个框架,毕竟更加安全稳定。我这里介绍一个我自制的基于 AOP(面向切面编程) 的安全框架,更加简洁的同时也实现了类似的功能(主要是为了兼容老系统那 sb 的权限系统)

AOP 需要引用的 jar 包,pom.xml 添加如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>

控制器代码如下:



首先,定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Authorize {
    /**
     * 访问权限,默认为空,登录即可访问
     * 可以多个定义
     */
    String[] value() default {};
}

此注解使用方法如上图所示。

然后创建一个handler,注意加上 @Aspect 注解。Sprig AOP 中有几个常用的注解:@Before,@After,@Around,@AfterRunning 等,具体的使用方法可以 谷歌。

AuthorizeAspect.java 处理:

@Aspect
@Component
public class AuthorizeAspect {
  private final RedisUtil redisUtil;
  private final RolePermissionMapper rolePermissionMapper;

  @Autowired
  public AuthorizeAspect(RedisUtil redisUtil, RolePermissionMapper rolePermissionMapper) {
    this.redisUtil = redisUtil;
    this.rolePermissionMapper = rolePermissionMapper;
  }

  /**
   * Spring中使用@Pointcut注解来定义方法切入点
   *
   * @Pointcut 用来定义切点,针对方法
   * @Aspect 用来定义切面,针对类
   * 后面的增强均是围绕此切入点来完成的
   */
  @Pointcut("@annotation(authorize)")
  public void doAuthorize(Authorize authorize) {
  }

  @Around(value = "doAuthorize(authorize)", argNames = "pjp,authorize")
  public Object deBefore(ProceedingJoinPoint pjp, Authorize authorize) throws Throwable {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    // 获取token
    String token = request.getHeader("token");
    // 获取注解
    String[] codes = authorize.value();

    // 未登录
    if (Strings.isNullOrEmpty(token) || !redisUtil.exists(token, RedisPrefixKeyEnum.Sys.toString()))
      return ApiRes.unAuthorized();

    if (codes.length == 0) {
      // 注解为空,登陆即可
      return pjp.proceed();
    } else {
      String[] arr = redisUtil.get(token, RedisPrefixKeyEnum.Sys.toString()).split(":");
      int roleId = Integer.valueOf(arr[2]);
      // roleId 的所有权限
      List<RolePermission> powers = rolePermissionMapper.selectList(
          new QueryWrapper<RolePermission>().lambda()
              .eq(RolePermission::getRoleId, roleId)
      );

      for (String code : codes) {
        for (RolePermission power : powers) {
          if (code.equals(power.getPermissionCode())) {
            // 身份匹配成功
            return pjp.proceed();
          }
        }
      }
      return ApiRes.unAuthorized("权限不足:" + Arrays.toString(codes));
    }
  }
}

这段代码我在之前的文章中出现过,现在详细分析一下。

Spring中使用@Pointcut注解来定义方法切入点,doAuthorize 自然就是其具体的实现。@Around 是环绕,方法之前之后的处理都包圆了。

接下来便是从request请求中获取token,从注解中获取权限:

// 获取token
String token = request.getHeader("token");
// 获取注解
String[] codes = authorize.value();

由于注解定义的时候是String[] value() default {};所以此处接收为 String[] 类型。

接下来通过这两个变量判断用户状态和方法所需权限。

然后根据用户拥有的权限匹配执行目标方法所需的权限,权限不足返回‘权限不足’警告并提示缺失权限。拥有权限则 return pjp.proceed(); 执行接下来的方法。