歡迎關注公眾號:冬瓜白
相關文章:
- 每天學習一點點之 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
,它可以靈活地控制用戶的訪問權限。