欢迎关注公众号:冬瓜白
相关文章:
- 每天学习一点点之 Spring Web MVC 之抽象 HandlerInterceptor 快速实现常用功能(限流、权限等)
在[每天学习一点点之 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";
}
在这个例子中使用了 @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);@Overrideprotected 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;}//获取目标idLong 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;}@Overrideprotected void afterCompletion(HttpServletRequest request, HttpServletResponse response,HandlerMethod handlerMethod, RequiresPermissions annotation, Exception ex) {// Do nothing}@Overrideprotected void postHandle(HttpServletRequest request, HttpServletResponse response,HandlerMethod handlerMethod, ModelAndView modelAndView, RequiresPermissions annotation) {// Do nothing}/*** 获取当前登陆用户*/private BaseUser getLogin() {return null;}
}
/*** @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();}@Overridepublic 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);}@Overridepublic boolean isFinished() {return this.cachedBodyInputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener readListener) {throw new UnsupportedOperationException();}@Overridepublic int read() throws IOException {return this.cachedBodyInputStream.read();}}
}
在鉴权逻辑中,需要从请求参数中获取目标id。这是因为权限检查可能需要根据这个id来进行。
例如可能需要检查用户是否有权限访问这个id对应的资源。为了获取这个id,需要解析请求参数。这个过程可能会比较复杂,因为请求参数可能以不同的方式传递,例如,它们可能在URL的查询字符串中,或者在POST请求的请求体中。因此这里提供了getId方法来处理这些情况,并尽可能地获取到id。
这里比较麻烦的是从接口中解析 id 参数,这里支持两种方式:
- 当前接口调用者需要有目标对象 ID为传入的 id 的
OWNER
权限:
@PostMapping("/count")
@ResponseBody
@RequiresPermissions("OWNER")
public Response count(Long id) {return Response.getSuccess(count(projectId));
}
- 当前接口调用者需要有项目ID为传入的 project.id 项目的
USE
权限:
@PostMapping("/list")
@ResponseBody
@RequiresPermissions("USE")
public Response list(@RequestBody Project project) {return Response.getSuccess(list(project);
}
getId
方法用于解析请求中的参数,包括URL的查询参数和POST请求的请求体参数。getIdFromBody
方法专门用于解析POST请求的请求体参数。通过这种方式,可以灵活地控制用户的访问权限。
总结
本文主要探讨了基于抽象的 HandlerInterceptor 来实现鉴权注解 @RequiresPermissions
,它可以灵活地控制用户的访问权限。