-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
implementation(Dependencies.SPRING_BOOT_STARTER.fullName) | ||
implementation(Dependencies.SPRING_SECURITY_CRYPTO.fullName) | ||
implementation(Dependencies.SPRING_BOOT_SECURITY.fullName) | ||
implementation(Dependencies.SPRING_BOOT_WEB.fullName) |
There was a problem hiding this comment.
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 가 포함되어 있으므로 대체합니다.
82bf593
to
cf8761f
Compare
@Bean | ||
@Order(0) | ||
fun apiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { | ||
http { | ||
securityMatcher("/api/**") | ||
authorizeHttpRequests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
시큐리티 필터체인은 여러개 등록할 수 있는데, 필터체인 설정 클래스 내에서 @Order
를 지정하여, 필터체인 순서를 지정할 수 있습니다. 이렇게 할 경우 securityMatcher에 가장 먼저 매칭된 요청이 해당 필터체인을 거치게 됩니다.
/api/..
로 시작되는 경우 먼저 매칭되어 api 필터체인을 타게 됩니다.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 기존 작성한 api 들은 모두 permitAll (무조건 통과) 처리했습니다.
- 다만 여기서 health-check 기능은 특정 시스템에 의해서만 되도록 해야하는데, 이 기능은 향후 개선할 예정입니다.
|
||
authorize(HttpMethod.POST, "/api/v1/auth/login", permitAll) | ||
|
||
authorize(anyRequest, authenticated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
별도의 설정을 하지 않은 api들은 무조건 인증을 필요로 하게 됩니다.
@Bean | ||
@Order(1) | ||
fun staticResourceSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { | ||
http { | ||
authorizeHttpRequests { | ||
authorize(anyRequest, permitAll) | ||
} | ||
} | ||
return http.build() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/api/ 로 시작하지 않는 경로는 모두 통과시킵니다. 정적 리소스로 간주하고 일단 통과시킬겁니다.
2de3238
to
c431959
Compare
- AuthorizationFilter에서 발생한 예외는 그 앞에 위치한 ExceptionTranslationFilter로 전파됩니다. - 여기서 인증예외/인가예외는 AuthenticationEntryPoint, AccessDeniedHandler로 전달되어 처리됩니다 - 이 부분을 커스터마이징하였습니다.
fb9ad3b
to
edcd3ab
Compare
- 스프링 시큐리티에서 발생하는 인증필요/인가실패 예외를 커스텀예외로 변환하여 처리하도록 했다.
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) | ||
} |
There was a problem hiding this comment.
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 을 반환합니다.
exceptionHandling { | ||
authenticationEntryPoint = CustomAuthenticationEntryPoint(handlerExceptionResolver) | ||
accessDeniedHandler = CustomAccessDeniedHandler(handlerExceptionResolver) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인증 /인가 담당 필터인 AuthorizationFilter 앞에 있는 ExceptionTranslationFilter는 스프링 시큐리티 예외 후속 처리를 AuthenticationEntryPoint, AccessDeniedHandler 에게 위임합니다.
이 부분에서 사용되는 구현체를 제가 커스터마이징 했습니다.
// 이미 인증됐다면 통과 | ||
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() | ||
} |
There was a problem hiding this comment.
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 문에서 시큐리티 컨텍스트홀더 자원을 반드시 정리합니다.
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}")) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AuthMemberAuthentication 은 AuthMember 로 구성한 스프링 시큐리티 Authentication 입니다.
JIRA 티켓