Skip to content

Commit

Permalink
feat: 스프링 시큐리티 jwt 적용 WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
hjj4060 committed Jul 31, 2024
1 parent 35385de commit 8444413
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.store.clothstar.common.config

import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
import org.store.clothstar.common.dto.MessageDTO
import java.io.IOException

@Component
class CustomAuthenticationEntryPoint : AuthenticationEntryPoint {
private val log = KotlinLogging.logger {}

@Throws(IOException::class, ServletException::class)
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException,
) {
log.error { "인증 실패 로직 실행" }
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.characterEncoding = "UTF-8"
response.contentType = "application/json"

val messageDTO = MessageDTO(
HttpServletResponse.SC_UNAUTHORIZED,
"권한이 없습니다."
)

response.writer.write(ObjectMapper().writeValueAsString(messageDTO))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.store.clothstar.common.config

import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
import org.store.clothstar.common.error.ErrorCode
import org.store.clothstar.member.domain.Account
import org.store.clothstar.member.domain.CustomUserDetails
import org.store.clothstar.member.repository.AccountRepository

@Service
class CustomUserDetailsService(
private val accountRepository: AccountRepository,
) : UserDetailsService {
private val log = KotlinLogging.logger {}

@Throws(UsernameNotFoundException::class)
override fun loadUserByUsername(email: String): UserDetails {
log.info { "loadUserByUsername() 실행" }
val account: Account = accountRepository.findByEmail(email)
?: throw UsernameNotFoundException(ErrorCode.NOT_FOUND_ACCOUNT.message)

return CustomUserDetails(account)
}
}
122 changes: 122 additions & 0 deletions src/main/kotlin/org/store/clothstar/common/config/LoginFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.store.clothstar.common.config

import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.DisabledException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.store.clothstar.common.config.jwt.JwtUtil
import org.store.clothstar.common.dto.MessageDTO
import org.store.clothstar.member.domain.CustomUserDetails
import org.store.clothstar.member.dto.request.MemberLoginRequest
import java.io.IOException

class LoginFilter(
private val authenticationManager: AuthenticationManager,
private val jwtUtil: JwtUtil,
) : UsernamePasswordAuthenticationFilter() {
init {
setFilterProcessesUrl("/v1/members/login")
}

private val log = KotlinLogging.logger {}

/**
* 로그인 창에서 입력한 id, password를 받아서
* Authentication Manager에 던져 줘야 하는데 그 DTO역할을 하는 객체가 UsernamePasswordAuthenticationToken이다.
* Authentication Manager에 전달하면 최종적으로 Authentication에 전달 된다.
* return 하면 Authentication Manager에 던져진다.
*
*
* AuthenticationManager에 던지기 위해서 주입을 받아야 한다.
*/
@Throws(AuthenticationException::class)
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
log.info { "로그인 진행" }

try {
val memberLoginRequest = ObjectMapper().readValue(request.inputStream, MemberLoginRequest::class.java)
log.info { "login parameter memberLoginRequest: ${memberLoginRequest.toString()}" }

val email = memberLoginRequest.email
val password = memberLoginRequest.password

val authTokenDTO = UsernamePasswordAuthenticationToken(email, password, null)
return authenticationManager.authenticate(authTokenDTO)
} catch (e: IOException) {
throw RuntimeException(e)
}
}

@Throws(IOException::class, ServletException::class)
override fun successfulAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
authentication: Authentication,
) {
log.info { "로그인 성공" }
val customUserDetails = authentication.principal as CustomUserDetails

val account = customUserDetails.account
log.info { "account: ${account.toString()}" }

val accessToken = jwtUtil.createAccessToken(account)
log.info { "생성 accessToken: Bearer $accessToken " }

val refreshToken = jwtUtil.createRefreshToken(account)
log.info { "생성 refreshToken: Bearer $refreshToken" }

response.addHeader("Authorization", "Bearer $accessToken")
response.addCookie(jwtUtil.createCookie("refreshToken", refreshToken))
response.status = HttpStatus.OK.value()
response.characterEncoding = "UTF-8"
response.contentType = "application/json"

val messageDTO: MessageDTO = MessageDTO(
HttpServletResponse.SC_OK,
"로그인 성공 하였습니다."
)

response.writer.print(ObjectMapper().writeValueAsString(messageDTO))
}

@Throws(IOException::class, ServletException::class)
override fun unsuccessfulAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
failed: AuthenticationException,
) {
log.info { "로그인 실패" }

response.status = HttpServletResponse.SC_UNAUTHORIZED
response.characterEncoding = "UTF-8"
response.contentType = "application/json"

val messageDTO: MessageDTO = MessageDTO(
HttpServletResponse.SC_UNAUTHORIZED,
errorMessage(failed),
)

response.writer.print(ObjectMapper().writeValueAsString(messageDTO))
}

