Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/spring security apply #3

Merged
merged 8 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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