Bí quyết công nghệ của EzyPlatform - Interceptor
Back To Blogspublic void addUser(User user) { if (user.getUsername() == null) { throw new IllegalArgumentException("username can not be null"); } }
public void updateUser(User user) { if (user.getUsername() == null) { throw new IllegalArgumentException("username can not be null"); } }
Thì bạn nên viết ra một lớp common kiểu UserValidator thế này:
public class UserValidator { public validate(User user) { if (user.getUsername() == null) { throw new IllegalArgumentException("username can not be null"); } } }
Sau đó sử dụng chung cho hai hàm kiểu này:
public void addUser(User user) { userValidator.validate(user); }
public void updateUser(User user) { userValidator.validate(user); }
Theo cách này thì giả sử bạn cần thay đổi việc ném ra IllegalArgumentException bằng HttpBadRequestException thì cũng chỉ cần cập nhật ở một nơi duy nhất là UserValidator mà thôi. Đây cũng chính là lý do lớn nhất mà Interceptor ra đời.
Đường đi của một yêu cầu trong EzyPlatform
Một yêu cầu từ client gửi đến sẽ cần trải qua lớp Servlet đầu tiên. Lớp Servlet này sẽ gọi đến các lớp interceptor. Trong EzyPlatform có thể tồn tại nhiều interceptor, về bản chất đây là một cài đặt của chain of responsibility.
Khi một yêu cầu đến interceptor nó sẽ phải trải qua hàm preHandle. Hàm này sẽ trả về kết quả true, false. True có nghĩa là yêu cầu hợp lệ và sẽ được chuyển đến interceptor tiếp theo, nếu mọi interceptor.preHandle đều trả về true thì yêu cầu có thể đến controller, ngược lại thì yêu cầu sẽ bị từ chối và mã 406 Not Acceptable sẽ được trả về cho client. Một cách làm khác thay vì trả về false nếu yêu cầu không hợp lệ bạn có thể ném ra một Exception và xử lý Exception này ở một lớp handler nào đó ví dụ WebGlobalExceptionHandler.
Khi một yêu cầu đã được xử lý thì kết quả phản hồi sẽ được đi qua hàm postHandle của mỗi interceptor, điểm quan trọng là hàm postHandle là dạng void vì nó không phải xác thực yêu cầu trả về, bạn có thể sử dụng hàm này để log ra kết quả của yêu cầu chẳng hạn.
Như bạn có thể thấy khi tất cả các yêu cầu và phản hồi phải đi qua các lớp interceptor sẽ giúp chúng ta common lại mã nguồn cực kỳ tốt ví dụ:
- Thay vì chỗ nào cũng log ra yêu cầu kết quả chúng ta có thể sử dụng một lớp interceptor để làm việc này.
- Thay vì chỗ nào cũng xác thực yêu cầu người dùng thì chúng ta có thể xác thực tập trung ở một lớp interceptor.
Một số lớp interceptor được cài đặt sẵn trong EzyPlatform
Lớp AdminLogInterceptor sẽ log ra yêu cầu và phản hồi phía admin:
@Interceptor(priority = Integer.MIN_VALUE) @AllArgsConstructor public class AdminLogInterceptor extends EzyLoggable implements RequestInterceptor { private final RequestURIManager requestUriManager; @Override public boolean preHandle(RequestArguments arguments, Method handler) { String uriTemplate = arguments.getUriTemplate(); HttpMethod method = arguments.getMethod(); if (!requestUriManager.isManagementURI(method, uriTemplate)) { logger.info( "pre handle request uri: {}, method: {}", arguments.getRequest().getRequestURI(), arguments.getMethod() ); } return true; } @Override public void postHandle(RequestArguments arguments, Method handler) { String uriTemplate = arguments.getUriTemplate(); HttpMethod method = arguments.getMethod(); if (!requestUriManager.isManagementURI(method, uriTemplate)) { logger.info( "post handle request uri: {}, method: {}, code: {}", arguments.getRequest().getRequestURI(), arguments.getMethod(), arguments.getResponse().getStatus() ); } } }
Một trong những người gác cửa cực kỳ chắc chắn của admin đó chính là lớp AdminAuthenticationInterceptor:
@Interceptor @AllArgsConstructor public class AdminAuthenticationInterceptor implements RequestInterceptor { private final AdminAdminService adminService; private final AdminAdminRoleService adminRoleService; private final AdminRoleFeatureService roleFeatureService; private final AdminSettingService settingService; private final RequestURIManager requestUriManager; private final FeatureURIManager featureUriManager; private final AtomicBoolean hasSuperAdmin = new AtomicBoolean(false); private static final Set<String> PUBLIC_URIS = Collections .unmodifiableSet( Sets.newHashSet( "/favicon.ico", "/login", "/setup-admin", "/sso-web", "/sso-login", "/css/all.min.css", "/css/icheck-bootstrap.min.css", "/css/adminlte.min.css", "/fonts/fa-brands-400.woff2", "/fonts/fa-regular-400.ttf", "/fonts/fa-regular-400.woff", "/fonts/fa-regular-400.woff2", "/fonts/fa-solid-900.ttf", "/fonts/fa-solid-900.woff", "/fonts/fa-solid-900.woff2", "/js/jquery.min.js", "/js/bootstrap.bundle.min.js", "/js/adminlte.min.js" ) ); @Override public boolean preHandle(RequestArguments arguments, Method handler) { String accessToken = arguments.getHeader(COOKIE_NAME_ADMIN_ACCESS_TOKEN); if (accessToken == null) { accessToken = arguments.getCookieValue(COOKIE_NAME_ADMIN_ACCESS_TOKEN); } HttpServletRequest request = arguments.getRequest(); request.setAttribute(COOKIE_NAME_ADMIN_ACCESS_TOKEN, accessToken); arguments.setArgument(AccessToken.class, accessToken); arguments.setArgument(AdminAccessToken.class, accessToken); String uri = request.getRequestURI(); if (PUBLIC_URIS.contains(uri)) { return true; } String uriTemplate = arguments.getUriTemplate(); Set<String> autoPassUris = settingService.getAutoPassManagementUris(); if (autoPassUris.contains(uriTemplate)) { return true; } ensureSuperAdmin(); long adminId = adminService.validateAdminAccessToken(accessToken); arguments.setArgument(AdminId.class, adminId); HttpMethod method = arguments.getMethod(); if (requestUriManager.isAuthenticatedURI(method, uriTemplate)) { AdminRolesProxy adminRolesProxy = checkPermission(method, uriTemplate, adminId); arguments.setArgument(AdminRoles.class, adminRolesProxy); } return true; } private void ensureSuperAdmin() { if (!hasSuperAdmin.get()) { hasSuperAdmin.set(adminRoleService.hasActiveSuperAdmin()); if (!hasSuperAdmin.get()) { throw new SuperAdminNotExistedException(); } } } private AdminRolesProxy checkPermission( HttpMethod method, String uriTemplate, long adminId ) { String feature = featureUriManager.getFeatureByURI( method, uriTemplate ); Set<Long> roleIds = adminRoleService.getRoleIdsByAdminId(adminId); Set<Long> specialRoleIds = adminRoleService.getSpecialRoleIds(); Map<Long, Map<String, Map<String, Set<HttpMethod>>>> methodsUriMapByFeatureByRoleId = roleFeatureService.getMethodsUriMapByFeatureByRoleId(); if (feature != null) { boolean hasPermission = false; for (Long roleId : roleIds) { if (specialRoleIds.contains(roleId)) { hasPermission = true; break; } Map<String, Map<String, Set<HttpMethod>>> methodsUriMapByFeature = methodsUriMapByFeatureByRoleId.get(roleId); if (methodsUriMapByFeature != null) { Map<String, Set<HttpMethod>> methodsUriMap = methodsUriMapByFeature.get(feature); if (methodsUriMap != null) { Set<HttpMethod> methods = methodsUriMap.get(uriTemplate); if (methods != null && methods.contains(method)) { hasPermission = true; break; } } } } if (!hasPermission) { throw new HttpForbiddenException( singletonMap("permission", "denied") ); } } return AdminRolesProxy.create( roleIds, adminRoleService.getSuperAdminRoleId(), specialRoleIds, featureUriManager, methodsUriMapByFeatureByRoleId ); } }
Hay kiểm soát việc truy cập của từng admin để thuận tiện truy vết sau này thông qua lớp AdminActivityHistoryInterceptor:
@Interceptor(priority = Integer.MAX_VALUE) @AllArgsConstructor public class AdminActivityHistoryInterceptor implements RequestInterceptor { private final RequestURIManager requestUriManager; private final FeatureURIManager featureUriManager; private final AdminAdminActivityHistoryService adminActivityHistoryService; private static final Set<HttpMethod> TRACKED_METHODS = unmodifiableSet( Sets.newHashSet( HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE ) ); @Override public void postHandle(RequestArguments arguments, Method handler) { String uriTemplate = arguments.getUriTemplate(); HttpMethod method = arguments.getMethod(); if (requestUriManager.isManagementURI(method, uriTemplate)) { return; } if (requestUriManager.isAuthenticatedURI(method, uriTemplate)) { boolean isApiUri = requestUriManager.isApiURI(method, uriTemplate); if (isApiUri && !TRACKED_METHODS.contains(method)) { return; } long adminId = arguments.getArgument(AdminId.class); String feature = featureUriManager.getFeatureByURI(method, uriTemplate); adminActivityHistoryService.addHistory( AddAdminActivityHistoryModel.builder() .adminId(adminId) .feature(feature) .method(method.name()) .uri(arguments.getRequest().getRequestURI()) .uriType(isApiUri ? URI_TYPE_API : URI_TYPE_VIEW) .parameters(arguments.getParameters()) .build() ); } } }
Có một điểm đáng chú ý là mỗi interceptor lại có thể quy định quyền ưu tiên thực thi, quyền thấp thực thi trước, quyền cao thực thi sau.
Tổng kết
Interceptor là người đánh chặc cực kỳ quan trọng của EzyPlatform để xử lý mã nguồn tập trung. Bạn có thể bổ sung interceptor nếu bạn muốn EzyPlatform sẽ tự động khởi tạo và đưa vào danh sách, ngoài ra bạn cũng có thể set quyền ưu tiên cho interceptor để thực hiện trước hoặc sau các interceptor khác.