From 3e7145c0917aa6492ee70ccabca7a8dcdec52ccb Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Fri, 22 May 2026 13:31:48 +0800 Subject: [PATCH] refactor(auth): replace JwtAuthInterceptor with non-blocking filter Remove the interceptor-based JWT auth and its WebConfig registration. Introduce JwtPopulateFilter that silently extracts JWT claims into request attributes without blocking unauthenticated requests. Update DashBoardController to accept nullable username and RequireRoleAspect to handle missing credentials with proper error messages. --- .../bookmgr/config/JwtPopulateFilter.kt | 37 +++++++++++ .../bookmgr/config/RequireRoleAspect.kt | 25 +++---- .../com/msksbr/bookmgr/config/WebConfig.kt | 31 --------- .../bookmgr/controller/DashBoardController.kt | 6 +- .../bookmgr/interceptor/JwtAuthInterceptor.kt | 65 ------------------- 5 files changed, 53 insertions(+), 111 deletions(-) create mode 100644 src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt delete mode 100644 src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt delete mode 100644 src/main/kotlin/com/msksbr/bookmgr/interceptor/JwtAuthInterceptor.kt diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt b/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt new file mode 100644 index 0000000..c068746 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt @@ -0,0 +1,37 @@ +package com.msksbr.bookmgr.config + +import com.msksbr.bookmgr.script.log +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +/* +* JWT 用户识别 Filter(可选模式,永不拦截) +* 在 Spring 参数解析之前运行,从 Authorization 头提取 JWT 并写入 request attribute +* 无 token / 格式错误 / 过期均静默放行 +*/ +@Component +class JwtPopulateFilter( + private val jwtUtils: JwtUtils +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader != null && authHeader.startsWith("Bearer ")) { + val token = authHeader.removePrefix("Bearer ") + val claims = jwtUtils.parseToken(token) + if (claims != null) { + request.setAttribute("username", claims.subject) + request.setAttribute("role", claims.get("role", String::class.java)) + log.debug("[JWT] identified: user={}, role={}", claims.subject, claims.get("role", String::class.java)) + } + } + filterChain.doFilter(request, response) + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt index 1d1557c..afad2de 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt @@ -11,24 +11,19 @@ import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.context.request.ServletRequestAttributes /* -* 角色权限校验切面 -* 拦截 @RequireRole 注解的方法,按角色继承关系校验 -* -* 角色继承链: -* admin → 自动拥有 user 权限(admin IS-A user) -* user → 仅拥有 user 权限 -* 游客 → 无角色,仅能访问无需 @RequireRole 的公开接口 +* 角色权限校验切面,拦截 @RequireRole 注解的方法 * +* 前置条件:JwtPopulateFilter 已将 request attribute(username / role)设置好 * 校验规则: -* @RequireRole("admin") → 仅 admin -* @RequireRole("user") → user 和 admin 均可 +* username 为 null → 401(未登录或 token 无效) +* 角色不匹配 → 403(权限不足) +* 角色继承:admin IS-A user */ @Aspect @Component class RequireRoleAspect( private val ipExtractor: IpExtractor ) { - // 角色权限集:admin 继承 user 的所有权限 private val rolePermissions = mapOf( "admin" to setOf("admin", "user"), "user" to setOf("user") @@ -41,11 +36,17 @@ class RequireRoleAspect( ): Any? { val request = (RequestContextHolder .currentRequestAttributes() as ServletRequestAttributes).request - val username = request.getAttribute("username") as? String ?: "unknown" - val role = request.getAttribute("role") as? String val ip = ipExtractor.getRealIp(request) val path = "${request.method} ${request.requestURI}" + val username = request.getAttribute("username") as? String + val role = request.getAttribute("role") as? String + + if (username == null) { + log.warn("[AUDIT] unauthenticated | ip={} | path={} | required={}", ip, path, requireRole.role) + return Result.unauthorized("Missing or invalid token") + } + val allowedRoles = rolePermissions[role] ?: emptySet() if (requireRole.role !in allowedRoles) { log.warn( diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt b/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt deleted file mode 100644 index 76964e8..0000000 --- a/src/main/kotlin/com/msksbr/bookmgr/config/WebConfig.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.msksbr.bookmgr.config - -import com.msksbr.bookmgr.interceptor.JwtAuthInterceptor -import org.springframework.context.annotation.Configuration -import org.springframework.web.servlet.config.annotation.InterceptorRegistry -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer - -/* -* Spring MVC 配置 — 注册 JWT 鉴权拦截器 -* -* 拦截规则: -* /api/ — 所有 /api/ 下的请求都需要校验 JWT -* /api/auth/login — 排除,登录不需要 token -* /api/auth/logout — 排除,登出不校验 token -* /api/dashboard/get-all-books — 排除,游客可浏览图书列表 -*/ -@Configuration -class WebConfig( - private val jwtAuthInterceptor: JwtAuthInterceptor -) : WebMvcConfigurer { - - override fun addInterceptors(registry: InterceptorRegistry) { - registry.addInterceptor(jwtAuthInterceptor) - .addPathPatterns("/api/**") - .excludePathPatterns( - "/api/auth/login", - "/api/auth/logout", - "/api/dashboard/get-all-books" - ) - } -} diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt index cfb34b7..d0c9905 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt @@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RestController * 路径前缀:/api/dashboard * * 权限模型: -* getAllBooks — 无注解 + WebConfig 排除 JWT,游客 / 用户 / admin 均可 +* getAllBooks — 无注解,游客 / 用户 / admin 均可 * getAllBorrowRecords — @RequireRole("admin"),仅管理员 */ @RestController @@ -53,10 +53,10 @@ class DashBoardController( @RequireRole("admin") @GetMapping("/admin/get-all-borrow-records") fun getAllBorrowRecords( - @RequestAttribute username: String, + @RequestAttribute(required = false) username: String?, request: HttpServletRequest ): Result { - log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username, ipExtractor.getRealIp(request)) + log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username ?: "unknown", ipExtractor.getRealIp(request)) log.info("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) return adminDashBoardService.getAllBorrowRecords() } diff --git a/src/main/kotlin/com/msksbr/bookmgr/interceptor/JwtAuthInterceptor.kt b/src/main/kotlin/com/msksbr/bookmgr/interceptor/JwtAuthInterceptor.kt deleted file mode 100644 index 788db1c..0000000 --- a/src/main/kotlin/com/msksbr/bookmgr/interceptor/JwtAuthInterceptor.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.msksbr.bookmgr.interceptor - -import com.msksbr.bookmgr.config.JwtUtils -import com.msksbr.bookmgr.script.log -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Component -import org.springframework.web.servlet.HandlerInterceptor - -/* -* JWT 鉴权拦截器 -* 在每个受保护的 API 请求到达 Controller 之前执行,从 Authorization 头提取并校验 JWT -* -* 校验失败时直接返回 401 JSON 响应(使用 Result.unauthorized 格式),请求不会到达 Controller -* 校验成功后从 token 中提取 username 和 role,写入 request attribute,后续可通过 @RequestAttribute 获取 -* -* 返回体格式: -* {"code":401,"message":"Missing Authorization header"} -* {"code":401,"message":"Invalid token format"} -* {"code":401,"message":"Token invalid or expired"} -*/ -@Component -class JwtAuthInterceptor( - private val jwtUtils: JwtUtils -) : HandlerInterceptor { - - override fun preHandle( - request: HttpServletRequest, - response: HttpServletResponse, - handler: Any - ): Boolean { - // 1. 检查 Authorization 头是否存在 - val authHeader = request.getHeader("Authorization") ?: run { - writeUnauthorized(response, "Missing Authorization header") - return false - } - // 2. 检查前缀是否为 "Bearer " - if (!authHeader.startsWith("Bearer ")) { - writeUnauthorized(response, "Invalid token format") - return false - } - // 3. 解析并验证 token - val token = authHeader.removePrefix("Bearer ") - val claims = jwtUtils.parseToken(token) - if (claims == null) { - writeUnauthorized(response, "Token invalid or expired") - return false - } - // 4. 校验通过,用户信息写入 request attribute - request.setAttribute("username", claims.subject) - request.setAttribute("role", claims.get("role", String::class.java)) - log.debug("[JWT] authenticated: user={}, role={}", claims.subject, claims.get("role", String::class.java)) - return true - } - - /* - * 写入 401 响应,message 字段按 JSON 字符串规范转义 - */ - private fun writeUnauthorized(response: HttpServletResponse, message: String) { - response.status = HttpStatus.UNAUTHORIZED.value() - response.contentType = "application/json;charset=UTF-8" - response.writer.write("""{"code":401,"message":"${message.replace("\\", "\\\\").replace("\"", "\\\"")}"}""") - } -}