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:
@@ -44,3 +44,8 @@ kotlin {
|
|||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
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
|
package com.msksbr.bookmgr.controller
|
||||||
|
|
||||||
|
import com.msksbr.bookmgr.config.IpExtractor
|
||||||
import com.msksbr.bookmgr.dto.UserLoginDTO
|
import com.msksbr.bookmgr.dto.UserLoginDTO
|
||||||
import com.msksbr.bookmgr.script.log
|
import com.msksbr.bookmgr.script.log
|
||||||
import com.msksbr.bookmgr.service.AuthService
|
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 com.msksbr.bookmgr.template.Result
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpSession
|
||||||
import jakarta.validation.Valid
|
import jakarta.validation.Valid
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
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
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
class AuthController(
|
class AuthController(
|
||||||
val authService: AuthService
|
val authService: AuthService,
|
||||||
|
val ipExtractor: IpExtractor
|
||||||
) {
|
) {
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
fun login(
|
fun login(
|
||||||
@@ -28,27 +30,28 @@ class AuthController(
|
|||||||
loginDTO: UserLoginDTO,
|
loginDTO: UserLoginDTO,
|
||||||
session: HttpSession,
|
session: HttpSession,
|
||||||
request: HttpServletRequest
|
request: HttpServletRequest
|
||||||
) : Result<String> {
|
): Result<String> {
|
||||||
log.info("Login from ${request.remoteAddr} username: ${loginDTO.username}")
|
log.info("Login from ${ipExtractor.getRealIp(request)} username: ${loginDTO.username}")
|
||||||
log.info("UA: ${request.getHeader("User-Agent")}")
|
log.info("UA: ${request.getHeader("User-Agent")}")
|
||||||
// 调用service验证
|
// 调用service验证
|
||||||
val pass=authService.login(loginDTO)
|
val pass = authService.login(loginDTO)
|
||||||
return if (pass) {
|
return if (pass) {
|
||||||
// 登录成功,存入session
|
// 登录成功,存入session
|
||||||
session.setAttribute("loginUser", loginDTO.username)
|
session.setAttribute("loginUser", loginDTO.username)
|
||||||
log.info("Login success")
|
log.info("Login success")
|
||||||
Result.success("login success")
|
Result.success("login success")
|
||||||
}else{
|
} else {
|
||||||
log.info("Login failed")
|
log.info("Login failed")
|
||||||
Result.error("username or password not match")
|
Result.error("username or password not match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/logout")
|
@PostMapping("/logout")
|
||||||
fun logout(
|
fun logout(
|
||||||
session: HttpSession,
|
session: HttpSession,
|
||||||
request:HttpServletRequest
|
request: HttpServletRequest
|
||||||
): Result<String> {
|
): Result<String> {
|
||||||
log.info("Logout from ${request.remoteAddr}")
|
log.info("Logout from ${ipExtractor.getRealIp(request)}")
|
||||||
log.info("UA: ${request.getHeader("User-Agent")}")
|
log.info("UA: ${request.getHeader("User-Agent")}")
|
||||||
// 直接销毁session
|
// 直接销毁session
|
||||||
session.invalidate()
|
session.invalidate()
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class AuthServiceImpl(private val userMapper: UserMapper, private val passwordEn
|
|||||||
// 找不到用户时直接返回false
|
// 找不到用户时直接返回false
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
log.debug("User ${loginDTO.username} does not exist")
|
log.debug("User ${loginDTO.username} does not exist")
|
||||||
|
// 跑一遍dummyHash来对齐响应时间,避免“信息泄露”攻击
|
||||||
|
passwordEncoder.encode("dummyHash")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 比对密码
|
// 比对密码
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
spring:
|
spring:
|
||||||
profiles:
|
|
||||||
active: dev
|
|
||||||
application:
|
application:
|
||||||
name: bookMgr
|
name: bookMgr
|
||||||
datasource:
|
datasource:
|
||||||
@@ -18,3 +16,8 @@ mybatis-plus:
|
|||||||
configuration:
|
configuration:
|
||||||
# 开启驼峰命名法
|
# 开启驼峰命名法
|
||||||
map-underscore-to-camel-case: true
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user