You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 22:45:40