欢迎关注公众号:冬瓜白

相关文章:

在[每天学习一点点之 Spring Web MVC 之抽象 HandlerInterceptor 快速实现常用功能(限流、权限等)](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)中已经介绍过可以基于抽象的 HandlerInterceptor 来实现很多常见的功能。本文快速实现了类似于 Shiro 的鉴权注解 @RequiresPermissions,并且功能更强大。

定义权限注解:

/**
 * @author Dongguabai
 * @description
 * @date 2024-06-25 10:57
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RequiresPermissions {

    /**
     * 当前接口需要的权限(如 ADMIN,USER)
     * @return
     */
    String[] value();

    /**
     * 对象id字段属性名称(默认id)
     */
    String key() default "id";
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

在这个例子中使用了 @RequiresPermissions 注解来指定这个接口需要的权限。指定了两种权限:ADMIN 和USER。意味着只有拥有 ADMIN 或 USER 权限的用户才能访问这个接口。还指定了key为"id",也就是说会从请求参数中获取名为"id"的参数,并使用这个参数来进行额外的权限检查。

这里 key 的作用是,比如有的目标对象只能由特定的用户去操作,这里的 key 就提供了这样一种方式。

继承 CustomizedHandlerMethodInterceptor 实现鉴权逻辑:

/**
 * @author dongguabai
 * @date 2024-06-25 10:58
 */
@Component
public class RequiresPermissionsHandlerMethodInterceptor extends CustomizedHandlerMethodInterceptor<RequiresPermissions> {

    private static final Logger LOGGER = LoggerFactory.getLogger(RequiresPermissionsHandlerMethodInterceptor.class);

    @Override
    protected boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                                HandlerMethod handlerMethod, RequiresPermissions annotation) throws Exception {
        //获取当前登陆用户
        BaseUser user = getLogin();
        if (user == null) {
            LOGGER.error("Unable to get login information");
            return false;
        }
        String key = annotation.key();
        String[] value = annotation.value();
        if (StringUtils.isBlank(key) || ArrayUtils.isEmpty(value)) {
            return true;
        }
        //获取目标id
        Long id = getId(request, handlerMethod, key);
        if (id != null) {
            //用户鉴权
            return checkUserPermission(response, user, value, id);
        }
        return true;
    }

    private Long getId(HttpServletRequest request, HandlerMethod handlerMethod, String key) throws IOException {
        CachingWrapper requestWrapper = new CachingWrapper(request);
        MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
        Long id = null;
        for (MethodParameter methodParameter : methodParameters) {
            id = getidFromParameter(request, key, methodParameter, requestWrapper);
            if (id != null) {
                break;
            }
        }
        return id;
    }

    private Long getidFromParameter(HttpServletRequest request, String key,
                                           MethodParameter methodParameter, CachingWrapper requestWrapper) throws IOException {
        String parameterName = methodParameter.getParameterName();
        if (key.equals(parameterName)) {
            return Long.valueOf(request.getParameter(parameterName));
        } else if (methodParameter.getParameterAnnotation(RequestBody.class) != null) {
            return getIdFromBody(key, requestWrapper);
        }
        return null;
    }

    private Long getIdFromBody(String key, CachingWrapper requestWrapper) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode = mapper.readTree(requestWrapper.getCachedBody());
        JsonNode idNode;
        if (rootNode.isArray() && rootNode.size() > 0) {
            idNode = rootNode.get(0).path(key);
        } else {
            idNode = rootNode.path(key);
        }
        if (!idNode.isMissingNode()) {
            return idNode.asLong();
        }
        return null;
    }

    private boolean checkUserPermission(HttpServletResponse response, BaseUser user, String[] value, Long id) {
        // 业务鉴权逻辑
        return true;
    }

    @Override
    protected void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                   HandlerMethod handlerMethod, RequiresPermissions annotation, Exception ex) {
        // Do nothing
    }

    @Override
    protected void postHandle(HttpServletRequest request, HttpServletResponse response,
                              HandlerMethod handlerMethod, ModelAndView modelAndView, RequiresPermissions annotation) {
        // Do nothing
    }

    /**
     * 获取当前登陆用户
     */
    private BaseUser getLogin() {
        return null;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
/**
 * @author dongguabai
 * @date 2024-06-25 17:33
 */
public class CachingWrapper extends HttpServletRequestWrapper {
    private byte[] cachedBody;

    public CachingWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        ByteArrayOutputStream cachedBodyOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length;
        while ((length = requestInputStream.read(buffer)) != -1) {
            cachedBodyOutputStream.write(buffer, 0, length);
        }
        this.cachedBody = cachedBodyOutputStream.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }

    public byte[] getCachedBody() {
        return this.cachedBody;
    }

    private static class CachedBodyServletInputStream extends ServletInputStream {
        private ByteArrayInputStream cachedBodyInputStream;

        CachedBodyServletInputStream(byte[] cachedBody) {
            this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
        }

        @Override
        public boolean isFinished() {
            return this.cachedBodyInputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int read() throws IOException {
            return this.cachedBodyInputStream.read();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.

在鉴权逻辑中,需要从请求参数中获取目标id。这是因为权限检查可能需要根据这个id来进行。

例如可能需要检查用户是否有权限访问这个id对应的资源。为了获取这个id,需要解析请求参数。这个过程可能会比较复杂,因为请求参数可能以不同的方式传递,例如,它们可能在URL的查询字符串中,或者在POST请求的请求体中。因此这里提供了getId方法来处理这些情况,并尽可能地获取到id。

这里比较麻烦的是从接口中解析 id 参数,这里支持两种方式:

  1. 当前接口调用者需要有目标对象 ID为传入的 id 的 OWNER 权限:
@PostMapping("/count")
@ResponseBody
@RequiresPermissions("OWNER")
public Response count(Long id) {
    return Response.getSuccess(count(projectId));
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  1. 当前接口调用者需要有项目ID为传入的 project.id 项目的 USE 权限:
@PostMapping("/list")
@ResponseBody
@RequiresPermissions("USE")
public Response list(@RequestBody Project project) {
    return Response.getSuccess(list(project);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

getId 方法用于解析请求中的参数,包括URL的查询参数和POST请求的请求体参数。getIdFromBody 方法专门用于解析POST请求的请求体参数。通过这种方式,可以灵活地控制用户的访问权限。

总结

本文主要探讨了基于抽象的 HandlerInterceptor 来实现鉴权注解 @RequiresPermissions,它可以灵活地控制用户的访问权限。