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
This commit is contained in:
@@ -1,8 +1,14 @@
|
|||||||
package com.msksbr.bookmgr.annotation
|
package com.msksbr.bookmgr.annotation
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 角色权限注解,贴在 Controller 方法上由 RequireRoleAspect 校验
|
* 角色权限注解,贴在 Controller 方法上,由 RequireRoleAspect 校验
|
||||||
* @param role 要求的角色名,如 "admin"、"user"
|
*
|
||||||
|
* 角色继承规则(admin IS-A user):
|
||||||
|
* @RequireRole("admin") → 仅 admin 可访问
|
||||||
|
* @RequireRole("user") → admin 和 user 均可访问
|
||||||
|
* 无注解 → 任意已登录用户均可访问(若 WebConfig 排除了 JWT 校验则游客也可)
|
||||||
|
*
|
||||||
|
* @param role 要求的角色名
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
|||||||
@@ -12,20 +12,28 @@ import org.springframework.web.context.request.ServletRequestAttributes
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 角色权限校验切面
|
* 角色权限校验切面
|
||||||
* 拦截 @RequireRole 注解的方法,校验 request 中 role 是否匹配
|
* 拦截 @RequireRole 注解的方法,按角色继承关系校验
|
||||||
*
|
*
|
||||||
* 等保三级审计日志字段:
|
* 角色继承链:
|
||||||
* - 用户身份:username
|
* admin → 自动拥有 user 权限(admin IS-A user)
|
||||||
* - 来源 IP:
|
* user → 仅拥有 user 权限
|
||||||
* - 请求路径:method + URI
|
* 游客 → 无角色,仅能访问无需 @RequireRole 的公开接口
|
||||||
* - 所需角色 / 实际角色
|
*
|
||||||
* - 操作结果:denied / passed
|
* 校验规则:
|
||||||
|
* @RequireRole("admin") → 仅 admin
|
||||||
|
* @RequireRole("user") → user 和 admin 均可
|
||||||
*/
|
*/
|
||||||
@Aspect
|
@Aspect
|
||||||
@Component
|
@Component
|
||||||
class RequireRoleAspect(
|
class RequireRoleAspect(
|
||||||
private val ipExtractor: IpExtractor
|
private val ipExtractor: IpExtractor
|
||||||
) {
|
) {
|
||||||
|
// 角色权限集:admin 继承 user 的所有权限
|
||||||
|
private val rolePermissions = mapOf(
|
||||||
|
"admin" to setOf("admin", "user"),
|
||||||
|
"user" to setOf("user")
|
||||||
|
)
|
||||||
|
|
||||||
@Around("@annotation(requireRole)")
|
@Around("@annotation(requireRole)")
|
||||||
fun checkRole(
|
fun checkRole(
|
||||||
joinPoint: ProceedingJoinPoint,
|
joinPoint: ProceedingJoinPoint,
|
||||||
@@ -38,7 +46,8 @@ class RequireRoleAspect(
|
|||||||
val ip = ipExtractor.getRealIp(request)
|
val ip = ipExtractor.getRealIp(request)
|
||||||
val path = "${request.method} ${request.requestURI}"
|
val path = "${request.method} ${request.requestURI}"
|
||||||
|
|
||||||
if (role != requireRole.role) {
|
val allowedRoles = rolePermissions[role] ?: emptySet()
|
||||||
|
if (requireRole.role !in allowedRoles) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}",
|
"[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}",
|
||||||
username, ip, path, requireRole.role, role
|
username, ip, path, requireRole.role, role
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
|||||||
* Spring MVC 配置 — 注册 JWT 鉴权拦截器
|
* Spring MVC 配置 — 注册 JWT 鉴权拦截器
|
||||||
*
|
*
|
||||||
* 拦截规则:
|
* 拦截规则:
|
||||||
* /api/ — 所有 /api/ 下的请求都需要校验 JWT
|
* /api/ — 所有 /api/ 下的请求都需要校验 JWT
|
||||||
* /api/auth/login — 排除,登录不需要 token(此时还没有 token)
|
* /api/auth/login — 排除,登录不需要 token
|
||||||
* /api/auth/logout — 排除,登出不校验 token(客户端自行删除即可)
|
* /api/auth/logout — 排除,登出不校验 token
|
||||||
|
* /api/dashboard/get-all-books — 排除,游客可浏览图书列表
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
class WebConfig(
|
class WebConfig(
|
||||||
@@ -23,7 +24,8 @@ class WebConfig(
|
|||||||
.addPathPatterns("/api/**")
|
.addPathPatterns("/api/**")
|
||||||
.excludePathPatterns(
|
.excludePathPatterns(
|
||||||
"/api/auth/login",
|
"/api/auth/login",
|
||||||
"/api/auth/logout"
|
"/api/auth/logout",
|
||||||
|
"/api/dashboard/get-all-books"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-15
@@ -12,50 +12,52 @@ import org.springframework.web.bind.annotation.RequestMapping
|
|||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 管理端仪表盘接口(仅限管理员访问)
|
* 仪表盘接口
|
||||||
* 路径前缀:/api/admin/dashboard
|
* 路径前缀:/api/dashboard
|
||||||
*
|
*
|
||||||
* 所有接口需要 @RequireRole("admin") + JWT 鉴权拦截器双重校验
|
* 权限模型:
|
||||||
|
* getAllBooks — 无注解 + WebConfig 排除 JWT,游客 / 用户 / admin 均可
|
||||||
|
* getAllBorrowRecords — @RequireRole("admin"),仅管理员
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/admin/dashboard")
|
@RequestMapping("/api/dashboard")
|
||||||
class AdminDashBoardController(
|
class DashBoardController(
|
||||||
private val adminDashBoardService: AdminDashBoardService,
|
private val adminDashBoardService: AdminDashBoardService,
|
||||||
private val ipExtractor: IpExtractor,
|
private val ipExtractor: IpExtractor,
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
* GET /api/admin/dashboard/get-all-books
|
* GET /api/dashboard/get-all-books
|
||||||
* 查询全部图书列表
|
* 查询全部图书列表,游客无需登录即可访问
|
||||||
*
|
*
|
||||||
* 成功响应(code=200):
|
* 成功响应(code=200):
|
||||||
* { "code":200, "message":"success", "data":[{"id":1,"name":"三体",...}, ...] }
|
* { "code":200, "message":"success", "data":[{"id":1,"name":"三体",...}, ...] }
|
||||||
*/
|
*/
|
||||||
@RequireRole("admin")
|
|
||||||
@GetMapping("/get-all-books")
|
@GetMapping("/get-all-books")
|
||||||
fun getAllBooks(
|
fun getAllBooks(
|
||||||
@RequestAttribute username: String,
|
@RequestAttribute(required = false) username: String?,
|
||||||
request: HttpServletRequest
|
request: HttpServletRequest
|
||||||
): Result<Any?> {
|
): Result<Any?> {
|
||||||
log.info("[AdminDashBoard] getAllBooks: user={}, ip={}", username, ipExtractor.getRealIp(request))
|
val user = username ?: "guest"
|
||||||
log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent"))
|
log.info("[DashBoard] getAllBooks: user={}, ip={}", user, ipExtractor.getRealIp(request))
|
||||||
|
log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent"))
|
||||||
return adminDashBoardService.getAllBooks()
|
return adminDashBoardService.getAllBooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* GET /api/admin/dashboard/get-all-borrow-records
|
* GET /api/dashboard/admin/get-all-borrow-records
|
||||||
* 查询全部借阅记录列表
|
* 查询全量借阅记录(仅管理员)
|
||||||
*
|
*
|
||||||
* 成功响应(code=200):
|
* 成功响应(code=200):
|
||||||
* { "code":200, "message":"success", "data":[{"id":1,"userId":1,"bookId":2,...}, ...] }
|
* { "code":200, "message":"success", "data":[{"id":1,"userId":1,"bookId":2,...}, ...] }
|
||||||
*/
|
*/
|
||||||
@RequireRole("admin")
|
@RequireRole("admin")
|
||||||
@GetMapping("/get-all-borrow-records")
|
@GetMapping("/admin/get-all-borrow-records")
|
||||||
fun getAllBorrowRecords(
|
fun getAllBorrowRecords(
|
||||||
@RequestAttribute username: String,
|
@RequestAttribute username: String,
|
||||||
request: HttpServletRequest
|
request: HttpServletRequest
|
||||||
): Result<Any?> {
|
): Result<Any?> {
|
||||||
log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username, ipExtractor.getRealIp(request))
|
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()
|
return adminDashBoardService.getAllBorrowRecords()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user