From 0ccc21288bb785563591beff9424ab1c05e6932d Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Fri, 22 May 2026 12:50:53 +0800 Subject: [PATCH] feat(auth): implement role hierarchy and allow guest book browsing - Add role inheritance where admin automatically has user permissions - Update RequireRoleAspect to validate role hierarchy instead of exact match - Expose /api/dashboard/get-all-books to unauthenticated guests - Rename AdminDashBoardController to DashBoardController - Enhance KDoc with role hierarchy rules and access control behavior Closes: #126 --- .../msksbr/bookmgr/annotation/RequireRole.kt | 10 ++++-- .../bookmgr/config/RequireRoleAspect.kt | 25 ++++++++++----- .../com/msksbr/bookmgr/config/WebConfig.kt | 10 +++--- ...rdController.kt => DashBoardController.kt} | 32 ++++++++++--------- 4 files changed, 48 insertions(+), 29 deletions(-) rename src/main/kotlin/com/msksbr/bookmgr/controller/{AdminDashBoardController.kt => DashBoardController.kt} (62%) diff --git a/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt b/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt index 51ab15c..16ba06b 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt @@ -1,8 +1,14 @@ package com.msksbr.bookmgr.annotation /* -* 角色权限注解,贴在 Controller 方法上由 RequireRoleAspect 校验 -* @param role 要求的角色名,如 "admin"、"user" +* 角色权限注解,贴在 Controller 方法上,由 RequireRoleAspect 校验 +* +* 角色继承规则(admin IS-A user): +* @RequireRole("admin") → 仅 admin 可访问 +* @RequireRole("user") → admin 和 user 均可访问 +* 无注解 → 任意已登录用户均可访问(若 WebConfig 排除了 JWT 校验则游客也可) +* +* @param role 要求的角色名 */ @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt index bc0f988..1d1557c 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt @@ -12,20 +12,28 @@ import org.springframework.web.context.request.ServletRequestAttributes /* * 角色权限校验切面 -* 拦截 @RequireRole 注解的方法,校验 request 中 role 是否匹配 +* 拦截 @RequireRole 注解的方法,按角色继承关系校验 * -* 等保三级审计日志字段: -* - 用户身份:username -* - 来源 IP: -* - 请求路径:method + URI -* - 所需角色 / 实际角色 -* - 操作结果:denied / passed +* 角色继承链: +* admin → 自动拥有 user 权限(admin IS-A user) +* user → 仅拥有 user 权限 +* 游客 → 无角色,仅能访问无需 @RequireRole 的公开接口 +* +* 校验规则: +* @RequireRole("admin") → 仅 admin +* @RequireRole("user") → user 和 admin 均可 */ @Aspect @Component class RequireRoleAspect( private val ipExtractor: IpExtractor ) { + // 角色权限集:admin 继承 user 的所有权限 + private val rolePermissions = mapOf( + "admin" to setOf("admin", "user"), + "user" to setOf("user") + ) + @Around("@annotation(requireRole)") fun checkRole( joinPoint: ProceedingJoinPoint, @@ -38,7 +46,8 @@ class RequireRoleAspect( val ip = ipExtractor.getRealIp(request) val path = "${request.method} ${request.requestURI}" - if (role != requireRole.role) { + val allowedRoles = rolePermissions[role] ?: emptySet() + if (requireRole.role !in allowedRoles) { log.warn( "[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}", username, ip, path, requireRole.role, role diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt b/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt index 37fab3f..76964e8 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt @@ -9,9 +9,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer * Spring MVC 配置 — 注册 JWT 鉴权拦截器 * * 拦截规则: -* /api/ — 所有 /api/ 下的请求都需要校验 JWT -* /api/auth/login — 排除,登录不需要 token(此时还没有 token) -* /api/auth/logout — 排除,登出不校验 token(客户端自行删除即可) +* /api/ — 所有 /api/ 下的请求都需要校验 JWT +* /api/auth/login — 排除,登录不需要 token +* /api/auth/logout — 排除,登出不校验 token +* /api/dashboard/get-all-books — 排除,游客可浏览图书列表 */ @Configuration class WebConfig( @@ -23,7 +24,8 @@ class WebConfig( .addPathPatterns("/api/**") .excludePathPatterns( "/api/auth/login", - "/api/auth/logout" + "/api/auth/logout", + "/api/dashboard/get-all-books" ) } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt similarity index 62% rename from src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt rename to src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt index 751d9a2..cfb34b7 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt @@ -12,50 +12,52 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController /* -* 管理端仪表盘接口(仅限管理员访问) -* 路径前缀:/api/admin/dashboard +* 仪表盘接口 +* 路径前缀:/api/dashboard * -* 所有接口需要 @RequireRole("admin") + JWT 鉴权拦截器双重校验 +* 权限模型: +* getAllBooks — 无注解 + WebConfig 排除 JWT,游客 / 用户 / admin 均可 +* getAllBorrowRecords — @RequireRole("admin"),仅管理员 */ @RestController -@RequestMapping("/api/admin/dashboard") -class AdminDashBoardController( +@RequestMapping("/api/dashboard") +class DashBoardController( private val adminDashBoardService: AdminDashBoardService, private val ipExtractor: IpExtractor, ) { /* - * GET /api/admin/dashboard/get-all-books - * 查询全部图书列表 + * GET /api/dashboard/get-all-books + * 查询全部图书列表,游客无需登录即可访问 * * 成功响应(code=200): * { "code":200, "message":"success", "data":[{"id":1,"name":"三体",...}, ...] } */ - @RequireRole("admin") @GetMapping("/get-all-books") fun getAllBooks( - @RequestAttribute username: String, + @RequestAttribute(required = false) username: String?, request: HttpServletRequest ): Result { - log.info("[AdminDashBoard] getAllBooks: user={}, ip={}", username, ipExtractor.getRealIp(request)) - log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) + val user = username ?: "guest" + log.info("[DashBoard] getAllBooks: user={}, ip={}", user, ipExtractor.getRealIp(request)) + log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent")) return adminDashBoardService.getAllBooks() } /* - * GET /api/admin/dashboard/get-all-borrow-records - * 查询全部借阅记录列表 + * GET /api/dashboard/admin/get-all-borrow-records + * 查询全量借阅记录(仅管理员) * * 成功响应(code=200): * { "code":200, "message":"success", "data":[{"id":1,"userId":1,"bookId":2,...}, ...] } */ @RequireRole("admin") - @GetMapping("/get-all-borrow-records") + @GetMapping("/admin/get-all-borrow-records") fun getAllBorrowRecords( @RequestAttribute username: String, request: HttpServletRequest ): Result { log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username, ipExtractor.getRealIp(request)) - log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) + log.info("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) return adminDashBoardService.getAllBorrowRecords() } }