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

🔀 :: Security 및 Jwt 세팅 #14

Merged
merged 35 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2b1748
:fire: .gitkeep 파일 삭제
uuuuuuuk Mar 6, 2024
ef5a6df
:memo: ErrorCode 에 TOKEN_TYPE_NOT_VALID enum 추가
uuuuuuuk Mar 6, 2024
74b0bff
:memo: PropertiesScanConfig 에 JwtProperties, JwtTimeProperties 를 base…
uuuuuuuk Mar 6, 2024
fccd142
:memo: AdminDetail 구현
uuuuuuuk Mar 6, 2024
091d787
:memo: AdminDetailService 구현
uuuuuuuk Mar 6, 2024
8763fea
:memo: CustomAccessDeniedHandler 구현
uuuuuuuk Mar 6, 2024
11cb133
:memo: CustomAuthenticationEntryPointHandler 구현
uuuuuuuk Mar 6, 2024
4f3fdc5
:memo: InvalidTokenTypeException 구현
uuuuuuuk Mar 6, 2024
984b282
:memo: JwtExceptionFilter 구현
uuuuuuuk Mar 6, 2024
37b827f
:memo: JwtProperties 구현
uuuuuuuk Mar 6, 2024
7811b59
:memo: JwtRequestFilter 구현
uuuuuuuk Mar 6, 2024
888638e
:memo: JwtTimeProperties 구현
uuuuuuuk Mar 6, 2024
44457f0
:memo: PasswordEncodeAdapter 구현
uuuuuuuk Mar 6, 2024
fe2e0d6
:memo: PasswordEncodePort 구현
uuuuuuuk Mar 6, 2024
2fe1940
:memo: QueryUserPersistenceAdapter 구현
uuuuuuuk Mar 6, 2024
a5105ed
:memo: QueryUserPort 구현
uuuuuuuk Mar 6, 2024
3b67e73
:memo: Role 구현
uuuuuuuk Mar 6, 2024
31017f8
:memo: SecurityConfig 구현
uuuuuuuk Mar 6, 2024
1adc9bb
:memo: TokenDto 구현
uuuuuuuk Mar 6, 2024
75474a3
:memo: TokenExpiredException 구현
uuuuuuuk Mar 6, 2024
9bde1a4
:memo: TokenGenerateAdapter 구현
uuuuuuuk Mar 6, 2024
71322a6
:memo: TokenGeneratePort 구현
uuuuuuuk Mar 6, 2024
f4c8f09
:memo: TokenInvalidException 구현
uuuuuuuk Mar 6, 2024
e4b423c
:memo: TokenParseAdapter 구현
uuuuuuuk Mar 6, 2024
0102f03
:memo: TokenParsePort 구현
uuuuuuuk Mar 6, 2024
4421205
:memo: TokenParsePort 구현
uuuuuuuk Mar 6, 2024
0d02a9e
:memo: User 구현
uuuuuuuk Mar 6, 2024
817c510
:memo: UserDetail 구현
uuuuuuuk Mar 6, 2024
142aff0
:memo: UserDetailService 구현
uuuuuuuk Mar 6, 2024
54674e3
:memo: UserEntity 구현
uuuuuuuk Mar 6, 2024
188b9e3
:memo: UserMapper 구현
uuuuuuuk Mar 6, 2024
0a9f0f3
:memo: UserNotFoundException 구현
uuuuuuuk Mar 6, 2024
294c20c
:memo: UserRepository 구현
uuuuuuuk Mar 6, 2024
1ecd6fa
:memo: UserSecurityAdapter 구현
uuuuuuuk Mar 6, 2024
ea0af53
:memo: UserSecurityPort 구현
uuuuuuuk Mar 6, 2024
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
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.domain.auth.application.port.output

