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

feat: Discord 기반 OAuth2 인증 구현 (#12) #36

Merged
merged 10 commits into from
Dec 20, 2023
Merged

feat: Discord 기반 OAuth2 인증 구현 (#12) #36

merged 10 commits into from
Dec 20, 2023

Conversation

seokjin8678
Copy link
Contributor

@seokjin8678 seokjin8678 commented Dec 18, 2023

관련 이슈

PR 세부 내용

Discord 기반의 OAuth2 인증 기능을 구현했습니다.
자체 JWT 토큰을 반환하는 기능은 TODO로 남겨두었습니다. (한 번에 전부 구현하려면 변경 내역이 너무 많아져서 그렇습니다..!)

외부 API를 호출하는 로직은 Spring WebFlux를 사용했습니다.
이전 프로젝트에서는 RestTemplate를 사용했는데, RestTemplate의 지원이 이제 종료되어 WebFlux를 사용하라고 하더라구요.
WebFlux의 특징이 논블럭킹 비동기를 제공한다는 특징이 있는데, 제대로 사용하려면 학습 곡선이 조금 있어 그냥 block()를 사용하여 동기로 처리했습니다. (애초에 비동기로 처리할 수 없는 것이, 사용자는 토큰을 바로 얻어야해서.. 😂)

WebFlux로 비동기로 처리할 껀덕지는 웹 크롤링을 수행할 때인데, Jsoup로 HTML을 크롤링 하는것 또한 WebFlux를 사용해 볼 수 있을 것 같네요.

개발, 테스트 환경에서 실제 OAuth2를 사용할 필요는 없으니, LocalOAuth2Client를 사용하면 될 것 같습니다.

자세한 내용은 코드에 리뷰로 남기겠습니다.

@seokjin8678 seokjin8678 added the 💎 핵심기능 핵심 기능에 관한 작업 label Dec 18, 2023
@seokjin8678 seokjin8678 requested a review from Laeng December 18, 2023 13:43
@seokjin8678 seokjin8678 self-assigned this Dec 18, 2023
@seokjin8678 seokjin8678 linked an issue Dec 18, 2023 that may be closed by this pull request
Copy link

github-actions bot commented Dec 18, 2023

Test Results

14 files  14 suites   2s ⏱️
39 tests 39 ✔️ 0 💤 0
48 runs  48 ✔️ 0 💤 0

Results for commit de7c2c0.

♻️ This comment has been updated with latest results.

@seokjin8678 seokjin8678 added the ☢️ DB 데이터베이스에 관한 작업 label Dec 18, 2023
Comment on lines +19 to +27
@GetMapping("/login")
fun login(
@RequestParam code: String,
@RequestParam socialType: SocialType,
): ResponseEntity<ApiResponse<LoginResponse>> {
val response = oAuth2FacadeService.login(code, socialType)
return ResponseEntity.ok()
.body(ApiResponse.success(response))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

로그인 요청에서 HTTP 메소드는 GET 보단 POST가 맞지만, 쿼리 파라미터를 사용해서 하나의 핸들러 메서드로 OAuth2 인증을 처리하기 위해 GET을 사용했습니다!

Comment on lines +41 to +52
private fun handleAccessTokenError(clientResponse: ClientResponse): Exception {
return when (clientResponse.statusCode()) {
HttpStatus.UNAUTHORIZED -> {
BadRequestException("잘못된 OAuth2 Authorization 코드입니다.")
}

else -> {
log.warn { "getAccessToken() 호출에서 ${clientResponse.statusCode()} 예외가 발생했습니다." }
InternalServerError("Discord OAuth2 서버에 문제가 발생했습니다.")
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discord AccessToken을 요청하는 과정에서 401 상태코드가 반환되는 것은 잘못된 code를 입력할 때 발생하더군요.
그 외는 설정을 잘못 입력할 때이므로, 서버 설정이 잘못되었다고 판단하여 InternalServerError를 던졌습니다. (500 상태코드)

Comment on lines +64 to +76
override fun getUserInfo(accessToken: String): UserInfo {
return webClient.get()
.uri("/api/users/@me")
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
.retrieve()
.onStatus({ it.is4xxClientError || it.is5xxServerError }) {
log.warn { "getUserInfo() 호출에서 ${it.statusCode()} 예외가 발생했습니다." }
throw InternalServerError("Discord OAuth2 서버에 문제가 발생했습니다.")
}
.bodyToMono<DiscordUserInfoResponse>()
.block()!!
.toUserInfo()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

userInfo를 요청하는 것은 사용자가 아닌, 서버에서 모두 통제하므로 500 상태코드를 반환하게 했습니다.

Comment on lines +92 to +109
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class DiscordUserInfoResponse(
val id: String,
val username: String,
val email: String?,
val avatar: String?,
) {

fun toUserInfo(): UserInfo {
return UserInfo(
email = email,
nickname = username,
socialType = SocialType.DISCORD,
socialId = id,
profileImage = avatar?.let { "https://cdn.discordapp.com/avatars/${id}/${avatar}.png" }
)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://discord.com/developers/docs/resources/user
디스코드에서 제공하는 User Object의 리턴 타입입니다.
id, username은 not null 이지만, email, avatar는 nullable 하더군요.
따라서 똑같이 필드를 nullable 하게 해주었습니다.

@seokjin8678
Copy link
Contributor Author

WebClient를 찾아보다, RestClient를 찾았는데 이걸 사용하면 굳이 WebFlux에 관한 의존성을 추가할 필요가 없겠네요!
https://spring.io/blog/2023/07/13/new-in-spring-6-1-restclient

Copy link
Member

@Laeng Laeng left a comment

Choose a reason for hiding this comment

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

고생 많으셨습니다!!!!!

저 같은경우 retrofit2을 사용하긴 했습니다! retorfit2 도 한번 사용해보세요 ㅎㅎ

Spring Security OAuth client 도입은 어떠세요?

@Laeng Laeng merged commit 5a5c9ee into dev Dec 20, 2023
3 checks passed
@seokjin8678 seokjin8678 deleted the feat/#12 branch December 20, 2023 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
☢️ DB 데이터베이스에 관한 작업 💎 핵심기능 핵심 기능에 관한 작업
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: Discord 기반의 OAuth2 인증을 구현한다.
2 participants