From 44b8326e96263272e42f28fdb74fb7f263a9ee84 Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Thu, 21 May 2026 14:04:00 +0800 Subject: [PATCH] feat(auth): implement JWT authentication - Add JwtUtils for token generation and validation using jjwt - Refactor AuthService.login to return User instead of Boolean - Add jjwt dependencies and integrate JWT into login flow - Externalize JWT secret, expiration, and log level as configurable env vars with defaults --- build.gradle.kts | 3 + .../com/msksbr/bookmgr/config/JwtUtils.kt | 71 +++++++++++++++++++ .../bookmgr/controller/AuthController.kt | 28 ++++---- .../com/msksbr/bookmgr/service/AuthService.kt | 3 +- .../bookmgr/service/impl/AuthServiceImpl.kt | 10 ++- src/main/resources/application.yaml | 14 ++-- 6 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt diff --git a/build.gradle.kts b/build.gradle.kts index db5fd77..48dee34 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,9 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("io.github.oshai:kotlin-logging-jvm:7.0.0") + implementation("io.jsonwebtoken:jjwt-api:0.13.0") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt b/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt new file mode 100644 index 0000000..bfa59a4 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt @@ -0,0 +1,71 @@ +package com.msksbr.bookmgr.config + +import com.msksbr.bookmgr.script.log +import io.jsonwebtoken.Claims +import io.jsonwebtoken.JwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.* +import javax.crypto.SecretKey + +/* +* 注册JWT的失效时间和密钥 +*/ +@Component +class JwtUtils( + @Value("\${jwt.secret}") private val configuredSecret: String, + @Value("\${jwt.expiration}") private val expiration: Long +) { + private val secretKey: SecretKey + + // 没有配置key时自动生成 + init { + val keyBytes = if (configuredSecret.isNotBlank()) { + // 把用户提供的secret,用SHA-256哈希成256字节固定长度 + val md = MessageDigest.getInstance("SHA-256") + md.digest(configuredSecret.toByteArray()) + } else { + val bytes = ByteArray(64) + SecureRandom().nextBytes(bytes) + val generated = Base64.getEncoder().encodeToString(bytes) + log.warn("JWT secret not configured, auto-generated: $generated") + log.warn("Set JWT_SECRET env var to persist across restarts") + generated.toByteArray() + } + secretKey = Keys.hmacShaKeyFor(keyBytes) + } + + // 生成token + fun generateToken(username: String, role: String): String { + log.debug("Generating token for user $username, $role") + val claims = Jwts.claims().add("role", role).build() + val token = Jwts.builder().claims(claims).subject(username) + .issuedAt(Date()).expiration( + Date( + System.currentTimeMillis() + + expiration + ) + ).signWith(secretKey).compact() + log.debug("Token generated $token") + return token + } + + // 解析token + fun parseToken(token: String): Claims? { + try { + log.debug("Parsing token $token") + val claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token) + log.debug("Token parsed: ${claims.payload.subject}") + return claims.payload + } catch (e: JwtException) { + log.error("Token parsing failed", e) + } catch (e: IllegalArgumentException) { + log.error("Token parsing failed with illegalArguments: $token", e) + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt index 8f586df..a535036 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt @@ -1,12 +1,12 @@ package com.msksbr.bookmgr.controller import com.msksbr.bookmgr.config.IpExtractor +import com.msksbr.bookmgr.config.JwtUtils import com.msksbr.bookmgr.dto.UserLoginDTO import com.msksbr.bookmgr.script.log import com.msksbr.bookmgr.service.AuthService import com.msksbr.bookmgr.template.Result import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpSession import jakarta.validation.Valid import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -21,25 +21,31 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/api/auth") class AuthController( val authService: AuthService, - val ipExtractor: IpExtractor + val ipExtractor: IpExtractor, + private val jwtUtils: JwtUtils ) { @PostMapping("/login") fun login( @Valid @RequestBody loginDTO: UserLoginDTO, - session: HttpSession, request: HttpServletRequest - ): Result { + ): Result { log.info("Login from ${ipExtractor.getRealIp(request)} username: ${loginDTO.username}") log.info("UA: ${request.getHeader("User-Agent")}") // 调用service验证 - val pass = authService.login(loginDTO) - return if (pass) { - // 登录成功,存入session - session.setAttribute("loginUser", loginDTO.username) + val user = authService.login(loginDTO) + return if (user != null) { + // 登录成功,返回JWT + val token = jwtUtils.generateToken(user.username, user.role) log.info("Login success") - Result.success("login success") + Result.success( + mapOf( + "token" to token, + "username" to user.username, + "role" to user.role + ) + ) } else { log.info("Login failed") Result.error("username or password not match") @@ -48,13 +54,11 @@ class AuthController( @PostMapping("/logout") fun logout( - session: HttpSession, request: HttpServletRequest ): Result { log.info("Logout from ${ipExtractor.getRealIp(request)}") log.info("UA: ${request.getHeader("User-Agent")}") - // 直接销毁session - session.invalidate() + // JWT无状态,只需返回成功让客户端自行删除token即可 log.info("Logout success") return Result.success("logout successfully") } diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/AuthService.kt b/src/main/kotlin/com/msksbr/bookmgr/service/AuthService.kt index 32b0b9d..7c6bf4f 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/AuthService.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/AuthService.kt @@ -1,7 +1,8 @@ package com.msksbr.bookmgr.service import com.msksbr.bookmgr.dto.UserLoginDTO +import com.msksbr.bookmgr.entity.User interface AuthService { - fun login(loginDTO: UserLoginDTO): Boolean + fun login(loginDTO: UserLoginDTO): User? } \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AuthServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AuthServiceImpl.kt index 9ac5613..dfe21b2 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AuthServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AuthServiceImpl.kt @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service @Service class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEncoder: PasswordEncoder) : AuthService { - override fun login(loginDTO: UserLoginDTO): Boolean { + override fun login(loginDTO: UserLoginDTO): User? { // 数据查询 log.debug("Select user{username=${loginDTO.username}} from database") val user = userMapper.selectOne( @@ -23,9 +23,13 @@ class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEn log.debug("User ${loginDTO.username} does not exist") // 跑一遍dummyHash来对齐响应时间,避免“信息泄露”攻击 passwordEncoder.encode("dummyHash") - return false + return null } // 比对密码 - return passwordEncoder.matches(loginDTO.password, user.password) + return if(passwordEncoder.matches(loginDTO.password, user.password)){ + user + }else{ + null + } } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index ce5ac48..6491c19 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -9,8 +9,8 @@ spring: jackson: default-property-inclusion: non_null property-naming-strategy: SNAKE_CASE - date-format: ${JSON_DATE_FORMAT} - time-zone: ${JSON_TIME_ZONE} + date-format: ${JSON_DATE_FORMAT:} + time-zone: ${JSON_TIME_ZONE:} mybatis-plus: configuration: @@ -18,6 +18,10 @@ mybatis-plus: map-underscore-to-camel-case: true # 开启日志输出sql语句 # log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl -# logging: -# level: -# com.msksbr.bookmgr: "DEBUG" \ No newline at end of file +logging: + level: + com.msksbr.bookmgr: ${LOG_LEVEL:INFO} + +jwt: + secret: ${JWT_SECRET:} + expiration: ${JWT_EXPIRATION_TIME:86400000} \ No newline at end of file