[MoGakGo] 인증 프로세스 개선하기

David Lee·2024년 3월 27일
0

MoGakGo

목록 보기
2/5
post-thumbnail

프로세스 개선이 필요로 한 이유는?

지난 포스트 GitHub OAuth2 + JWT 활용 인증 구현하기에서 아쉬움으로 남았던 직접 리다이렉트 방식의 수정과 세션이 stateless한 상태에서의 인증을 구현하기 위해 리펙토링을 진행하기로 했습니다

변경된 인증 프로세스 살펴보기

기존 인증 프로세스에서 변경된 방식은

  • Client에서 GitHub OAuth2 로그인 링크로 로그인을 진행한 후 Callback을 받습니다.
  • Client는 Callback으로 전달받은 code와 함께 MoGakGo 서버에 Access Token 발급 요청을 보냅니다.
  • MoGakGo 서버는 전달받은 code로 GithubOAuth2Manager를 통해 GitHub 서버와 OAuth2 인증 과정을 진행합니다.
  • 인증 절차가 완료되면 AuthService가 GitHub 유저 정보를 기반으로 기반으로 MoGakGo 유저 관리, JWT 토큰 생성 및 저장을 진행합니다.
  • 생성된 JWT 토큰을 Client에 전달합니다.

세부 구현 살펴보기

자세한 코드는 레포지토리에서 확인할 수 있습니다.

GithubOAuth2Manager

  • GithubOAuth2ManagerSpring OAuth2 Client를 대신해 Github 로그인을 담당하는 컴포넌트입니다.
    • Spring에서 제공하는 WebClient를 활용해 구현이 이루어졌으며 로그인 과정은 동기로 이루어집니다.
    • 전달받은 code는 getAccessToken() 메서드에서 사용하며, Github Resource Server에 접근 가능한 Access Token을 발급 받습니다.
    • 위에서 발급받은 Access Token을 활용해서 getGithubUserInfo() 메서드는 Github Resource Server에 접근, 사용자 정보를 받아옵니다.
@Component
public class GithubOAuth2Manager {

    private final String clientId;
    private final String clientSecret;

    public GithubOAuth2Manager(@Value("${github.oauth2.registration.client-id}") String clientId,
        @Value("${github.oauth2.registration.client-secret}") String clientSecret) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

    public String getAccessToken(String code) {
        WebClient webClient = WebClient.builder()
            .baseUrl("https://github.com/login/oauth/access_token")
            .defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
            .build();
        var result = webClient.post().uri(uriBuilder ->
            uriBuilder
                .queryParam("client_id", clientId)
                .queryParam("client_secret", clientSecret)
                .queryParam("code", code)
                .build()
        ).retrieve().bodyToMono(new ParameterizedTypeReference<Map<String, String>>() {
        }).block();
        return Objects.requireNonNull(result).get("access_token");
    }

    public Map<String, Object> getGithubUserInfo(String accessToken) {
        WebClient webClient = WebClient.builder()
            .baseUrl("https://api.github.com/user")
            .build();
        return webClient.get().header(AUTHORIZATION, "Bearer " + accessToken).retrieve()
            .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
            }).doOnError(error -> {
                throw new AuthException(ErrorCode401.AUTH_MISSING_CREDENTIALS);
            }).block();
    }
}

AuthService

  • AuthServiceOAuth2AuthenticationSuccessHandler를 대신해 API 호출로 전달된 로그인 요청을 처리하는 서비스 레이어입니다.
@Slf4j
@RequiredArgsConstructor
@Service
public class AuthService {

    private final JwtRedisDao jwtRedisDao;
    private final JwtHelper jwtHelper;
    private final AuthUserService authUserService;
    private final GithubOAuth2Manager githubOAuth2Manager;

    @Transactional
    public AuthLoginResponse loginViaGithubCode(String code) {
        verifyCode(code);
        var githubAccessToken = githubOAuth2Manager.getAccessToken(code);
        var githubUserInfo = githubOAuth2Manager.getGithubUserInfo(githubAccessToken);
        var userOAuth2Response = authUserService.manageOAuth2User(githubUserInfo);
        var jwtToken = generateJwtToken(userOAuth2Response);
        return AuthLoginResponse.of(userOAuth2Response, jwtToken);
    }
    
    private void verifyCode(String code) {
        if (code == null || code.isBlank()) {
            throw new AuthException(ErrorCode401.AUTH_MISSING_CREDENTIALS);
        }
    }

    private String generateAccessToken(String expiredAccessToken) {
        Map<String, Claim> claims = jwtHelper.verifyWithoutExpiry(expiredAccessToken);
        long userId = claims.get(JwtHelper.USER_ID_STR).asLong();
        String[] roles = claims.get(JwtHelper.ROLES_STR).asArray(String.class);
        return jwtHelper.sign(userId, roles, expiredAccessToken).getAccessToken();
    }
    ...
}

마무리

API 호출에 의해 응답을 전달할 수 있게 되어 클라이언트의 뷰와 강결합을 맺지 않아도 된다는 장점과, 세션 상태를 stateless하게 가져갈 수 있다는 점을 모두 챙길 수 있는 리펙토링 방식이었습니다.
기존 Spring OAuth2 Client는 로그인 페이지를 무조건 생성한다는 점과 위의 단점이 존재했지만, 프로젝트에서 OAuth2 로그인을 다중으로 사용한다(ex. 카카오, 네이버, Google, Github 를 다 사용할거야!)면 관리의 효율성과 편의성이 더 뛰어나기에 직접 구현보다는 더 좋은 선택지라고 생각합니다.

래퍼런스

profile
쌓아가기

0개의 댓글

관련 채용 정보