private fun errorMessage(failed: AuthenticationException): String? {
return if (failed is BadCredentialsException) {
"이메일 또는 비밀번호가 올바르지 않습니다. 다시 확인해주세요."
} else if (failed is DisabledException) {
"계정이 비활성화 되어있습니다. 이메일 인증을 완료해주세요"
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,95 @@
package org.store.clothstar.common.config

import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.annotation.web.configurers.*
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.store.clothstar.common.config.jwt.JwtAuthenticationFilter
import org.store.clothstar.common.config.jwt.JwtUtil

@EnableWebSecurity
@Configuration
class SecurityConfiguration(
private val authenticationConfiguration: AuthenticationConfiguration,
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
private val jwtUtil: JwtUtil,
) {
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}

) {
@Bean
@Throws(Exception::class)
fun authenticationManager(): AuthenticationManager {
return authenticationConfiguration.authenticationManager
}

@Bean
fun configure(): WebSecurityCustomizer {
return WebSecurityCustomizer { web ->
web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
}
}

@Bean
@Throws(java.lang.Exception::class)
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.cors { obj: CorsConfigurer<HttpSecurity> -> obj.disable() }
.csrf { obj: CsrfConfigurer<HttpSecurity> -> obj.disable() }
.httpBasic { obj: HttpBasicConfigurer<HttpSecurity> -> obj.disable() }
.formLogin { obj: FormLoginConfigurer<HttpSecurity> -> obj.disable() }

http.authorizeHttpRequests(
Customizer { auth ->
auth
.requestMatchers(
"/", "/login", "/userPage", "/sellerPage", "/adminPage", "/main",
"/v1/members/login", "/signup", "/v1/members/email/**", "/v1/access",
"/v1/categories/**", "/v1/products/**", "/v1/productLines/**", "/v2/productLines/**",
"/productLinePagingSlice", "/productLinePagingOffset",
"/v1/orderdetails", "/v1/orders", "membersPagingOffset", "membersPagingSlice",
"/v1/orderdetails", "/v1/orders", "/v2/orders", "/v3/orders", "/v1/orders/list",
"/v1/orders/list", "/ordersPagingOffset", "/ordersPagingSlice", "/v2/orders/list",
"/v1/seller/orders/**", "/v1/seller/orders", "/v1/orders/**", "/v1/orderdetails/**",
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v1/members/auth/**"
).permitAll()
.requestMatchers(HttpMethod.POST, "/v1/members").permitAll()
.requestMatchers(HttpMethod.POST, "/v1/sellers/**").authenticated()
.requestMatchers("/seller/**", "/v1/sellers/**").hasRole("SELLER")
.requestMatchers("/admin/**", "/v1/admin/**").hasRole("ADMIN")
.requestMatchers("v2/members", "v3/members").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/v1/members").hasRole("ADMIN")
.anyRequest().authenticated()
}
)

//JWT 토큰 인증 방식 사용하기에 session 유지 비활성화
http.sessionManagement { sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}

//UsernamePasswordAuthenticationFilter 대신에 LoginFilter가 실행된다.
//LoginFilter 이전에 jwtAhthenticationFilter가 실행된다.
http.addFilterBefore(jwtAuthenticationFilter, LoginFilter::class.java)
http.addFilterAt(
LoginFilter(authenticationManager(), jwtUtil),
UsernamePasswordAuthenticationFilter::class.java
)

return http.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ import java.util.function.Consumer
class GlobalExceptionHandler {
private val log = KotlinLogging.logger {}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundAccountException::class)
protected fun memberNotFoundException(ex: NotFoundAccountException): ResponseEntity<ErrorResponseDTO> {
log.error { "NotFoundAccountException : ${ex.message}" }
ex.fillInStackTrace()

val errorResponseDTO = ErrorResponseDTO(
HttpStatus.NOT_FOUND.value(),
ex.message!!
)

return ResponseEntity<ErrorResponseDTO>(errorResponseDTO, HttpStatus.BAD_REQUEST)
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundMemberException::class)
protected fun memberNotFoundException(ex: NotFoundMemberException): ResponseEntity<ErrorResponseDTO> {
Expand Down Expand Up @@ -47,6 +61,20 @@ class GlobalExceptionHandler {
return ResponseEntity(errorResponseDTO, HttpStatus.BAD_REQUEST)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(DuplicatedTelNoException::class)
protected fun duplicatedEmailException(ex: DuplicatedTelNoException): ResponseEntity<ErrorResponseDTO> {
log.error { "DuplicatedTelNoException : ${ex.message}" }
ex.fillInStackTrace()

val errorResponseDTO = ErrorResponseDTO(
HttpStatus.BAD_REQUEST.value(),
ex.message!!
)

return ResponseEntity(errorResponseDTO, HttpStatus.BAD_REQUEST)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(DuplicatedSellerException::class)
protected fun duplicatedSellerException(ex: DuplicatedSellerException): ResponseEntity<ErrorResponseDTO> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
Expand Down Expand Up @@ -43,7 +42,7 @@ class JwtAuthenticationFilter(
val accountId = jwtUtil.getAccountId(token)
log.info("refresh 토큰 memberId: {}", accountId)

val account = accountRepository.findByIdOrNull(accountId)
val account = accountRepository.findByAccountId(accountId)
?: throw IllegalStateException("해당 아이디를 찾을 수 없습니다.")

val customUserDetails = CustomUserDetails(account)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.store.clothstar.common.config.jwt

class JwtController(
private val jwtUtil: JwtUtil,
private val jwtService: JwtService,
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.store.clothstar.common.config.jwt

import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.store.clothstar.common.error.ErrorCode
import org.store.clothstar.common.error.exception.NotFoundMemberException
import org.store.clothstar.member.repository.AccountRepository
import org.store.clothstar.member.repository.MemberRepository
import java.util.*

class JwtService(
private val jwtUtil: JwtUtil,
private val memberRepository: MemberRepository,
private val accountRepository: AccountRepository,
) {
fun getRefreshToken(request: HttpServletRequest): String? {
if (request.cookies == null) {
return null
}

return Arrays.stream(request.cookies)
.filter { cookie: Cookie -> cookie.name == "refreshToken" }
.findFirst()
.map { cookie: Cookie -> cookie.value }
.orElse(null)
}

fun getAccessTokenByRefreshToken(refreshToken: String): String {
val accountId = jwtUtil.getAccountId(refreshToken)

val account = accountRepository.findByAccountId(accountId)
?: throw NotFoundMemberException(ErrorCode.NOT_FOUND_ACCOUNT)

return jwtUtil.createAccessToken(account)
}
}
Loading

0 comments on commit 8444413

Please sign in to comment.