Skip to content

Commit

Permalink
Merge pull request #8 from ClothingStoreService/feature/spring-securi…
Browse files Browse the repository at this point in the history
…ty-apply

Feature/spring security apply
  • Loading branch information
hjj4060 authored Aug 4, 2024
2 parents 03fc360 + a06a523 commit 7499767
Show file tree
Hide file tree
Showing 99 changed files with 4,051 additions and 77 deletions.
18 changes: 9 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,21 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-validation")
compileOnly("org.projectlombok:lombok")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
implementation("io.github.oshai:kotlin-logging-jvm:5.1.4")

//web
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")

//security
// implementation("org.springframework.boot:spring-boot-starter-security")
// implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
// implementation("io.jsonwebtoken:jjwt-api:0.12.3")
// runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
// runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
// testImplementation("org.springframework.boot:spring-boot-starter-test")
// testImplementation("org.springframework.security:spring-security-test")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")

//DB
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/org/store/clothstar/ClothstarApplication.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.store.clothstar

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@ConfigurationPropertiesScan
@SpringBootApplication
@EnableJpaAuditing //Jpa Auditing 기능 활성화
class ClothstarApplication {
Expand Down
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)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.store.clothstar.common
package org.store.clothstar.common.config

import org.jasypt.encryption.StringEncryptor
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor
Expand Down
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
@@ -0,0 +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()
}
}
55 changes: 55 additions & 0 deletions src/main/kotlin/org/store/clothstar/common/config/SwaggerConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.store.clothstar.common.config

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springdoc.core.models.GroupedOpenApi
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class SwaggerConfig {
private val BEARER_TOKEN = "Bearer Token"
private val BEARER = "Bearer"
private val AUTHORIZATION = "Authorization"
private val JWT = "JWT"

@Bean
fun groupedAllOpenApi(): GroupedOpenApi {
return GroupedOpenApi.builder()
.group("All")
.pathsToMatch("/v1/**", "/v2/**", "/v3/**")
.build()
}

@Bean
fun groupedMemberOpenApi(): GroupedOpenApi {
return GroupedOpenApi.builder()
.group("Member")
.pathsToMatch("/v1/members/**", "/v1/sellers/**")
.build()
}

@Bean
fun springShopOpenAPI(): OpenAPI {
val info = Info().title("clothstar-v3 Project API")
.description("의류 쇼핑몰 3차 프로젝트 입니다.")
.version("v0.3")

val apiKey = SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.`in`(SecurityScheme.In.HEADER)
.name(AUTHORIZATION)
.scheme(BEARER)
.bearerFormat(JWT)

val securityRequirement = SecurityRequirement()
.addList(BEARER_TOKEN)

return OpenAPI().info(info)
.components(Components().addSecuritySchemes(BEARER_TOKEN, apiKey))
.addSecurityItem(securityRequirement)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.store.clothstar.common.config.exception

class BaseException(
private val exceptionType: ExceptionType
) : RuntimeException(exceptionType.message())
Loading

0 comments on commit 7499767

Please sign in to comment.