Spring Boot+Kotlin电商后端:基于JWT实现多用户认证方案
问题:多用户JWT认证异常排查与修复
我用Spring Boot + Kotlin开发电商后端,采用JWT存储到httpOnly Cookie的认证方案,单用户登录时功能正常,但多用户登录后只有最新登录的用户能发起API请求。请问该如何构建正确的多用户认证系统?
我的User Controller代码
@RestController @RequestMapping("api") class UserController( private val userService: UserService ) { @ExceptionHandler(NoSuchElementException::class) fun handleNotFound(e: NoSuchElementException): ResponseEntity<String> = ResponseEntity(e.message, HttpStatus.NOT_FOUND) @ExceptionHandler(IllegalArgumentException::class) fun handleBadRequest(e: IllegalArgumentException): ResponseEntity<String> = ResponseEntity(e.message, HttpStatus.BAD_REQUEST) @PostMapping("register") fun register(@RequestBody body: RegisterDTO): ResponseEntity<User> { val user = User() user.name = body.name user.email = body.email user.password = body.password return ResponseEntity.ok(this.userService.save(user)) } @PostMapping("login") fun login(@RequestBody body: LoginDTO, response: HttpServletResponse): ResponseEntity<Any> { val user = this.userService.findByEmail(body.email) ?: return ResponseEntity.badRequest().body(Message("User not found")) if (!user.comparePasswords(body.password)) return ResponseEntity.badRequest().body(Message("Invalid password")) val issuer = user.id.toString() val jwt = Jwts.builder() .setIssuer(issuer) .setExpiration(Date(System.currentTimeMillis() + 60 * 24 * 1000)) .signWith(SignatureAlgorithm.HS512, "secret").compact() val cookie = Cookie("jwt", jwt) cookie.isHttpOnly = true //very IMPORTANT response.addCookie(cookie) return ResponseEntity.ok(Message("Succes")) } @GetMapping("user") fun user(@CookieValue("jwt") jwt: String?): ResponseEntity<Any> { try { if (jwt == null) return ResponseEntity.status(401).body("Not authenticated") val body = Jwts.parser().setSigningKey("secret").parseClaimsJws(jwt).body return ResponseEntity.ok(this.userService.getById(body.issuer.toInt())) } catch (e: Exception) { return ResponseEntity.status(401).body("Not authenticated") } } @PostMapping("logout") fun logout(response: HttpServletResponse): ResponseEntity<Any> { val cookie = Cookie("jwt", "") cookie.maxAge = 0 response.addCookie(cookie) return ResponseEntity.ok("success to delete") } }
DataController示例接口
@GetMapping fun getBanks(@CookieValue jwt: String?): ResponseEntity<Any> { if (jwt == null) return ResponseEntity.status(401).body("Not authenticated") return ResponseEntity.ok(service.getBanks()) }
问题根源与修复方案
1. 核心问题分析
- Cookie配置缺失:当前创建的Cookie未设置
Path和Domain,导致浏览器无法正确区分不同用户的Cookie,新登录用户的JWT会覆盖旧用户的Cookie。 - 校验逻辑零散:每个接口手动检查JWT是否存在,但未验证JWT的签名、过期时间等有效性,存在安全风险,同时多用户场景下旧JWT无法被正确识别。
2. 分步修复
(1)完善Cookie配置
在登录接口创建Cookie时,补充关键属性,避免Cookie被覆盖:
val cookie = Cookie("jwt", jwt) cookie.isHttpOnly = true cookie.path = "/" // 让Cookie在整个应用路径下生效 cookie.domain = "your-domain.com" // 替换为实际域名,本地开发用localhost cookie.maxAge = 86400 // 与JWT过期时间一致,单位秒 cookie.secure = true // 生产环境开启,仅HTTPS传输 response.addCookie(cookie)
(2)统一JWT校验逻辑
使用Spring Security过滤器统一处理JWT校验,避免重复代码:
@Component class JwtFilter(private val userService: UserService) : OncePerRequestFilter() { override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain ) { val jwt = request.cookies?.firstOrNull { it.name == "jwt" }?.value if (jwt != null) { try { val claims = Jwts.parser() .setSigningKey("secret") // 建议将密钥存入配置文件,不要硬编码 .parseClaimsJws(jwt) .body val userId = claims.issuer.toInt() val user = userService.getById(userId) // 将用户信息存入SecurityContext,供后续接口使用 val authentication = UsernamePasswordAuthenticationToken( user, null, emptyList() ) SecurityContextHolder.getContext().authentication = authentication } catch (e: Exception) { // 无效JWT,清除上下文 SecurityContextHolder.clearContext() } } filterChain.doFilter(request, response) } }
(3)配置Spring Security
让Security接管接口认证,使用上述过滤器:
@Configuration @EnableWebSecurity class SecurityConfig(private val jwtFilter: JwtFilter) : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http.csrf().disable() .authorizeRequests() .antMatchers("/api/register", "/api/login").permitAll() // 开放登录注册接口 .anyRequest().authenticated() // 其他接口需认证 .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,不依赖Session } }
(4)简化接口代码
去掉接口中手动JWT校验,直接从SecurityContext获取用户信息:
- UserController的user接口:
@GetMapping("user") fun user(): ResponseEntity<User> { val user = SecurityContextHolder.getContext().authentication.principal as User return ResponseEntity.ok(user) }
- DataController的getBanks接口:
@GetMapping fun getBanks(): ResponseEntity<List<Bank>> { return ResponseEntity.ok(service.getBanks()) }
(5)额外优化
- 将JWT密钥、过期时间存入配置文件(
application.properties):
jwt.secret=your-strong-secret-key jwt.expiration=86400000 # 24小时,单位毫秒
然后通过@Value注入使用,避免硬编码。
- 给用户添加角色权限,在JWT中存入权限信息,实现细粒度权限控制。
- 实现JWT刷新机制,避免用户频繁登录。
内容的提问来源于stack exchange,提问作者Sevban Bayır
相关产品推荐
相关产品推荐

