Merge pull request #23 from beaniejoy/feature/20

JWT를 이용한 인증프로세스로 전환
This commit is contained in:
Hanbin Lee
2022-11-20 17:32:46 +09:00
committed by GitHub
16 changed files with 122 additions and 192 deletions

View File

@@ -52,6 +52,11 @@ subprojects {
runtimeOnly("com.h2database:h2") // H2
implementation("org.flywaydb:flyway-core:${Version.Deps.flywayCore}") // flyway
// JWT
implementation("io.jsonwebtoken:jjwt-api:${Version.Deps.Jwt}")
runtimeOnly("io.jsonwebtoken:jjwt-impl:${Version.Deps.Jwt}")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:${Version.Deps.Jwt}")
// Logging
implementation("io.github.microutils:kotlin-logging:${Version.Deps.kotlinLogging}")

View File

@@ -10,5 +10,6 @@ object Version {
object Deps {
const val flywayCore = "7.15.0"
const val kotlinLogging = "2.1.21"
const val Jwt = "0.11.5"
}
}

View File

@@ -1,49 +1,35 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.common.security.ApiAuthenticationFilter
import org.springframework.beans.factory.annotation.Autowired
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.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.core.userdetails.UserDetailsService
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Autowired
lateinit var authenticationConfiguration: AuthenticationConfiguration
@Autowired
lateinit var apiAuthenticationSuccessHandler: AuthenticationSuccessHandler
@Autowired
lateinit var apiAuthenticationFailureHandler: AuthenticationFailureHandler
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.antMatchers("/auth/members/sign-up").permitAll()
.antMatchers("/auth/authenticate").permitAll()
.antMatchers("/test").hasRole("USER") // 임시 인가 테스트용
.anyRequest().authenticated()
.and()
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 방식(세션 불필요)
.and()
.build()
}
@@ -59,15 +45,4 @@ class SecurityConfig {
fun passwordEncoder(): PasswordEncoder {
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
@Bean
fun apiAuthenticationFilter(): ApiAuthenticationFilter {
return ApiAuthenticationFilter(
AntPathRequestMatcher("/auth/authenticate", HttpMethod.POST.name)
).apply {
this.setAuthenticationManager(authenticationConfiguration.authenticationManager)
this.setAuthenticationSuccessHandler(apiAuthenticationSuccessHandler)
this.setAuthenticationFailureHandler(apiAuthenticationFailureHandler)
}
}
}

View File

@@ -1,53 +0,0 @@
package io.beaniejoy.dongnecafe.common.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.util.StringUtils
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
AbstractAuthenticationProcessingFilter(requestMatcher) {
private val objectMapper = jacksonObjectMapper()
override fun attemptAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
): Authentication {
if (isValidRequest(request).not()) {
throw IllegalStateException("request is not supported. check request method and content-type")
}
val signInRequest = objectMapper.readValue(request.reader, SignInRequest::class.java)
request.setAttribute("email", signInRequest.email)
val token = signInRequest.let {
if (StringUtils.hasText(it.email).not() || StringUtils.hasText(it.password).not()) {
throw IllegalArgumentException("Email & Password are not empty!!")
}
UsernamePasswordAuthenticationToken(it.email, it.password)
}
return authenticationManager.authenticate(token)
}
private fun isValidRequest(request: HttpServletRequest): Boolean {
if (request.method != HttpMethod.POST.name) {
return false
}
if (request.contentType != MediaType.APPLICATION_JSON_VALUE) {
return false
}
return true
}
}

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.common.security
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.BadCredentialsException
@@ -26,14 +27,13 @@ class ApiAuthenticationProvider(
val email = authentication.name
val password = authentication.credentials as String?
val user = userDetailsService.loadUserByUsername(email)
val user = userDetailsService.loadUserByUsername(email) as SecurityUser
if (!passwordEncoder.matches(password, user.password)) {
throw BadCredentialsException("Input password does not match stored password")
}
logger.info { "User password ${user.password}" }
// password null로 반환
return UsernamePasswordAuthenticationToken(user, null, user.authorities)
}

View File

