[#17] feat: 인증 절차 개발

- Spring Security 스펙 활용한 인증 절차 개발(Custom Filter 적용중)
- UserDetailsService custom 구현체 적용
- Member Entity activated(활성화 여부) 칼럼 및 필드 추가
This commit is contained in:
beaniejoy
2022-10-29 11:49:20 +09:00
parent 30b50c0254
commit e6aafebb53
9 changed files with 160 additions and 9 deletions

View File

@@ -1,12 +1,16 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.common.security.ApiAuthenticationFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
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.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
@Configuration
@EnableWebSecurity
@@ -19,9 +23,22 @@ class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.cors().disable()
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.antMatchers("/auth/members/sign-up").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
.build()
}
@Bean
fun apiAuthenticationFilter(): ApiAuthenticationFilter {
return ApiAuthenticationFilter(
AntPathRequestMatcher("/auth/authenticate", HttpMethod.POST.name)
)
}
}

View File

@@ -0,0 +1,5 @@
package io.beaniejoy.dongnecafe.common.entity
class SecurityUser(
)

View File

@@ -0,0 +1,53 @@
package io.beaniejoy.dongnecafe.common.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
import mu.KotlinLogging
import org.springframework.http.HttpMethod
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 log = KotlinLogging.logger {}
private val objectMapper = jacksonObjectMapper()
override fun attemptAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
): Authentication {
if (isPostMethod(request).not()) {
val errorMsg = "Authentication is not supported (only support for POST method)"
log.error { errorMsg }
throw IllegalStateException(errorMsg)
}
val signInRequest = objectMapper.readValue(request.reader, SignInRequest::class.java)
val token = signInRequest.let {
if (StringUtils.hasText(it.email).not() || StringUtils.hasText(it.password).not()) {
log.error { "Email(${it.email}) & Password are not empty" }
throw IllegalArgumentException("Email & Password are not empty!!")
}
UsernamePasswordAuthenticationToken(it.email, it.password)
}
return authenticationManager.authenticate(token)
}
private fun isPostMethod(request: HttpServletRequest): Boolean {
if (request.method != HttpMethod.POST.name) {
return false
}
return true
}
}

View File

@@ -0,0 +1,31 @@
package io.beaniejoy.dongnecafe.common.security
import mu.KLogging
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component
@Component
class ApiAuthenticationProvider(
private val userDetailsService: UserDetailsService,
private val passwordEncoder: PasswordEncoder
) : AuthenticationProvider {
companion object: KLogging()
override fun authenticate(authentication: Authentication): Authentication {
logger.info { "start authentication" }
val email = authentication.name
val password = authentication.credentials as String?
val user = userDetailsService.loadUserByUsername(email)
TODO("Not yet implemented")
}
override fun supports(authentication: Class<*>): Boolean {
return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
}
}

View File

@@ -13,12 +13,11 @@ import org.springframework.web.bind.annotation.RestController
class AuthController(
private val authenticationManagerBuilder: AuthenticationManagerBuilder
) {
@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) {
// val authenticationToken =
// UsernamePasswordAuthenticationToken(signInRequest.email, signInRequest.password)
//
// val authenticate = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
// }
}

View File

@@ -0,0 +1,3 @@
package io.beaniejoy.dongnecafe.error
class MemberDeactivatedException(email: String): RuntimeException("Member[$email] is deactivated")

View File

@@ -0,0 +1,38 @@
package io.beaniejoy.dongnecafe.service
import io.beaniejoy.dongnecafe.domain.member.entity.Member
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
import io.beaniejoy.dongnecafe.error.MemberDeactivatedException
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
@Component("userDetailsService")
class UserDetailsServiceImpl(
private val memberRepository: MemberRepository
) : UserDetailsService {
companion object: KLogging()
override fun loadUserByUsername(email: String): UserDetails {
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 {
if (member.activated.not()) {
throw MemberDeactivatedException(member.email)
}
return User(
/* username = */ member.email,
/* password = */ member.password,
/* authorities = */ listOf(SimpleGrantedAuthority(member.roleType.name))
)
}
}

View File

@@ -38,6 +38,10 @@ class Member(
var roleType: RoleType = RoleType.ROLE_USER
protected set
@Column(name = "activated", nullable = false)
var activated: Boolean = true
protected set
companion object {
fun createMember(
email: String,

View File

@@ -5,6 +5,7 @@ CREATE TABLE `member` (
`address` varchar(100) NOT NULL COMMENT '회원 주소',
`phone_number` varchar(11) NOT NULL COMMENT '회원 전화번호',
`role_type` varchar(20) COMMENT '회원 권한',
`activated` tinyint(1) NOT NULL COMMENT '계정 활성화 여부',
`created_at` datetime NOT NULL COMMENT '회원 등록날짜',
`created_by` varchar(20) NOT NULL COMMENT '회원 등록자',
`updated_at` datetime NULL COMMENT '회원 변경날짜',