Skip to content

Commit

Permalink
#15, 16 - Security 설정, 회원가입 개발 (#34)
Browse files Browse the repository at this point in the history
* chore: Spring Security gradle 추가

* feat: User 도메인 정보 추가

- email, password, role, socialEmail 추가

* feat: PrincipalDetail 설정

- 인증과 권한 부여를 위해 사용자 정보를 캡슐화

* feat: 시큐리티 필터 설정 및 JWT 개발

* feat: refreshToken 개발

* feat: JWT Provider 개발

* refactor: BaseEntity 변경

* refactor: School 도메인 리펙토링

* feat: School Repositroy, mapper 개발

* feat: Profile Repositroy, mapper 개발

* refactor: Setting, FCM 도메인 수정

* feat: UserConsent 도메인 추가

* refactor: Social 도메인 수정

* refactor: User 도메인 수정 및 Role 추가

* feat: Auth Dto 설정

* refactor: School 도메인 리팩토링

유저와 연결 끊도록 설정

* refactor: RefreshToken create, update 설정 및 서비스 추가

* feat: Redis 모듈 추가, 설정 및 AuthDate 저장

* refactor: DTO 폴더 경로 추가

* feat: SecurityUtils 추가

* feat: AuthService 추가

* feat: Auth Controller 추가 및 url 설정 변경

* refactor: rebase 후 코드 리팩토링

* test: service Test 코드 작성

* fix: baseEntity notnull 조건 제거

* fix: VoteServiceTest 에러 주석 처리

* test: 주석 제거

* chore: 서브모듈에 jwt 추가

* refactor: gitmodules 수정 이후, 이전 backend-submodule 삭제

* refactor: submoudle 이름 config로 변경

* refactor: LocalDateTime.now() 모킹

* refactor:  401에러 예외 메시지 설정, cors 주소, delete에 트랜잭션  추가

* refactor: api prefix 제거

* refactor: refreshToken test에 있는 createdAt 수정

---------

Co-authored-by: jaeyeon kim <[email protected]>
  • Loading branch information
sectionr0 and kpeel5839 authored Jul 17, 2024
1 parent a0591d9 commit b0983e5
Show file tree
Hide file tree
Showing 104 changed files with 2,122 additions and 253 deletions.
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "app/src/main/resources/backend-submodule"]
path = app/src/main/resources/backend-submodule
url = https://github.com/yapp-wespot/backend-submodule.git
[submodule "app/src/main/resources/config"]
path = app/src/main/resources/config
url = https://github.com/yapp-wespot/config.git
9 changes: 9 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ dependencies {
implementation(project(":domain"))
implementation(project(":core"))
implementation(project(":infrastructure:mysql"))
implementation(project(":infrastructure:redis"))


// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
testImplementation("io.jsonwebtoken:jjwt-api:0.11.2")
testImplementation("io.jsonwebtoken:jjwt-impl:0.11.2")
testImplementation("io.jsonwebtoken:jjwt-jackson:0.11.2")

testImplementation("io.rest-assured:rest-assured")


}
71 changes: 71 additions & 0 deletions app/src/main/kotlin/com/wespot/auth/AuthController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.wespot.auth

import com.wespot.auth.dto.request.AuthLoginRequest
import com.wespot.auth.dto.request.RefreshTokenRequest
import com.wespot.auth.dto.request.SignUpRequest
import com.wespot.auth.dto.response.SignUpResponse
import com.wespot.auth.dto.response.TokenResponse
import com.wespot.auth.service.AuthService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
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("/v1/auth")
class AuthController(
private val authService: AuthService
) {

@PostMapping("/login")
fun signIn(
@RequestBody request: AuthLoginRequest
): Any {

val response = authService.socialAccess(request)

return if (response is SignUpResponse) {
ResponseEntity.status(HttpStatus.ACCEPTED).body(response)
} else {
ResponseEntity.ok()
.body(response)
}

}

@PostMapping("/signup")
fun signUp(
@RequestBody request: SignUpRequest
): ResponseEntity<TokenResponse> {

val signUp = authService.signUp(request)

return ResponseEntity.ok()
.body(signUp)

}

@PostMapping("/reissue")
fun reissue(
@RequestBody request: RefreshTokenRequest
): ResponseEntity<TokenResponse> {

val response = authService.reIssueToken(request)

return ResponseEntity.ok()
.body(response)

}

@PostMapping("/revoke")
fun revoke(): ResponseEntity<Unit> {

authService.revoke()

return ResponseEntity.noContent().build()

}

}
76 changes: 76 additions & 0 deletions app/src/main/kotlin/com/wespot/config/security/CustomUrlFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.wespot.config.security

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ProblemDetail
import org.springframework.stereotype.Component
import org.springframework.util.AntPathMatcher
import org.springframework.web.filter.OncePerRequestFilter
import java.net.URI
import kotlin.text.Charsets.UTF_8

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

