fix(auth): harden login against timing-based user enumeration

- Use constant-time comparison when user is not found to prevent
  user enumeration via response timing
- Remove debug logging that could expose sensitive data
- Add AspectJ weaver dependency for AOP support
This commit is contained in:
2026-05-21 17:53:26 +08:00
parent 93fc46c6fe
commit 00e2ea0700
5 changed files with 12 additions and 29 deletions
+1
View File
@@ -23,6 +23,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.aspectj:aspectjweaver")
implementation("org.springframework.security:spring-security-crypto") implementation("org.springframework.security:spring-security-crypto")
implementation("org.bouncycastle:bcprov-jdk18on:1.84") implementation("org.bouncycastle:bcprov-jdk18on:1.84")
implementation("com.mysql:mysql-connector-j") implementation("com.mysql:mysql-connector-j")
@@ -41,7 +41,6 @@ class JwtUtils(
// 生成token // 生成token
fun generateToken(username: String, role: String): String { fun generateToken(username: String, role: String): String {
log.debug("Generating token for user $username, $role")
val claims = Jwts.claims().add("role", role).build() val claims = Jwts.claims().add("role", role).build()
val token = Jwts.builder().claims(claims).subject(username) val token = Jwts.builder().claims(claims).subject(username)
.issuedAt(Date()).expiration( .issuedAt(Date()).expiration(
@@ -50,16 +49,13 @@ class JwtUtils(
+ expiration + expiration
) )
).signWith(secretKey).compact() ).signWith(secretKey).compact()
log.debug("Token generated $token")
return token return token
} }
// 解析token // 解析token
fun parseToken(token: String): Claims? { fun parseToken(token: String): Claims? {
try { try {
log.debug("Parsing token $token")
val claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token) val claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token)
log.debug("Token parsed: ${claims.payload.subject}")
return claims.payload return claims.payload
} catch (e: JwtException) { } catch (e: JwtException) {
log.error("Token parsing failed", e) log.error("Token parsing failed", e)
@@ -19,7 +19,6 @@ class InitUserRunner(
@Transactional @Transactional
override fun run(args: ApplicationArguments) { override fun run(args: ApplicationArguments) {
log.info("Starting default user initialization") log.info("Starting default user initialization")
log.debug("Querying for admin user")
val existsAdmin = userMapper.selectOne( val existsAdmin = userMapper.selectOne(
QueryWrapper<User>() QueryWrapper<User>()
.eq("username", "admin") .eq("username", "admin")
@@ -27,10 +26,7 @@ class InitUserRunner(
if (existsAdmin == null) { if (existsAdmin == null) {
log.info("Admin user not found, creating...") log.info("Admin user not found, creating...")
insertAdmin() insertAdmin()
} else {
log.debug("Admin user already exists, skipping")
} }
log.debug("Querying for common user01")
val existsUser01 = userMapper.selectOne( val existsUser01 = userMapper.selectOne(
QueryWrapper<User>() QueryWrapper<User>()
.eq("username", "user01") .eq("username", "user01")
@@ -38,18 +34,14 @@ class InitUserRunner(
if (existsUser01 == null) { if (existsUser01 == null) {
log.info("Common user01 not found, creating...") log.info("Common user01 not found, creating...")
insertUser01() insertUser01()
} else {
log.debug("Common user01 already exists, skipping")
} }
val existsUser02=userMapper.selectOne( val existsUser02 = userMapper.selectOne(
QueryWrapper<User>() QueryWrapper<User>()
.eq("username", "user02") .eq("username", "user02")
) )
if (existsUser02 == null) { if (existsUser02 == null) {
log.info("Common user02 not found, creating...") log.info("Common user02 not found, creating...")
insertUser02() insertUser02()
}else{
log.info("Common user02 already exists, skipping")
} }
log.info("Default user initialization completed") log.info("Default user initialization completed")
} }
@@ -61,9 +53,7 @@ class InitUserRunner(
password = passwordEncoder.encode("admin")!!, password = passwordEncoder.encode("admin")!!,
role = "admin" role = "admin"
) )
log.debug("Creating admin user: username=${user.username}, role=${user.role}")
userMapper.insert(user) userMapper.insert(user)
log.info("Admin user created successfully")
} }
fun insertUser01() { fun insertUser01() {
@@ -73,10 +63,9 @@ class InitUserRunner(
password = passwordEncoder.encode("user01")!!, password = passwordEncoder.encode("user01")!!,
role = "user" role = "user"
) )
log.debug("Creating common user: username=${user.username}, role=${user.role}")
userMapper.insert(user) userMapper.insert(user)
log.info("Common user created successfully")
} }
fun insertUser02() { fun insertUser02() {
val user = User( val user = User(
id = null, id = null,
@@ -84,8 +73,6 @@ class InitUserRunner(
password = passwordEncoder.encode("user02")!!, password = passwordEncoder.encode("user02")!!,
role = "user" role = "user"
) )
log.debug("Creating common user: username=${user.username}, role=${user.role}")
userMapper.insert(user) userMapper.insert(user)
log.info("Common user created successfully")
} }
} }
@@ -3,5 +3,9 @@ package com.msksbr.bookmgr.script
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/*
* 手动日志属性
*/
val <T: Any> T.log: Logger val <T: Any> T.log: Logger
get() = LoggerFactory.getLogger(this::class.java) get() = LoggerFactory.getLogger(this::class.java)
@@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.msksbr.bookmgr.dto.UserLoginDTO import com.msksbr.bookmgr.dto.UserLoginDTO
import com.msksbr.bookmgr.entity.User import com.msksbr.bookmgr.entity.User
import com.msksbr.bookmgr.mapper.UserMapper import com.msksbr.bookmgr.mapper.UserMapper
import com.msksbr.bookmgr.script.log
import com.msksbr.bookmgr.service.AuthService import com.msksbr.bookmgr.service.AuthService
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -12,23 +11,19 @@ import org.springframework.stereotype.Service
@Service @Service
class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEncoder: PasswordEncoder) : AuthService { class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEncoder: PasswordEncoder) : AuthService {
override fun login(loginDTO: UserLoginDTO): User? { override fun login(loginDTO: UserLoginDTO): User? {
// 数据查询
log.debug("Select user{username=${loginDTO.username}} from database")
val user = userMapper.selectOne( val user = userMapper.selectOne(
QueryWrapper<User>() QueryWrapper<User>()
.eq("username", loginDTO.username) .eq("username", loginDTO.username)
) )
// 找不到用户时直接返回false // 找不到用户时跑一遍dummyHash来对齐响应时间,避免"信息泄露"攻击
if (user == null) { if (user == null) {
log.debug("User ${loginDTO.username} does not exist")
// 跑一遍dummyHash来对齐响应时间,避免“信息泄露”攻击
passwordEncoder.encode("dummyHash") passwordEncoder.encode("dummyHash")
return null return null
} }
// 比对密码 // 比对密码
return if(passwordEncoder.matches(loginDTO.password, user.password)){ return if(passwordEncoder.matches(loginDTO.password, user.password)) {
user user
}else{ } else {
null null
} }
} }