fix(auth): harden password verification against timing attacks

- Run dummy hash when user is not found to prevent timing-based enumeration
- Extract and log real client IP on login requests
- Remove unused test files
- Reorder application config for clarity
This commit is contained in:
2026-05-21 02:39:06 +08:00
parent 2cf3806298
commit 3937224341
7 changed files with 52 additions and 94 deletions
+5
View File
@@ -44,3 +44,8 @@ kotlin {
tasks.withType<Test> {
useJUnitPlatform()
}
// 打包时排除dev环境配置
tasks.named<ProcessResources>("processResources") {
exclude("application-dev.yaml")
}
@@ -0,0 +1,25 @@
package com.msksbr.bookmgr.config
import jakarta.servlet.http.HttpServletRequest
import org.springframework.stereotype.Component
//获取真实IP的bean
@Component
class IpExtractor {
fun getRealIp(request: HttpServletRequest): String {
val headers = listOf(
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"X-Real-IP"
)
return headers
.mapNotNull { request.getHeader(it) }
.firstOrNull { it.isNotBlank() && !it.equals("unknown", ignoreCase = true) }
?.split(",")
?.first()
?.trim()
?: request.remoteAddr
}
}
@@ -1,16 +1,17 @@
package com.msksbr.bookmgr.controller
import com.msksbr.bookmgr.config.IpExtractor
import com.msksbr.bookmgr.dto.UserLoginDTO
import com.msksbr.bookmgr.script.log
import com.msksbr.bookmgr.service.AuthService
import jakarta.servlet.http.HttpSession
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
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
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/*
* 登录接口
@@ -19,7 +20,8 @@ import org.springframework.web.bind.annotation.RequestBody
@RestController
@RequestMapping("/api/auth")
class AuthController(
val authService: AuthService
val authService: AuthService,
val ipExtractor: IpExtractor
) {
@PostMapping("/login")
fun login(
@@ -28,27 +30,28 @@ class AuthController(
loginDTO: UserLoginDTO,
session: HttpSession,
request: HttpServletRequest
) : Result<String> {
log.info("Login from ${request.remoteAddr} username: ${loginDTO.username}")
): Result<String> {
log.info("Login from ${ipExtractor.getRealIp(request)} username: ${loginDTO.username}")
log.info("UA: ${request.getHeader("User-Agent")}")
// 调用service验证
val pass=authService.login(loginDTO)
val pass = authService.login(loginDTO)
return if (pass) {
// 登录成功,存入session
session.setAttribute("loginUser", loginDTO.username)
log.info("Login success")
Result.success("login success")
}else{
} else {
log.info("Login failed")
Result.error("username or password not match")
}
}
@PostMapping("/logout")
fun logout(
session: HttpSession,
request:HttpServletRequest
request: HttpServletRequest
): Result<String> {
log.info("Logout from ${request.remoteAddr}")
log.info("Logout from ${ipExtractor.getRealIp(request)}")
log.info("UA: ${request.getHeader("User-Agent")}")
// 直接销毁session
session.invalidate()
@@ -21,6 +21,8 @@ class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEn
// 找不到用户时直接返回false
if (user == null) {
log.debug("User ${loginDTO.username} does not exist")
// 跑一遍dummyHash来对齐响应时间,避免“信息泄露”攻击
passwordEncoder.encode("dummyHash")
return false
}
// 比对密码
+5 -2
View File
@@ -1,6 +1,4 @@
spring:
profiles:
active: dev
application:
name: bookMgr
datasource:
@@ -18,3 +16,8 @@ mybatis-plus:
configuration:
# 开启驼峰命名法
map-underscore-to-camel-case: true
# 开启日志输出sql语句
# log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl
# logging:
# level:
# com.msksbr.bookmgr: "DEBUG"
@@ -1,13 +0,0 @@
package com.msksbr.bookmgr
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class BookMgrApplicationTests {
@Test
fun contextLoads() {
}
}
@@ -1,67 +0,0 @@
package com.msksbr.bookmgr.mapper
import com.msksbr.bookmgr.entity.User
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.transaction.annotation.Transactional
@SpringBootTest
@Transactional // 测试完可以自动回滚
class UserMapperTest {
// Spring自动装配userMapper
@Autowired
private lateinit var userMapper: UserMapper
@Test
// 新增测试
fun testInsert() {
// 准备用户
val user = User(null,"Test", "123456", "admin")
// 插入
userMapper.insert(user)
// 拿到插入后的自增id
val userId = user.id ?: throw AssertionError("用户插入失败,ID为空")
// 查询插入后的用户
val res = userMapper.selectById(userId)
// 比对
assertNotNull(res)
assertEquals(user.username, res.username)
assertEquals(user.password, res.password)
assertEquals(user.role, res.role)
}
// 修改测试
@Test
fun testUpdate() {
// 先新增
val user = User(null, "OldName", "123456", "admin")
userMapper.insert(user)
val userId = user.id!!
// 修改字段
user.username="newName"
user.password="654321"
userMapper.updateById(user)
// 比对
val res = userMapper.selectById(userId)
assertNotNull(res)
assertEquals(user.username, res.username)
assertEquals(user.password, res.password)
assertEquals(user.role, res.role)
}
// 删除测试
@Test
fun testDelete() {
// 新增
val user = User(null,"DelTest", "123456", "admin")
userMapper.insert(user)
val userId = user.id!!
// 删除
userMapper.deleteById(userId)
// 确认删除
val res = userMapper.selectById(userId)
assertNull(res)
}
}