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 세팅 #6

Merged
merged 17 commits into from
Oct 10, 2023
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
2 changes: 2 additions & 0 deletions src/main/kotlin/andreas311/miso/MisoApplication.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package andreas311.miso

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication
@ConfigurationPropertiesScan
class MisoApplication

fun main(args: Array<String>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.domain.auth.exception

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

class RoleNotExistException : MisoException(ErrorCode.ROLE_NOT_EXIST) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.domain.user.exception

import andreas311.miso.global.error.exception.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
Expand Up @@ -5,4 +5,6 @@ import org.springframework.data.repository.CrudRepository
import java.util.UUID

interface UserRepository : CrudRepository<User, UUID> {

fun findByEmail(email: String): User?
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ enum class ErrorCode(
// SERVER ERROR
UNKNOWN_ERROR(500, "알 수 없는 에러입니다."),

// USER
USER_NOT_FOUND(404, "사용자를 찾을 수 없습니다."),
ROLE_NOT_EXIST(404, "역할이 존재하지 않습니다"),

// TOKEN
TOKEN_IS_EXPIRED(401, "토큰이 만료 되었습니다."),
TOKEN_NOT_VALID(401, "토큰이 유효 하지 않습니다."),
Expand Down
50 changes: 50 additions & 0 deletions src/main/kotlin/andreas311/miso/global/security/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package andreas311.miso.global.security

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.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.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 jwtExceptionFilter: JwtExceptionFilter,
private val jwtRequestFilter: JwtRequestFilter
) {

@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()

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

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

.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package andreas311.miso.global.security.auth

import andreas311.miso.domain.user.exception.UserNotFoundException
import andreas311.miso.domain.user.repository.UserRepository
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service

@Service
class AuthDetailService(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {

val user = userRepository.findByEmail(username) ?: throw UserNotFoundException()

return AuthDetails(user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package andreas311.miso.global.security.auth

import andreas311.miso.domain.user.entity.User
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails

class AuthDetails(
private val user: User
) : UserDetails {
override fun getAuthorities(): MutableCollection<out GrantedAuthority>? = user.role

override fun getPassword(): String? = null

override fun getUsername(): String = user.email

override fun isAccountNonExpired(): Boolean = true

override fun isAccountNonLocked(): Boolean = true

override fun isCredentialsNonExpired(): Boolean = true

override fun isEnabled(): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.global.security.exception

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

class TokenExpiredException : MisoException(ErrorCode.TOKEN_IS_EXPIRED) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package andreas311.miso.global.security.exception

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

class TokenInvalidException : MisoException(ErrorCode.TOKEN_NOT_VALID) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class ExceptionFilter(
class JwtExceptionFilter(
private val objectMapper: ObjectMapper
) : OncePerRequestFilter() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package andreas311.miso.global.security.filter

import andreas311.miso.global.security.jwt.TokenProvider
import org.slf4j.LoggerFactory
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class JwtRequestFilter(
private val tokenProvider: TokenProvider
) : OncePerRequestFilter() {

private val log = LoggerFactory.getLogger(this::class.simpleName)

override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

val accessToken = tokenProvider.resolveToken(request)

if(!accessToken.isNullOrBlank()) {

val authentication = tokenProvider.authentication(accessToken)

SecurityContextHolder.getContext().authentication = authentication

log.info("current user email = ${authentication.name}")
}

filterChain.doFilter(request, response)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package andreas311.miso.global.security.handler

import org.slf4j.LoggerFactory
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component
import java.io.IOException
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class CustomAccessDeniedHandler : AccessDeniedHandler {
private val log = LoggerFactory.getLogger(this::class.simpleName)

@Throws(IOException::class, ServletException::class)
override fun handle(request: HttpServletRequest, response: HttpServletResponse, accessDeniedException: AccessDeniedException) {

log.info("=== Access Denied ===")

response.sendError(HttpServletResponse.SC_FORBIDDEN)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package andreas311.miso.global.security.handler

import org.slf4j.LoggerFactory
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
import java.io.IOException
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@Component
class CustomAuthenticationEntryPointHandler : AuthenticationEntryPoint {
private val log = LoggerFactory.getLogger(this::class.simpleName)

@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {

log.info("=== AuthenticationEntryPoint ===")

response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
}
111 changes: 111 additions & 0 deletions src/main/kotlin/andreas311/miso/global/security/jwt/TokenProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package andreas311.miso.global.security.jwt

import andreas311.miso.domain.auth.exception.RoleNotExistException
import andreas311.miso.domain.user.enums.Role
import andreas311.miso.global.security.auth.AuthDetailService
import andreas311.miso.global.security.exception.TokenExpiredException
import andreas311.miso.global.security.exception.TokenInvalidException
import andreas311.miso.global.security.jwt.properties.JwtProperties
import andreas311.miso.global.security.jwt.properties.TokenTimeProperties
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import java.security.Key
import java.time.ZonedDateTime
import java.util.*
import javax.servlet.http.HttpServletRequest

@Component
class TokenProvider(
private val jwtProperties: JwtProperties,
private val tokenTimeProperties: TokenTimeProperties,
private val authDetailService: AuthDetailService,
) {
companion object {
const val ACCESS_TYPE = "access"
const val REFRESH_TYPE = "refresh"
const val TOKEN_PREFIX = "Bearer "
const val AUTHORITY = "authority"
}

val accessExpiredTime: ZonedDateTime
get() = ZonedDateTime.now().plusSeconds(tokenTimeProperties.accessTime)

val refreshExpiredTime: ZonedDateTime
get() = ZonedDateTime.now().plusSeconds(tokenTimeProperties.refreshTime)

fun generateAccessToken(email: String, role: Role): String =
generateToken(email, ACCESS_TYPE, jwtProperties.accessSecret, tokenTimeProperties.accessTime, role)

fun generateRefreshToken(email: String, role: Role): String =
generateToken(email, REFRESH_TYPE, jwtProperties.refreshSecret, tokenTimeProperties.refreshTime, role)

fun resolveToken(req: HttpServletRequest): String? {
val token = req.getHeader("Authorization") ?: return null
return parseToken(token)
}

fun exactEmailFromRefreshToken(refresh: String): String {
return getTokenSubject(refresh, jwtProperties.refreshSecret)
}

fun exactRoleFromRefreshToken(refresh: String): Role {
val authority = getTokenBody(refresh, jwtProperties.refreshSecret)
.get(AUTHORITY, String::class.java)

return when (authority) {
"ROLE_USER" -> Role.ROLE_USER
"ROLE_ADMIN" -> Role.ROLE_ADMIN
else -> throw RoleNotExistException()
}

}

fun exactTypeFromRefreshToken(refresh: String): String =
getTokenSubject(refresh, jwtProperties.refreshSecret)

fun authentication(token: String): Authentication {
val userDetails = authDetailService.loadUserByUsername(getTokenSubject(token, jwtProperties.accessSecret))
return UsernamePasswordAuthenticationToken(userDetails, "", userDetails.authorities)
}

fun parseToken(token: String): String? =
if (token.startsWith(TOKEN_PREFIX))
token.replace(TOKEN_PREFIX, "")
else
null

fun generateToken(email: String, type: String, secret: Key, exp: Long, role: Role): String {
val claims = Jwts.claims().setSubject(email)
claims["type"] = type
claims[AUTHORITY] = role
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.signWith(secret, SignatureAlgorithm.HS256)
.setClaims(claims)
.setIssuedAt(Date())
.setExpiration(Date(System.currentTimeMillis() + exp * 1000))
.compact()
}

private fun getTokenBody(token: String, secret: Key): Claims {
return try {
Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token)
.body
} catch (e: ExpiredJwtException) {
throw TokenExpiredException()
} catch (e: Exception) {
throw TokenInvalidException()
}
}

private fun getTokenSubject(token: String, secret: Key): String =
getTokenBody(token, secret).subject
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package andreas311.miso.global.security.jwt.properties

import io.jsonwebtoken.security.Keys
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.security.Key

@ConstructorBinding
@ConfigurationProperties(prefix = "jwt")
class JwtProperties(
accessSecret: String,
refreshSecret: String
) {

val accessSecret: Key
val refreshSecret: Key

init {
this.accessSecret = Keys.hmacShaKeyFor(accessSecret.toByteArray())
this.refreshSecret = Keys.hmacShaKeyFor(refreshSecret.toByteArray())
}
}
Loading