private val antPathMatcher = AntPathMatcher()

private val validUrlPatterns = listOf(
"/health",
"/",
"/api/v1/auth/reissue",
"/api/v1/auth/login",
"/api/v1/auth/signup",
"/api/v1/auth/revoke",
)

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

if (!isValidUrl(request)) {
handleInvalidUrl(request, response)
return
}
filterChain.doFilter(request, response)

}

private fun isValidUrl(request: HttpServletRequest): Boolean {

val requestUri = request.requestURI

return validUrlPatterns.any { antPathMatcher.match(it, requestUri) }

}

private fun handleInvalidUrl(
request: HttpServletRequest,
response: HttpServletResponse
) {

response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = UTF_8.name()
response.status = HttpStatus.NOT_FOUND.value()

val body = objectMapper.writeValueAsString(
ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
NoSuchFieldException("잘못된 URL입니다.").message!!,
).apply {
type = URI.create("/errors/not-found")
instance = URI.create(request.requestURI)
}
)

response.writer.write(body)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.wespot.config.security

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ProblemDetail
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component
import java.net.URI
import kotlin.text.Charsets.UTF_8

@Component
class JwtAccessDeniedHandler(
private val objectMapper: ObjectMapper
) : AccessDeniedHandler {
override fun handle(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AccessDeniedException
) {

response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = UTF_8.name()
response.status = HttpStatus.FORBIDDEN.value()

val body = objectMapper.writeValueAsString(
ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED,
AccessDeniedException("자신의 것만 가능합니다").message!!,
).apply {
type = URI.create("/errors/forbidden")
instance = URI.create(request.requestURI)
}
)
response.writer.write(body)

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.wespot.config.security

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ProblemDetail
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
import java.net.URI
import kotlin.text.Charsets.UTF_8

@Component
class JwtAuthenticationEntryPoint(
private val objectMapper: ObjectMapper
) : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException?
) {

response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = UTF_8.name()
response.status = HttpStatus.UNAUTHORIZED.value()

val body = objectMapper.writeValueAsString(
ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED,
BadCredentialsException("로그인이 만료되었습니다. 계속하려면 다시 로그인해 주세요.").message!!,
).apply {
type = URI.create("/errors/unauthenticated")
instance = URI.create(request.requestURI)
}
)
response.writer.write(body)

}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.wespot.config.security

import com.wespot.auth.JwtTokenInfo
import com.wespot.auth.port.`in`.AuthenticationUseCase
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
private val authenticationUseCase: AuthenticationUseCase,
private val jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint,
) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val token = extractToken(request)
if (token != null && StringUtils.hasText(token)) {
try {
authenticateUserByToken(token)
} catch (e: BadCredentialsException) {
jwtAuthenticationEntryPoint.commence(request, response, authException = null)
return
}
}
filterChain.doFilter(request, response)
}

private fun authenticateUserByToken(token: String) {
try {
val authentication = authenticationUseCase.getAuthentication(token)
SecurityContextHolder.getContext().authentication = authentication
} catch (e: BadCredentialsException) {
SecurityContextHolder.clearContext()
}
}

private fun extractToken(request: HttpServletRequest): String? {
val bearerToken = request.getHeader(JwtTokenInfo.AUTHORIZATION_HEADER)
return if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtTokenInfo.BEARER_TYPE)) {
bearerToken.substring(JwtTokenInfo.BEARER_TYPE.length).trim()
} else null
}
}
Loading

0 comments on commit b0983e5

Please sign in to comment.