소셜로그인 문제점 식별 후 변경

박우영·2023년 9월 12일
0

프로젝트

목록 보기
7/7

문제점 식별

기존 게시글
에서 Front-End 에서 Back-End 에 등록되어있는 소셜로그인 링크를 a 태그로 이동하면
회원가입과 Redirect 를 Back-End 에서 진행을 했었습니다.

문제점

  • Front-End 에서 개발, 배포 환경에 맞게 Test 를 진행할 수 없음
    • Back-End 코드에서 Redirect 를 정의하다보니 Front-End 의 환경이 운영, 개발 환경 에 따른 유연한 전환의 어려움

기능적인 문제 X
개발자의 편의성 향상 O

계획

위 의 문제점으로 인해 아래 사진과 같이 Flow 를 변경 할 예정입니다.

Back-End 에서 Authorization code 발급부터 회원 관리까지 하던것을
Front-End 에게 code 와 Redirect Uri 정보를 받아 기존의 문제점을 극복하고자 했습니다.

Code

TO-BE

현재 Baeker 에서는 Kakao 로그인만 제공합니다. 추후에 Google, naver 등 다른 기능 추가의 필요성을 느낀다면 controller 명과 @GetMapping 으로 정의함으로 유연하게 전환 하는것을 고려했습니다.

KakaoController

@RestController
@RequiredArgsConstructor
@RequestMapping("/login/oauth2")
public class KakaoController {

    private final KakaoService kakaoService;

    @GetMapping("/kakao")
    public SocialLoginResponse kakaoLogin(String code, String redirectUri) {
        return kakaoService.kakaoLogin(code, redirectUri);
    }
}

OidcService

@Service
public class CustomOidcUserService extends AbstractOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {


    @Override
    @Transactional
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {

        // Open ID Connect 인 경우 User name Attribute Key 가 sub 이기 때문에 재정의함
        ClientRegistration clientRegistration = ClientRegistration
                .withClientRegistration(userRequest.getClientRegistration())
                .userNameAttributeName("sub")
                .build();

        OidcUserRequest oidcUserRequest =
                new OidcUserRequest(clientRegistration, userRequest.getAccessToken(),
                        userRequest.getIdToken());

        OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService = new OidcUserService();
        OidcUser oidcUser = null;
        try {
            oidcUser = oidcUserService.loadUser(oidcUserRequest);
        } catch (Exception e) {
            e.printStackTrace();
        }

        ProviderUserRequest providerUserRequest = new ProviderUserRequest(clientRegistration,oidcUser);
        ProviderUser providerUser = providerUser(providerUserRequest);


        selfCertificate(providerUser);

        super.register(providerUser, oidcUserRequest);

        return new PrincipalUser(providerUser);
    }
}

저희는 Oidc 프로토콜을 사용하고, 책임과 분리를 위해 OidcUserService 를 재정의 해줬습니다.

이를 사용하려면 각 Provider 에 맞는 서비스를 제공하고 위 loadUser 메서드를 활용하면 됩니다.

KakaoService

 public SocialLoginResponse kakaoLogin(String code, String redirectUri) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.add(CONTENT_TYPE, "application/x-www-form-urlencoded;charset=utf-8");
        String body = "grant_type=" + authorizationGrantType.getValue() + "&client_id=" + clientId + "&redirect_uri=" + redirectUri + "&code=" + code + "&client_secret=" + clientSecret;
        HttpEntity<String> request = new HttpEntity<>(body, headers);

        String response = restTemplate.postForObject(tokenUri, request, String.class);
        log.info("카카오 로그인 response : {}", response);
        SocialLoginResponse socialLoginResponse = null;
        try {
            socialLoginResponse = getOidcTokenId(response, redirectUri);
        } catch (JsonProcessingException | ParseException e) {
            throw new JwtCreateException(JWT_CREATE_EXCEPTION.getMessage());
        }
        return socialLoginResponse;
    }


    private SocialLoginResponse getOidcTokenId(String response, String redirectUri) throws JsonProcessingException, ParseException {
        JSONObject jsonObject = Json.mapper().readValue(response, JSONObject.class);
        String idToken = jsonObject.get("id_token").toString();
        String oauth2TokenId = jsonObject.get("access_token").toString();
        String scopes = jsonObject.get("scope").toString();
        JWT parse = JWTParser.parse(idToken);
        String subject = parse.getJWTClaimsSet().getSubject();
        OidcIdToken oidcIdToken = OidcIdToken.withTokenValue(idToken)
                .tokenValue(idToken)
                .subject(subject)
                .build();
        OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, oauth2TokenId, null, null);
        String[] scope = scopeParsing(scopes);
        ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(KAKAO.getSocialName())
                .clientId(clientId)
                .clientSecret(clientSecret)
                .authorizationGrantType(authorizationGrantType)
                .redirectUri(redirectUri)
                .scope(scope)
                .tokenUri(tokenUri)
                .clientName(KAKAO.getSocialName())
                .userNameAttributeName("sub")
                .jwkSetUri(jwkSetUri)
                .userInfoUri(userInfoUri)
                .issuerUri(issuerUri)
                .clientAuthenticationMethod(clientAuthenticationMethod)
                .authorizationUri(authorizationUri)
                .build();
        OidcUserRequest oidcUserRequest = new OidcUserRequest(clientRegistration, oAuth2AccessToken, oidcIdToken);
        OidcUser oidcUser = oidcUserService.loadUser(oidcUserRequest);
        Member byUsername = memberService.findByUsername(oidcUser.getName());
        JwtTokenResponse token = jwtTokenProvider.genAccessTokenAndRefreshToken(byUsername);
        boolean baekJoonConnect = false;
        if (byUsername.getBaekJoonName() != null) baekJoonConnect = true;
        return new SocialLoginResponse(token.accessToken(), token.refreshToken(), byUsername.getId(), baekJoonConnect);
    }



    private String[] scopeParsing(String scope) {
        return scope.split(" ");
    }

위 사진의 8번부터 과정입니다. Front-End 에서부터 받은 Kakao code 와 Redirect 정보로
Access Token 발급과 위에서 정의한 OidcService 를 호출하여 회원가입 을 진행합니다.

Front-end 에서 알아야 할 Access Token, Refresh Token, 회원 정보(id, 우리 서비스 연동여부)
를 return 해줍니다.

좀더 생각해보기

지금은 Kakao 만 제공하지만 google, naver 등 다른 서비스에 대한 확장성도 고려해야 한다고 생각합니다.

이를 위해 interface 와 abstract class 를 활용해보며 확장성 과 설계를 해보는 좋은 경험이었습니다.

하지만 OAuth2.0 은 많은곳에서 제공하는것에 비해 OIDC 프로토콜은 지원하지 않는 곳 이 많아 좀 더 고민을 해봐야할 것 같습니다.

0개의 댓글