@@ -1,15 +1,15 @@
package io.beaniejoy.dongnecafe.service
package io.beaniejoy.dongnecafe.common.security
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberDeactivatedException
import io.beaniejoy.dongnecafe.security.SecurityUser
import mu.KLogging
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
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.Component
import org.springframework.transaction.annotation.Transactional
@Component("userDetailsService")
class UserDetailsServiceImpl(
@@ -17,22 +17,22 @@ class UserDetailsServiceImpl(
) : UserDetailsService {
companion object: KLogging()
override fun loadUserByUsername(email: String): UserDetails {
@Transactional(readOnly = true)
override fun loadUserByUsername(email: String): SecurityUser {
return memberRepository.findByEmail(email)?.let {
logger.info { "[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}" }
createSecurityUser(it)
} ?: throw UsernameNotFoundException(email)
}
private fun createSecurityUser(member: Member): User {
private fun createSecurityUser(member: Member): SecurityUser {
if (member.activated.not()) {
throw MemberDeactivatedException(member.email)
}
return User(
/* username = */ member.email,
/* password = */ member.password,
/* authorities = */ listOf(SimpleGrantedAuthority(member.roleType.name))
return SecurityUser(
member = member,
authorities = listOf(SimpleGrantedAuthority(member.roleType.name))
)
}
}

View File

@@ -1,42 +0,0 @@
package io.beaniejoy.dongnecafe.common.security.handler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class ApiAuthenticationFailureHandler : AuthenticationFailureHandler {
private val objectMapper = jacksonObjectMapper()
companion object : KLogging()
override fun onAuthenticationFailure(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException,
) {
val email = request.getAttribute("email") as String
logger.error { "[AUTH FAILED] $email" }
response.apply {
this.status = HttpStatus.UNAUTHORIZED.value()
this.contentType = MediaType.APPLICATION_JSON_VALUE
}
objectMapper.writeValue(
response.writer,
AuthenticationResult(
email = email,
msg = "authentication failed"
)
)
}
}

View File

@@ -1,43 +0,0 @@
package io.beaniejoy.dongnecafe.common.security.handler
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.stereotype.Component
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class ApiAuthenticationSuccessHandler :AuthenticationSuccessHandler {
private val objectMapper = jacksonObjectMapper()
companion object: KLogging()
override fun onAuthenticationSuccess(
request: HttpServletRequest,
response: HttpServletResponse,
authentication: Authentication,
) {
val user = authentication.principal as User
logger.info { "[AUTH SUCCESS] email: ${user.username}, authorities: ${user.authorities}" }
response.apply {
this.status = HttpStatus.OK.value()
this.contentType = MediaType.APPLICATION_JSON_VALUE
}
objectMapper.writeValue(
response.writer,
AuthenticationResult(
email = user.username,
authorities = user.authorities,
msg = "authentication success"
)
)
}
}

View File

@@ -1,8 +1,9 @@
package io.beaniejoy.dongnecafe.controller
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import io.beaniejoy.dongnecafe.model.TokenResponse
import io.beaniejoy.dongnecafe.service.AuthService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
@@ -11,13 +12,18 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/auth")
class AuthController(
private val authenticationManagerBuilder: AuthenticationManagerBuilder
private val authService: AuthService,
private val jwtTokenUtils: JwtTokenUtils
) {
// @PostMapping("/authenticate")
// fun signIn(@RequestBody signInRequest: SignInRequest) {
// val authenticationToken =
// UsernamePasswordAuthenticationToken(signInRequest.email, signInRequest.password)
//
// val authenticate = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
// }
@PostMapping("/authenticate")
fun signIn(@RequestBody signInRequest: SignInRequest): TokenResponse {
val authentication = authService.signIn(
email = signInRequest.email,
password = signInRequest.password
)
val accessToken = jwtTokenUtils.createToken(authentication)
return TokenResponse(accessToken)
}
}

View File

@@ -0,0 +1,5 @@
package io.beaniejoy.dongnecafe.model
data class TokenResponse(
val accessToken: String
)

View File

@@ -0,0 +1,18 @@
package io.beaniejoy.dongnecafe.service
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Service
@Service
class AuthService(authenticationConfiguration: AuthenticationConfiguration) {
private val authenticationManager: AuthenticationManager = authenticationConfiguration.authenticationManager
fun signIn(email: String, password: String): Authentication {
val authenticationToken = UsernamePasswordAuthenticationToken(email, password)
return authenticationManager.authenticate(authenticationToken)
}
}

View File

@@ -3,3 +3,6 @@ spring:
url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
username: root
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
jwt:
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
expiration_time: 86400

View File

@@ -0,0 +1,38 @@
package io.beaniejoy.dongnecafe.security
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import mu.KLogging
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import java.security.Key
import java.util.*
@Component
class JwtTokenUtils(
@Value("\${jwt.secret_key}")
private val secretKey: String,
@Value("\${jwt.expiration_time}")
private val expirationTime: Long
) {
private val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray())
companion object: KLogging()
fun createToken(authentication: Authentication): String {
val authenticatedMember = (authentication.principal as SecurityUser).member
val nowTime = Date().time
val expirationDate = Date(nowTime + this.expirationTime)
return Jwts.builder()
.setSubject(authenticatedMember.email)
.claim("memberId", authenticatedMember.id)
.claim("email", authenticatedMember.email)
.claim("roles", authentication.authorities.joinToString(",") { it.authority })
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(expirationDate)
.compact()
}
}

View File

@@ -0,0 +1,10 @@
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User
class SecurityUser(
val member: Member,
authorities: Collection<GrantedAuthority>
) : User(member.email, member.password, authorities)

View File

@@ -3,3 +3,6 @@ spring:
url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
username: root
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
jwt:
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
expiration_time: 86400

View File

@@ -10,4 +10,8 @@ spring:
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace # 실제 sql 쿼리의 parameter 값을 확인하고자 함
org.hibernate.type: trace # 실제 sql 쿼리의 parameter 값을 확인하고자 함
jwt:
secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo
expiration_time: 60