Merge pull request #18 from beaniejoy/feature/17
Spring Security를 사용한 인증 프로세스 적용
This commit is contained in:
79
build.gradle
79
build.gradle
@@ -1,79 +0,0 @@
|
||||
buildscript {
|
||||
ext {
|
||||
springBootVersion = '2.7.0'
|
||||
dependencyManagementVersion = '1.0.11.RELEASE'
|
||||
kotlinVersion = '1.6.21'
|
||||
flywayVersion = '7.15.0'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
|
||||
classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementVersion}"
|
||||
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
||||
classpath "org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}"
|
||||
classpath "org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-spring'
|
||||
apply plugin: "kotlin-jpa"
|
||||
apply plugin: "kotlin-noarg"
|
||||
apply plugin: "kotlin-allopen"
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
group = 'io.beaniejoy.dongecafe'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
sourceCompatibility = '17'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
allOpen {
|
||||
annotation("javax.persistence.Entity")
|
||||
annotation("javax.persistence.MappedSuperclass")
|
||||
annotation("javax.persistence.Embeddable")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
|
||||
implementation('io.github.microutils:kotlin-logging:2.1.21')
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
|
||||
// log4j2
|
||||
// implementation 'org.springframework.boot:spring-boot-starter-log4j2'
|
||||
// testImplementation 'org.springframework.boot:spring-boot-starter-log4j2'
|
||||
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
|
||||
runtimeOnly 'mysql:mysql-connector-java' // MySQL
|
||||
runtimeOnly 'com.h2database:h2' // H2
|
||||
|
||||
implementation "org.flywaydb:flyway-core:${flywayVersion}" // flyway
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
|
||||
//configurations {
|
||||
// all {
|
||||
// // log4j2 적용을 위해 기존 spring boot에서 제공하는 logging exclude
|
||||
// exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
|
||||
// }
|
||||
//}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
89
build.gradle.kts
Normal file
89
build.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "2.7.0"
|
||||
id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||
kotlin("jvm") version "1.6.21"
|
||||
kotlin("plugin.spring") version "1.6.21" apply false // TODO: apply false what?
|
||||
kotlin("plugin.jpa") version "1.6.21" apply false
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
allprojects {
|
||||
group = "io.beaniejoy.dongecafe"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
|
||||
apply(plugin = "io.spring.dependency-management")
|
||||
apply(plugin = "org.springframework.boot")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
|
||||
|
||||
apply(plugin = "kotlin")
|
||||
apply(plugin = "kotlin-spring")
|
||||
apply(plugin = "kotlin-jpa")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// configurations {
|
||||
// all {
|
||||
// // log4j2 적용을 위해 기존 spring boot에서 제공하는 logging exclude
|
||||
// exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
|
||||
// }
|
||||
// }
|
||||
|
||||
dependencies {
|
||||
// Spring Boot Project
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
|
||||
//kotlin
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
// DB
|
||||
runtimeOnly("mysql:mysql-connector-java") // MySQL
|
||||
runtimeOnly("com.h2database:h2") // H2
|
||||
implementation("org.flywaydb:flyway-core:7.15.0") // flyway
|
||||
|
||||
// Logging
|
||||
// log4j2
|
||||
// implementation("org.springframework.boot:spring-boot-starter-log4j2")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-log4j2")
|
||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
||||
|
||||
// Test
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict")
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
3
dongne-account-api/build.gradle.kts
Normal file
3
dongne-account-api/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":dongne-common"))
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class DongneCafeApiApplication
|
||||
class DongneAccountApiApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<DongneCafeApiApplication>(*args)
|
||||
runApplication<DongneAccountApiApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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.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("/test").hasRole("USER") // 임시 인가 테스트용
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web ->
|
||||
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
|
||||
web.ignoring().antMatchers("/error")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.beaniejoy.dongnecafe.common.entity
|
||||
|
||||
import org.springframework.data.domain.AuditorAware
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.*
|
||||
|
||||
@Component
|
||||
class BaseEntityAuditorAware: AuditorAware<String> {
|
||||
override fun getCurrentAuditor(): Optional<String> {
|
||||
return Optional.of("system")
|
||||
}
|
||||
}
|
||||
@@ -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 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.beaniejoy.dongnecafe.common.security
|
||||
|
||||
import mu.KLogging
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
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
|
||||
|
||||
/**
|
||||
* 실제 인증 절차 수행
|
||||
* @property userDetailsService email로 계정 찾기
|
||||
*/
|
||||
@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)
|
||||
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)
|
||||
}
|
||||
|
||||
override fun supports(authentication: Class<*>): Boolean {
|
||||
return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.beaniejoy.dongnecafe.common.security.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
data class AuthenticationResult(
|
||||
val email: String,
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
val authorities: Collection<GrantedAuthority> = listOf(),
|
||||
val msg: String
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.beaniejoy.dongnecafe.controller
|
||||
|
||||
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 org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
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)
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.beaniejoy.dongnecafe.controller
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.service.MemberService
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth/members")
|
||||
class MemberController(
|
||||
private val memberService: MemberService
|
||||
) {
|
||||
@PostMapping("/sign-up")
|
||||
fun signUp(@RequestBody resource: MemberRegisterRequest): Long {
|
||||
return memberService.registerMember(resource)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.beaniejoy.dongnecafe.controller
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class TestController {
|
||||
|
||||
@GetMapping("/test")
|
||||
fun test(): String {
|
||||
return "authorize OK!"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.beaniejoy.dongnecafe.error
|
||||
|
||||
class MemberExistedException(email: String): RuntimeException("Member[$email] is already existed")
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.beaniejoy.dongnecafe.error
|
||||
|
||||
class MemberDeactivatedException(email: String): RuntimeException("Member[$email] is deactivated")
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.beaniejoy.dongnecafe.service
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.member.entity.Member
|
||||
import io.beaniejoy.dongnecafe.domain.member.model.request.MemberRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
|
||||
import io.beaniejoy.dongnecafe.error.MemberExistedException
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class MemberService(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val passwordEncoder: PasswordEncoder
|
||||
) {
|
||||
fun registerMember(resource: MemberRegisterRequest): Long {
|
||||
memberRepository.findByEmail(resource.email!!)?.also {
|
||||
throw MemberExistedException(resource.email!!)
|
||||
}
|
||||
|
||||
val registeredMember = memberRepository.save(
|
||||
Member.createMember(
|
||||
email = resource.email!!,
|
||||
password = passwordEncoder.encode(resource.password!!),
|
||||
address = resource.address!!,
|
||||
phoneNumber = resource.phoneNumber!!
|
||||
)
|
||||
)
|
||||
|
||||
return registeredMember.id
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
27
dongne-account-api/src/main/resources/application.yml
Normal file
27
dongne-account-api/src/main/resources/application.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: local
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none # use [service-api] flyway migration
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||
format_sql: true
|
||||
show-sql: false
|
||||
open-in-view:
|
||||
flyway:
|
||||
enabled: false
|
||||
devtools:
|
||||
livereload:
|
||||
enabled: false # no use devtools' LiveReload Server
|
||||
|
||||
server:
|
||||
port: 9090
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: debug # logger 통해 로깅
|
||||
# org.hibernate.type: trace
|
||||
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
internal class DongneCafeSirenOrderApplicationTests {
|
||||
internal class DongneAccountApiApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
19
dongne-common/build.gradle.kts
Normal file
19
dongne-common/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||
|
||||
val jar: Jar by tasks
|
||||
val bootJar: BootJar by tasks
|
||||
|
||||
bootJar.enabled = false
|
||||
jar.enabled = true
|
||||
|
||||
allOpen {
|
||||
annotation("javax.persistence.Entity")
|
||||
annotation("javax.persistence.Embeddable")
|
||||
annotation("javax.persistence.MappedSuperclass")
|
||||
}
|
||||
|
||||
noArg {
|
||||
annotation("javax.persistence.Entity")
|
||||
annotation("javax.persistence.Embeddable")
|
||||
annotation("javax.persistence.MappedSuperclass")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.beaniejoy.dongnecafe.common.entity
|
||||
package io.beaniejoy.dongnecafe.common
|
||||
|
||||
import org.springframework.data.annotation.CreatedBy
|
||||
import org.springframework.data.annotation.CreatedDate
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
|
||||
import javax.persistence.*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest
|
||||
import java.math.BigDecimal
|
||||
import javax.persistence.*
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest
|
||||
import javax.persistence.*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.entity.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import java.math.BigDecimal
|
||||
import javax.persistence.*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.domain.member.constant
|
||||
|
||||
enum class RoleType(val value: String) {
|
||||
ROLE_ADMIN("어드민 관리자"),
|
||||
ROLE_USER("일반 사용자"),
|
||||
ROLE_OWNER("카페 관리자");
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.beaniejoy.dongnecafe.domain.member.entity
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.BaseTimeEntity
|
||||
import io.beaniejoy.dongnecafe.domain.member.constant.RoleType
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "member")
|
||||
class Member(
|
||||
email: String,
|
||||
password: String,
|
||||
address: String,
|
||||
phoneNumber: String
|
||||
): BaseTimeEntity() {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "member_id", nullable = false)
|
||||
val id: Long = 0L
|
||||
|
||||
@Column(name = "email", nullable = false)
|
||||
var email: String = email
|
||||
protected set
|
||||
|
||||
@Column(name = "password", nullable = false)
|
||||
var password: String = password
|
||||
protected set
|
||||
|
||||
@Column(name = "address", nullable = false)
|
||||
var address: String = address
|
||||
protected set
|
||||
|
||||
@Column(name = "phone_number", nullable = false)
|
||||
var phoneNumber: String = phoneNumber
|
||||
protected set
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "role_type", nullable = false)
|
||||
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,
|
||||
password: String,
|
||||
address: String,
|
||||
phoneNumber: String
|
||||
): Member {
|
||||
return Member(
|
||||
email = email,
|
||||
password = password,
|
||||
address = address,
|
||||
phoneNumber = phoneNumber
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.domain.member.model.request
|
||||
|
||||
class MemberRegisterRequest(
|
||||
val email: String? = null,
|
||||
val password: String? = null,
|
||||
val address: String? = null,
|
||||
val phoneNumber: String? = null
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.domain.member.model.request
|
||||
|
||||
data class SignInRequest(
|
||||
val email: String,
|
||||
val password: String
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.domain.member.repository
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.member.entity.Member
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface MemberRepository : JpaRepository<Member, Long> {
|
||||
fun findByEmail(email: String): Member?
|
||||
}
|
||||
3
dongne-service-api/build.gradle.kts
Normal file
3
dongne-service-api/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies {
|
||||
implementation(project(":dongne-common"))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.beaniejoy.dongnecafe
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class DongneServiceApiApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<DongneServiceApiApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
class AuditingConfig
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.beaniejoy.dongnecafe.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.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.web.SecurityFilterChain
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http
|
||||
.csrf().disable()
|
||||
.formLogin().disable()
|
||||
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated() // 임시 허용
|
||||
|
||||
.and()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web ->
|
||||
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
|
||||
web.ignoring().antMatchers("/error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import org.springframework.data.web.PageableDefault
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/cafes")
|
||||
@RequestMapping("/api/cafes")
|
||||
class CafeController(
|
||||
private val cafeService: CafeService
|
||||
) {
|
||||
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/cafes/{cafeId}/menus")
|
||||
@RequestMapping("/api/cafes/{cafeId}/menus")
|
||||
class CafeMenuController(
|
||||
private val cafeMenuService: CafeMenuService
|
||||
) {
|
||||
@@ -0,0 +1,5 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/dongne?autoreconnect=true&characterEncoding=utf8&serverTimezone=Asia/Seoul
|
||||
username: root
|
||||
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
|
||||
@@ -13,8 +13,11 @@ spring:
|
||||
show-sql: false
|
||||
flyway:
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
baseline-version: "000"
|
||||
locations: classpath:db/migration,classpath:db/seed
|
||||
devtools:
|
||||
livereload:
|
||||
enabled: false # no use devtools' LiveReload Server
|
||||
|
||||
logging:
|
||||
level:
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE `member` (
|
||||
`member_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '회원 ID',
|
||||
`email` varchar(20) NOT NULL COMMENT '계정 이메일',
|
||||
`password` varchar(255) NOT NULL COMMENT '비밀번호',
|
||||
`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 '회원 변경날짜',
|
||||
`updated_by` varchar(20) NULL COMMENT '회원 변경자',
|
||||
PRIMARY KEY (`member_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.beaniejoy.dongnecafe
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
internal class DongneServiceApiApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.service
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.*
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionUpdateRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailUpdateRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
@@ -98,7 +98,7 @@ internal class CafeServiceTest {
|
||||
|
||||
// TODO 'findByIdOrNull'은 kotlin test 라이브러리 필요한 듯
|
||||
val mockCafe = mock(Cafe::class.java)
|
||||
doReturn(Optional.of(cafe)).`when`(mockCafeRepository.findById(eq(cafeId)))
|
||||
`when`(mockCafeRepository.findById(cafeId)).thenReturn(Optional.of(mockCafe))
|
||||
|
||||
doNothing().`when`(mockCafe).updateInfo(
|
||||
name = anyString(),
|
||||
@@ -109,7 +109,7 @@ internal class CafeServiceTest {
|
||||
|
||||
// when
|
||||
mockCafeService.updateInfo(
|
||||
id = eq(cafeId),
|
||||
id = cafeId,
|
||||
name = "updated_name",
|
||||
address = "updated_address",
|
||||
phoneNumber = "updated_phoneNumber",
|
||||
@@ -1,15 +1,8 @@
|
||||
package io.beaniejoy.dongnecafe.domain.cafe.utils
|
||||
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailRegisterRequest
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.entity.CafeMenu
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.entity.MenuOption
|
||||
import io.beaniejoy.dongnecafe.domain.cafe.entity.OptionDetail
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import java.math.BigDecimal
|
||||
import javax.persistence.GeneratedValue
|
||||
|
||||
class CafeTestUtils {
|
||||
@@ -1 +0,0 @@
|
||||
rootProject.name = 'dongne-cafe-api'
|
||||
4
settings.gradle.kts
Normal file
4
settings.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
rootProject.name = "dongne-cafe-api"
|
||||
include("dongne-common")
|
||||
include("dongne-service-api")
|
||||
include("dongne-account-api")
|
||||
Reference in New Issue
Block a user