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:
@@ -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,8 +34,6 @@ 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>()
|
||||||
@@ -48,8 +42,6 @@ class InitUserRunner(
|
|||||||
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,16 +11,12 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user