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.
This commit is contained in:
2026-05-22 13:31:48 +08:00
parent 0ccc21288b
commit 3e7145c091
5 changed files with 53 additions and 111 deletions
@@ -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)
}
}
@@ -11,24 +11,19 @@ import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes import org.springframework.web.context.request.ServletRequestAttributes
/* /*
* 角色权限校验切面 * 角色权限校验切面,拦截 @RequireRole 注解的方法
* 拦截 @RequireRole 注解的方法,按角色继承关系校验
*
* 角色继承链:
* admin → 自动拥有 user 权限(admin IS-A user
* user → 仅拥有 user 权限
* 游客 → 无角色,仅能访问无需 @RequireRole 的公开接口
* *
* 前置条件:JwtPopulateFilter 已将 request attributeusername / role)设置好
* 校验规则: * 校验规则:
* @RequireRole("admin") → 仅 admin * username 为 null → 401(未登录或 token 无效)
* @RequireRole("user") → user 和 admin 均可 * 角色不匹配 → 403(权限不足)
* 角色继承:admin IS-A user
*/ */
@Aspect @Aspect
@Component @Component
class RequireRoleAspect( class RequireRoleAspect(
private val ipExtractor: IpExtractor private val ipExtractor: IpExtractor
) { ) {
// 角色权限集:admin 继承 user 的所有权限
private val rolePermissions = mapOf( private val rolePermissions = mapOf(
"admin" to setOf("admin", "user"), "admin" to setOf("admin", "user"),
"user" to setOf("user") "user" to setOf("user")
@@ -41,11 +36,17 @@ class RequireRoleAspect(
): Any? { ): Any? {
val request = (RequestContextHolder val request = (RequestContextHolder
.currentRequestAttributes() as ServletRequestAttributes).request .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 ip = ipExtractor.getRealIp(request)
val path = "${request.method} ${request.requestURI}" 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() val allowedRoles = rolePermissions[role] ?: emptySet()
if (requireRole.role !in allowedRoles) { if (requireRole.role !in allowedRoles) {
log.warn( log.warn(
@@ -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"
)
}
}
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.RestController
* 路径前缀:/api/dashboard * 路径前缀:/api/dashboard
* *
* 权限模型: * 权限模型:
* getAllBooks — 无注解 + WebConfig 排除 JWT,游客 / 用户 / admin 均可 * getAllBooks — 无注解,游客 / 用户 / admin 均可
* getAllBorrowRecords — @RequireRole("admin"),仅管理员 * getAllBorrowRecords — @RequireRole("admin"),仅管理员
*/ */
@RestController @RestController
@@ -53,10 +53,10 @@ class DashBoardController(
@RequireRole("admin") @RequireRole("admin")
@GetMapping("/admin/get-all-borrow-records") @GetMapping("/admin/get-all-borrow-records")
fun getAllBorrowRecords( fun getAllBorrowRecords(
@RequestAttribute username: String, @RequestAttribute(required = false) 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 ?: "unknown", ipExtractor.getRealIp(request))
log.info("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) log.info("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent"))
return adminDashBoardService.getAllBorrowRecords() return adminDashBoardService.getAllBorrowRecords()
} }
@@ -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("\"", "\\\"")}"}""")
}
}