interface PasswordEncodePort {
fun passwordEncode(password: String): String

fun isPasswordMatch(password: String, passwordCheck: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package andreas311.miso.domain.auth.application.port.output

import andreas311.miso.domain.auth.application.port.output.dto.TokenDto
import andreas311.miso.domain.user.domain.Role

interface TokenGeneratePort {
fun generateToken(email: String, role: Role): TokenDto
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package andreas311.miso.domain.auth.application.port.output

import org.springframework.security.core.Authentication
import javax.servlet.http.HttpServletRequest

interface TokenParsePort {
fun parseAccessToken(request: HttpServletRequest): String?

fun parseRefreshTokenToken(refreshToken: String): String?

fun authentication(token: String): Authentication
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.domain.auth.application.port.output

import andreas311.miso.domain.user.domain.User

interface UserSecurityPort {
fun currentUser(): User
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package andreas311.miso.domain.auth.application.port.output.dto

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

data class TokenDto(
val accessToken: String,
val refreshToken: String,
val accessExp: Long,
val refreshExp: Long
) {
override fun toString(): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
return "{" +
"\"accessToken\":" + "\"" + this.accessToken + "\"," +
"\"refreshToken\":" + "\"" + this.refreshToken + "\"," +
"\"accessTokenExpiredAt\":" + "\"" + LocalDateTime.now().plusSeconds(this.accessExp).format(formatter) + "\"," +
"\"refreshTokenExpiredAt\":" + "\"" + LocalDateTime.now().plusSeconds(this.refreshExp).format(formatter) + "\"}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package andreas311.miso.domain.user.adapter.output.persistence

import andreas311.miso.domain.user.adapter.output.persistence.mapper.UserMapper
import andreas311.miso.domain.user.adapter.output.persistence.repository.UserRepository
import andreas311.miso.domain.user.application.port.output.QueryUserPort
import andreas311.miso.domain.user.domain.User
import org.springframework.stereotype.Component

@Component
class QueryUserPersistenceAdapter(
private val userRepository: UserRepository,
private val userMapper: UserMapper
): QueryUserPort {
override fun findByEmailOrNull(email: String): User? {
val userEntity = userRepository.findByEmail(email)
return userMapper.toDomain(userEntity)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package andreas311.miso.domain.user.adapter.output.persistence.entity

import andreas311.miso.domain.user.domain.Role
import org.hibernate.annotations.GenericGenerator
import java.util.*
import javax.persistence.*

@Entity
@Table(name = "user")
class UserEntity(
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)")
var id: UUID,

@Column(name = "email", nullable = false)
val email: String,

@Column(name = "password", nullable = false)
val password: String,

@Column(name = "point", nullable = false)
var point: Int = 0,

@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "Role", joinColumns = [JoinColumn(name = "user_id")])
val role: MutableList<Role>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package andreas311.miso.domain.user.adapter.output.persistence.mapper

import andreas311.miso.domain.user.adapter.output.persistence.entity.UserEntity
import andreas311.miso.domain.user.domain.User
import org.springframework.stereotype.Component

@Component
class UserMapper {
fun toDomain(entity: UserEntity?): User? =
entity?.let {
User(
id = entity.id,
email = entity.email,
password = entity.password,
point = entity.point,
role = entity.role
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package andreas311.miso.domain.user.adapter.output.persistence.repository

import andreas311.miso.domain.user.adapter.output.persistence.entity.UserEntity
import org.springframework.data.repository.CrudRepository
import java.util.UUID

interface UserRepository: CrudRepository<UserEntity, UUID> {
fun findByEmail(email: String): UserEntity?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package andreas311.miso.domain.user.application.exception

import andreas311.miso.global.error.ErrorCode
import andreas311.miso.global.error.exception.MisoException

class UserNotFoundException: MisoException(ErrorCode.USER_NOT_FOUND)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.domain.user.application.port.output

import andreas311.miso.domain.user.domain.User

interface QueryUserPort {
fun findByEmailOrNull(email: String): User?
}
9 changes: 9 additions & 0 deletions src/main/kotlin/andreas311/miso/domain/user/domain/Role.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package andreas311.miso.domain.user.domain

import org.springframework.security.core.GrantedAuthority

enum class Role : GrantedAuthority {
ROLE_USER, ROLE_ADMIN;

override fun getAuthority(): String = name
}
25 changes: 25 additions & 0 deletions src/main/kotlin/andreas311/miso/domain/user/domain/User.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package andreas311.miso.domain.user.domain

import java.util.*

data class User(
var id: UUID,
val email: String,
val password: String,
var point: Int,
val role: MutableList<Role>
) {
fun addPoint(point: Int): User {
synchronized(this) {
this.point += point
}
return this
}

fun removePoint(point: Int): User {
synchronized(this) {
this.point -= point
}
return this
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package andreas311.miso.global.config

import andreas311.miso.global.redis.properties.RedisProperties
import andreas311.miso.global.security.jwt.common.properties.JwtProperties
import andreas311.miso.global.security.jwt.common.properties.JwtTimeProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationPropertiesScan(
basePackageClasses = [
RedisProperties::class
RedisProperties::class,
JwtProperties::class,
JwtTimeProperties::class
]
)
class PropertiesScanConfig
1 change: 1 addition & 0 deletions src/main/kotlin/andreas311/miso/global/error/ErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum class ErrorCode(
// TOKEN
TOKEN_IS_EXPIRED(401, "토큰이 만료 되었습니다."),
TOKEN_NOT_VALID(401, "토큰이 유효 하지 않습니다."),
TOKEN_TYPE_NOT_VALID(401, "토큰 타입이 유효하지 않습니다"),

// USER
EMAIL_KEY_IS_INVALID(401, "이메일 인증번호가 일치하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package andreas311.miso.global.security.adapter

import andreas311.miso.domain.auth.application.port.output.PasswordEncodePort
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component

@Component
class PasswordEncodeAdapter(
private val passwordEncoder: PasswordEncoder
): PasswordEncodePort {
override fun passwordEncode(password: String): String =
passwordEncoder.encode(password)

override fun isPasswordMatch(rawPassword: String, encodedPassword: String): Boolean =
passwordEncoder.matches(rawPassword, encodedPassword)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package andreas311.miso.global.security.adapter

import andreas311.miso.domain.auth.application.port.output.UserSecurityPort
import andreas311.miso.domain.user.application.exception.UserNotFoundException
import andreas311.miso.domain.user.application.port.output.QueryUserPort
import andreas311.miso.domain.user.domain.User
import andreas311.miso.global.security.principal.AdminDetail
import andreas311.miso.global.security.principal.UserDetail
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Component

@Component
class UserSecurityAdapter(
private val queryUserPort: QueryUserPort
): UserSecurityPort {
override fun currentUser(): User {
val email = fetchUserEmail()
return fetchUserByEmail(email)
}

private fun fetchUserByEmail(email: String): User =
queryUserPort.findByEmailOrNull(email) ?: throw UserNotFoundException()

private fun fetchUserEmail(): String =
when(val principal = SecurityContextHolder.getContext().authentication.principal) {
is UserDetails -> {
when (principal) {
is UserDetail -> principal.username
is AdminDetail -> principal.username
else -> throw IllegalArgumentException()
}
}
else -> principal.toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package andreas311.miso.global.security.config

import andreas311.miso.global.security.filter.JwtExceptionFilter
import andreas311.miso.global.security.filter.JwtRequestFilter
import andreas311.miso.global.security.handler.CustomAccessDeniedHandler
import andreas311.miso.global.security.handler.CustomAuthenticationEntryPointHandler
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.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.springframework.security.web.util.matcher.RequestMatcher
import org.springframework.web.cors.CorsUtils

@Configuration
@EnableWebSecurity
class SecurityConfig(
private val jwtRequestFilter: JwtRequestFilter,
private val jwtExceptionFilter: JwtExceptionFilter
) {
@Bean
fun filterChain(http: HttpSecurity) : SecurityFilterChain {
return http
.cors().and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()

.authorizeRequests()
.requestMatchers(RequestMatcher { request ->
CorsUtils.isPreFlightRequest(request)
}).permitAll()

.antMatchers(HttpMethod.POST, "/auth").permitAll()
.antMatchers(HttpMethod.POST, "/auth/signIn").permitAll()
.antMatchers(HttpMethod.DELETE, "/auth").permitAll()
.antMatchers(HttpMethod.PATCH, "/auth").permitAll()

.antMatchers(HttpMethod.POST, "/email").permitAll()

.antMatchers(HttpMethod.GET, "/item").authenticated()
.antMatchers(HttpMethod.GET, "/item/{id}").authenticated()

.antMatchers(HttpMethod.GET, "/user").authenticated()
.antMatchers(HttpMethod.GET, "/user/point").authenticated()
.antMatchers(HttpMethod.POST, "/user/give").authenticated()

.antMatchers(HttpMethod.GET, "/purchase").authenticated()
.antMatchers(HttpMethod.POST, "/purchase/{id}").authenticated()

.antMatchers(HttpMethod.POST, "/inquiry").authenticated()
.antMatchers(HttpMethod.GET, "/inquiry").authenticated()
.antMatchers(HttpMethod.GET, "/inquiry/list").authenticated()
.antMatchers(HttpMethod.GET, "/inquiry/all").hasAuthority("ROLE_ADMIN")
.antMatchers(HttpMethod.GET, "/inquiry/filter/{state}").authenticated()
.antMatchers(HttpMethod.GET, "/inquiry/{id}").authenticated()
.antMatchers(HttpMethod.PATCH, "/inquiry/respond/{id}").hasAuthority("ROLE_ADMIN")

.antMatchers(HttpMethod.GET, "/recyclables").authenticated()
.antMatchers(HttpMethod.GET, "/recyclables/search").authenticated()
.antMatchers(HttpMethod.GET, "/recyclables/all").authenticated()
.antMatchers(HttpMethod.POST, "/recyclables/process").authenticated()

.antMatchers(HttpMethod.POST, "/notification/save/{deviceToken}").authenticated()
.antMatchers(HttpMethod.GET, "/notification/{id}").authenticated()

.antMatchers(HttpMethod.GET, "/environment").authenticated()

.anyRequest().denyAll()
.and()
.exceptionHandling()
.accessDeniedHandler(CustomAccessDeniedHandler())
.authenticationEntryPoint(CustomAuthenticationEntryPointHandler())

.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(jwtExceptionFilter, JwtRequestFilter::class.java)

// 추가 예정
// .addFilterBefore(logRequestFilter, JwtExceptionFilter::class.java)

.build()
}

@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder(12)
}
}
Loading
Loading