Spring Security ์์ด ๊ตฌํํ๋ ์ด์
ํฐ ํ๋ฆ์ ์ก์๋ณด์.
ํด๋ผ์ด์ธํธ๋ ํ์๊ฐ์
, ๋ก๊ทธ์ธ, ๋ง์ดํ์ด์ง ์ด 3๊ฐ์ง API์ ์ ๊ทผํ ์ ์๋ค.
๋ก๊ทธ์ธ ์ ,ํ๋ก ๋ง์ดํ์ด์ง ์ ๊ทผ ๊ฐ๋ฅ์ฌ๋ถ๋ก ํ ์คํธ๋ฅผ ์งํํ๋ค.
@Component
class JwtUtils {
val SECRET = "secret"
val EXP_TIME = 1000 * 60 * 10
// ํ ํฐ์์ฑ
fun generateToken(userId: Long, username: String): String {
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setExpiration(Date(System.currentTimeMillis() + EXP_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact()
}
// ํ ํฐ๊ฒ์ฆ
fun verifyToken(token: String): Boolean {
return try {
val claims = getAllClaims(token)
val expiration = claims.expiration
expiration.after(Date())
} catch (e: JwtException) {
false
} catch (e: IllegalArgumentException) {
false
}
}
// Claim ์กฐํ
fun getClaim(token: String, key: String ): Any? {
val claims: Claims = getAllClaims(token)
return claims[key]
}
private fun getAllClaims(token: String): Claims {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJwt(token).body
}
}
class JwtTokenExtractor {
companion object{
const val AUTHORIZATION_HEADER_PREFIX = "Authorization"
const val BEARER_TYPE_PREFIX = "Bearer "
fun extract(request: HttpServletRequest): String? {
val authorization: String? = request.getHeader(AUTHORIZATION_HEADER_PREFIX)
authorization?.let {
if(authorization.startsWith(BEARER_TYPE_PREFIX)) {
return authorization.substring(BEARER_TYPE_PREFIX.length)
}
}
return null
}
}
}
Spring Security๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด SecurityContext์ Authentication์ ๊ฐ์ ธ์ค๋ @AuthenticationPrincipal
์ด๋
ธํ
์ด์
์ ๋งค์ฐ ์์ฃผ ์ฌ์ฉํ๋ค. ๋์ผํ ๊ธฐ๋ฅ์ ๊ฐ์ง๋ ์ด๋
ธํ
์ด์
์ ์์ฑํด๋ณธ๋ค.
์ด๋ ธํ ์ด์ ํด๋์ค ์์ฑ
@Target(AnnotationTarget.VALUE_PARAMETER) // ์์ฑ์ ๋งค๊ฐ๋ณ์์ ์ ์ฉ
@Retention(AnnotationRetention.RUNTIME)
annotation class LoginUser()
์ด๋ ธํ ์ด์ ๊ตฌํ ํด๋์ค ์์ฑ (HandlerMethodArgumentResolver)
@Component
class LoginUserArgumentResolver(
private val jwtUtils: JwtUtils,
private val userRepository: UserRepository
): HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(LoginUser::class.java)
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): User? {
val httpServletRequest: HttpServletRequest = webRequest.getNativeRequest(HttpServletRequest::class.java) as HttpServletRequest
// Http ์์ฒญ์์ ํ ํฐ์ถ์ถ
val token = JwtTokenExtractor.extract(httpServletRequest)
// ํ ํฐ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
token?.let {
if(jwtUtils.verifyToken(token)) { // ํ ํฐ๊ฒ์ฆ
val userId = jwtUtils.getClaim(token, "id") // ๊ฒ์ฆ๋ ํ ํฐ์์ id๊ฐ์ ๋ฝ์์ User๋ฅผ ์กฐํ
return userRepository.findByIdOrNull(userId as Long)
}
}
return null
}
// ํ ํฐ์์ user pk ์ถ์ถ
private fun getUserIdFromToken(token: String): Long? {
return jwtUtils.getClaim(token, "id") as Long?
}
}
์ด์ ์์ฑ์์์ User
๋ฅผ ๋ฐ๋ ๊ฒฝ์ฐ @LoginUser
๋ฅผ ๋ถ์ฌ์ฃผ๋ฉด ํ์ฌ ํ ํฐ์ ์์ ํ User
๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ค.
JwtUtils
์ ๊ฒ์ฆ๋ฉ์๋๋ฅผ ์ด์ฉํด์ ์์ฒญ์ ํค๋๊ฐ ํ ํฐ์ ํฌํจํ๋ ๊ฒฝ์ฐ ๊ฒ์ฆ์ ์ํํ๋ค.
@Component
class TokenVerifyInterceptor(
private val jwtUtils: JwtUtils
) : HandlerInterceptor {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
val method = request.method
// ์ฌ์ ์์ฒญ์ ๊ฒฝ์ฐ pass
if (method.equals(HttpMethod.OPTIONS)) return true
// Http ์์ฒญ์์ ํ ํฐ ์ถ์ถ
val token: String? = JwtTokenExtractor.extract(request)
// ํ ํฐ์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
token?.let {
jwtUtils.verifyToken(it) // ํ ํฐ๊ฒ์ฆ
} ?: throw AuthenticationException("์ธ์ฆ์คํจ")
return true
}
}
excludePathPatterns
LoginUser
๊ตฌํ๋ถ (ArgumentResolver) ๋ฑ๋ก@Configuration
class WebMvcConfig(
private val tokenVerifyInterceptor: TokenVerifyInterceptor,
private val loginUserArgumentResolver: LoginUserArgumentResolver
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(tokenVerifyInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/signup", "/user/signin")
}
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(loginUserArgumentResolver)
}
}
ํ์๊ฐ์ (signup), ๋ก๊ทธ์ธ(signin), ๋ง์ดํ์ด์ง(me)
/users/me
๋ก ์ ๊ทผํ๋ ๊ฒฝ์ฐ ํ ํฐ๊ฒ์ฆ ์ธํฐ์
ํฐ๋ฅผ ๊ฑฐ์น๊ณ @LoginUser
๋ฅผ ํตํด ํ์ฌ ํ ํฐ์ ์์ ํ User์ ์ ๋ณด๋ฅผ ๋ฐ์์์ ๊ทธ๋๋ก ๋ฆฌํดํ๋ค. @RequestMapping("/users")
@RestController
class UserController(private val userService: UserService) {
@PostMapping("/signup")
fun signup(@RequestBody signupRequest: SignupRequest): ResponseEntity<String>
= ResponseEntity.status(HttpStatus.CREATED).body(userService.saveUser(signupRequest))
@PostMapping("/signin")
fun signin(@RequestBody signinRequest: SigninRequest): ResponseEntity<String>
= ResponseEntity.ok().body(userService.signin(signinRequest))
@GetMapping("/me")
fun me(@LoginUser user: User?): ResponseEntity<User> {
return ResponseEntity.ok().body(user)
}
}
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
@SpringBootTest
class UserTest {
@Autowired lateinit var mockMvc: MockMvc
@Autowired lateinit var objectMapper: ObjectMapper
@Autowired lateinit var userRepository: UserRepository
@Autowired lateinit var passwordEncoder: PasswordEncoder
@Autowired lateinit var userService: UserService
lateinit var user: User
@BeforeAll
fun beforeAll() {
user = User(null, "test", passwordEncoder.encode("test"))
userRepository.save(user)
}
@DisplayName("ํ์๊ฐ์
")
@Test
fun `ํ์๊ฐ์
ํ
์คํธ` () {
val signupRequest = SignupRequest("username", "password")
mockMvc.post("/users/signup")
{
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(signupRequest)
}
.andExpect {
status { isCreated() }
}
.andDo {
print()
}
}
@DisplayName("์ธ์ฆ (ํ ํฐ๋ฐ๊ธ)")
@Test
fun `์ธ์ฆ ํ
์คํธ ํ ํฐ๋ฐ๊ธ์ด ์ ๋๋ก ๋๋์ง` () {
val signinRequest = SigninRequest("test", "test")
mockMvc.post("/users/signin")
{
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(signinRequest)
}
.andExpect {
status { isOk() }
}
.andDo {
print()
}
}
@DisplayName("์ ๊ทผ๋ถ๊ฐ ํ
์คํธ")
@Test
fun `ํ ํฐ์์ด ์ธ์ฆ์ ํ์๋กํ๋ API์ ์ ๊ทผ ํ
์คํธ`() {
try {
mockMvc.get("/users/me")
.andDo {
print()
}
.andExpect {
status {
is5xxServerError()
}
}
} catch (e: Exception) {
println(e)
}
}
@DisplayName("ํ ํฐ์ธ์ฆ ํ
์คํธ")
@Test
fun `๋ฐ๊ธ๋ ํ ํฐ์ ์ด์ฉํ ์ธ์ฆ ํ
์คํธ` () {
val signinRequest = SigninRequest("test", "test")
val token = userService.signin(signinRequest)
mockMvc.get("/users/me")
{
header("Authorization", "Bearer " + token)
}
.andDo {
print()
}
.andExpect {
status { isOk() }
}
}
}
์ ๋ณด๊ณ ๊ฐ๋๋ค!! ๐๐ผ