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: (BRD-74) 액세스토큰 인증 기능 추가 #53

Merged
merged 14 commits into from
Nov 17, 2024

Conversation

ttasjwi
Copy link
Owner

@ttasjwi ttasjwi commented Nov 15, 2024

JIRA 티켓


@ttasjwi ttasjwi self-assigned this Nov 15, 2024
Comment on lines -2 to +3
implementation(Dependencies.SPRING_BOOT_STARTER.fullName)
implementation(Dependencies.SPRING_SECURITY_CRYPTO.fullName)
implementation(Dependencies.SPRING_BOOT_SECURITY.fullName)
implementation(Dependencies.SPRING_BOOT_WEB.fullName)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • spring-boot-starter-security 의존성을 추가합니다.
  • spring-boot-starter-security 에 이미 spring-security-crypto 가 포함되어 있으므로 대체합니다.

@ttasjwi ttasjwi force-pushed the BRD-74__access-token-authenticate branch from 82bf593 to cf8761f Compare November 15, 2024 11:28
Comment on lines +16 to +21
@Bean
@Order(0)
fun apiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**")
authorizeHttpRequests {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시큐리티 필터체인은 여러개 등록할 수 있는데, 필터체인 설정 클래스 내에서 @Order 를 지정하여, 필터체인 순서를 지정할 수 있습니다. 이렇게 할 경우 securityMatcher에 가장 먼저 매칭된 요청이 해당 필터체인을 거치게 됩니다.

/api/.. 로 시작되는 경우 먼저 매칭되어 api 필터체인을 타게 됩니다.

Comment on lines +22 to +32
authorize(HttpMethod.GET, "/api/v1/deploy/health-check", permitAll)

authorize(HttpMethod.GET, "/api/v1/members/email-available", permitAll)
authorize(HttpMethod.GET, "/api/v1/members/username-available", permitAll)
authorize(HttpMethod.GET, "/api/v1/members/nickname-available", permitAll)

authorize(HttpMethod.POST, "/api/v1/members/email-verification/start", permitAll)
authorize(HttpMethod.POST, "/api/v1/members/email-verification", permitAll)
authorize(HttpMethod.POST, "/api/v1/members", permitAll)

authorize(HttpMethod.POST, "/api/v1/auth/login", permitAll)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 기존 작성한 api 들은 모두 permitAll (무조건 통과) 처리했습니다.
  2. 다만 여기서 health-check 기능은 특정 시스템에 의해서만 되도록 해야하는데, 이 기능은 향후 개선할 예정입니다.


authorize(HttpMethod.POST, "/api/v1/auth/login", permitAll)

authorize(anyRequest, authenticated)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별도의 설정을 하지 않은 api들은 무조건 인증을 필요로 하게 됩니다.

Comment on lines +50 to +59
@Bean
@Order(1)
fun staticResourceSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, permitAll)
}
}
return http.build()
}
Copy link
Owner Author

@ttasjwi ttasjwi Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/api/ 로 시작하지 않는 경로는 모두 통과시킵니다. 정적 리소스로 간주하고 일단 통과시킬겁니다.

@ttasjwi ttasjwi force-pushed the BRD-74__access-token-authenticate branch from 2de3238 to c431959 Compare November 15, 2024 13:37
@ttasjwi ttasjwi force-pushed the BRD-74__access-token-authenticate branch from fb9ad3b to edcd3ab Compare November 17, 2024 02:48
- 스프링 시큐리티에서 발생하는 인증필요/인가실패 예외를 커스텀예외로 변환하여 처리하도록 했다.
Comment on lines 7 to 18
class BearerTokenResolver {

private val bearerTokenHeaderName = HttpHeaders.AUTHORIZATION

fun resolve(request: HttpServletRequest): String? {
val authorizationHeader = request.getHeader(this.bearerTokenHeaderName) ?: return null

if (!authorizationHeader.startsWith("Bearer ", ignoreCase = true)) {
throw InvalidAuthorizationHeaderFormatException()
}
return authorizationHeader.substring(7)
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • BearerTokenResolver 는 AuthorizationHeader 에서 "Bearere " 뒤의 문자열을 추출합니다.
  • 이 때 "Bearer "로 시작하지 않으면 예외가 발생합니다.
  • AuthorizationHeader 가 없으면 null 을 반환합니다.

Comment on lines +62 to +65
exceptionHandling {
authenticationEntryPoint = CustomAuthenticationEntryPoint(handlerExceptionResolver)
accessDeniedHandler = CustomAccessDeniedHandler(handlerExceptionResolver)
}
Copy link
Owner Author

@ttasjwi ttasjwi Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인증 /인가 담당 필터인 AuthorizationFilter 앞에 있는 ExceptionTranslationFilter는 스프링 시큐리티 예외 후속 처리를 AuthenticationEntryPoint, AccessDeniedHandler 에게 위임합니다.

이 부분에서 사용되는 구현체를 제가 커스터마이징 했습니다.

Comment on lines +24 to +48
// 이미 인증됐다면 통과
if (isAuthenticated()) {
filterChain.doFilter(request, response)
return
}

// 헤더를 통해 토큰을 가져옴. 없다면 통과
val tokenValue = bearerTokenResolver.resolve(request)
if (tokenValue == null) {
filterChain.doFilter(request, response)
return
}

// 토큰값을 통해 인증
val authentication = attemptAuthenticate(tokenValue)

// 인증 결과를 SecurityContextHolder 에 저장
saveAuthenticationToSecurityContextHolder(authentication)

// 통과
try {
filterChain.doFilter(request, response)
} finally {
SecurityContextHolder.getContextHolderStrategy().clearContext()
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AccessTokenAuthenticationFilter 는 액세스토큰 인증을 담당합니다.

  • 이미 인증된 회원이면 통과시킵니다.
  • BearerTokenResolver 를 통해 Authorization 헤더에서 Bearer 토큰값을 추출합니다. 토큰값이 없으면 통과시킵니다.
  • 토큰값이 있으면 액세스토큰을 파싱하고 유효한지 확인한 뒤, 인증 결과물인 Authentication 을 획득합니다.
  • 여기서 얻어진 Authentication 을 SecurityContextHolder 에 저장하고, 필터체인을 통해 통과시킵니다.
  • finally 문에서 시큐리티 컨텍스트홀더 자원을 반드시 정리합니다.

Comment on lines +8 to +25
class AuthMemberAuthentication
private constructor(
private val authMember: AuthMember
) : Authentication {

companion object {
fun from(authMember: AuthMember): AuthMemberAuthentication {
return AuthMemberAuthentication(authMember)
}
}

override fun getName(): String? {
return null
}

override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
return mutableListOf(SimpleGrantedAuthority("ROLE_${authMember.role.name}"))
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthMemberAuthentication 은 AuthMember 로 구성한 스프링 시큐리티 Authentication 입니다.

@ttasjwi ttasjwi merged commit d851ed9 into master Nov 17, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant