@@ -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}")
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.beaniejoy.dongnecafe.model
|
||||
|
||||
data class TokenResponse(
|
||||
val accessToken: String
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user