[OAuth2.0] Spring Security 없이 Oauth2.0 소셜 로그인 구현

SJ.CHO·2024년 11월 3일

요구조건

  • 이번 내배캠 챌린지반 첫과제로 소셜로그인을 Spring Security 를 사용하지않고 구현하기 과제가 발제가 되었다 (와! 과제가 두배!)

  • 카카오의 소셜로그인 구조는 크게 3단계로
  1. APP 인가코드 발급
  2. 긴가코드를 활용한 액세스토큰 발급
  3. 액세스토큰을 이용한 사용자(로그인유저) 정보 받아오기로 구성된다.

APP 인가코드 발급

https://developers.kakao.com/

  • 우선적으론 카카오 API를 이용하기 위해선 카카오 Developers에서 APP 등록이 필요하다.

  • 카카오로그인을 활성해준다.

  • 동의 항목들을 활성화 해주자

  • 발급된 키들을 기억해둔다 (외부의 노출되서는 안된다!)

  • 사이트 도메인 및 리다이렉트 URI를 등록해준다. 프론트는 구현이 안되기에 도메인은 실질적으로 하는일은 없다.

  • 리다이렉트 URI 는 사용자가 카카오인증을 끝내고 인가코드를 전달받는 URI 로 APP이 카카오 서버의 접근이 가능한 인가코드 를 받을 수 있는 URI로 리다이렉트된다.

public class AuthController {
  @Value("${oauth2.kakao.client-secret}")
  private String kakaoKey;

  @GetMapping("/{provider}/login")
  public RedirectView socialLogin(@PathVariable String provider) {
    if ("kakao".equals(provider)) {
      String redirectUrl =
          "https://kauth.kakao.com/oauth/authorize"
              + "?client_id="
              + kakaoKey
              + "&redirect_uri="
              + "http://localhost:8080/api/oauth2/kakao/callback"
              + "&response_type=code";
      return new RedirectView(redirectUrl);
    }
    throw new IllegalArgumentException("Unsupported provider: " + provider);
  }
}

  • 사용자가 엔트포인트 /api/oauth2/kakao/login 에 접근하면 해당 메서드가 호출
  • 사용자가 카카오의 인증페이지로 리다이렉트 시켜주는 URL 생성 및 RedirectView 를 통해 브라우저가 카카오 로그인페이지로 이동하도록 한다.
  • 사용자가 카카오 로그인페이지를 통해 로그인을 완료하면 인가 코드 와 함께 http://localhost:8080/api/oauth2/kakao/callback URL 로 리다이렉트 시켜준다.
  • 조건문을 이용한 이유는 이후 구글 네이버 등 다양한 provider 확장성이 필요할시 유지보수가 용이하도록 코드작성

AccessToken 받기

  • 인가코드 만으로는 카카오 로그인을 완료하지못한다. 우리는 인가코드를 통해 APP이 카카오서버의 접근가능한 권한만을 얻은것이지 해당유저의 정보등을 가져오는 API 호출에 필요한 액세스토큰 이 필요하다.

  • 카카오 측 API에서 요구하는 값들을 HTTP Headers 에 담아서 보내줄 필요가 있다.
 private String getKakaoAccessToken(String code) throws JsonProcessingException {
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    body.add("grant_type", "authorization_code");
    body.add("client_id", kakaoKey);
    body.add("redirect_uri","http://localhost:8080/api/oauth2/kakao/callback");
    body.add("code", code);
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
    ResponseEntity<String> response = restTemplate.postForEntity("https://kauth.kakao.com/oauth/token", request, String.class);

    ObjectMapper mapper = new ObjectMapper();
    JsonNode node = mapper.readTree(response.getBody());
    return node.get("access_token").asText();
  }
  • HttpEntity 를 생성해 카카오가 요구하는 값들을 MultiValueMap 의 형태로 담아준다.
  • restTemplate 을 통해 완성된 HttpEntity 를 post 방식으로 카카오 인증서버인 https://kauth.kakao.com/oauth/token 로 전달 이후 String 타입으로 값을 받음.
  • 반환된 본문을 ObjectMapper 를 통해 String->JSON 변환하여 내부에 있는 access_token 을 추출한다.

  • 정상적으로 액세스 토큰을 반환 받은 것 을 확인 가능하다.

사용자 정보 받아오기

  • 해당 액세스토큰을 통해 카카오로그인을 실행한 유저의 정보를 받아오자.

@Getter
@Builder
@NoArgsConstructor
public class KakaoUserInfoDto {
    private Long id;
    private String nickname;
    private String email;

    public KakaoUserInfoDto(Long id, String nickname, String email) {
        this.id = id;
        this.nickname = nickname;
        this.email = email;
    }
}
  • 카카오 로그인 유저의 정보를 담기위한 DTO 클래스를 생성해준다.
  private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + accessToken);
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    RequestEntity<MultiValueMap<String, String>> requestEntity =
        RequestEntity.post("https://kapi.kakao.com/v2/user/me")
            .headers(headers)
            .body(new LinkedMultiValueMap<>());

    ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    Long id = jsonNode.get("id").asLong();
    String nickname = jsonNode.get("properties").get("nickname").asText();
    String email = jsonNode.get("kakao_account").get("email").asText();
    return new KakaoUserInfoDto(id, nickname, email);
  }
  • 전체적인 구성은 액세스 토큰을 받아오는 방식과 비슷하다. 카카오가 요구하는 헤더에 발급받은 accessToken 을 담아 URI 를 통해 접근 한다.
{
  "id": 1632335751,
  "properties": {
    "nickname": "르탄이",
    "profile_image": "http://k.kakaocdn.net/...jpg",
    "thumbnail_image": "http://k.kakaocdn.net/...jpg"
  },
  "kakao_account": {
    "profile_needs_agreement": false,
    "profile": {
      "nickname": "르탄이",
      "thumbnail_image_url": "http://k.kakaocdn.net/...jpg",
      "profile_image_url": "http://k.kakaocdn.net/...jpg"
    },
    "has_email": true,
    "email_needs_agreement": false,
    "is_email_valid": true,
    "is_email_verified": true,
    "email": "letan@sparta.com"
  }
}
  • 해당 형태의 사용자 정보 JSON 에서 필요한값들만을 추출하여 KakaoUserInfoDto 를 통해 데이터를 담아준다.

회원가입 처리

  • 회원가입의 경우 기존 회원가입 처리와 동일한 양상으로 흘러간다.
  private Member registerSocialMemberIfNeeded(UserInfoDto userInfo) {
    Member socialMember = memberRepository.findByProviderId(userInfo.getId()).orElse(null);
    if (socialMember != null) {
      return socialMember;
    }
    if (memberRepository.existsByEmail(userInfo.getEmail())) {
      throw new IllegalArgumentException("이메일이 이미 등록되어 있습니다. 다른 소셜 로그인으로 로그인해주세요.");
    }
    Member member =
        Member.builder()
            .email(userInfo.getEmail())
            .providerId(userInfo.getId())
            .provider(userInfo.getProvider())
            .nickname(userInfo.getNickname())
            .build();
    memberRepository.save(member);
    return member;
  }
  • 기존의 이미 소셜로그인을 통해 회원가입처리가 된 유저라면 정보를 그대로 반환.
  • 회원가입이 필요한 첫 로그인 유저라면 DB에 등록해주는 절차를 밟는다.
  • 예외의 경우 기존의 소셜로그인을 통해 회원가입이 완료되었지만, 다른 소셜 플랫폼을 통해 동일 이메일로 로그인의 경우를 차단해주었다. (별개의 ROW를 작성하여 동일 ID 의 다른 provider 컬럼으로 여러 ID 의 처리도 가능하다)


참조 :
https://devlopsquare.tistory.com/188
https://kangcoder.tistory.com/7
https://velog.io/@wlsgur1533/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%97%86%EC%9D%B4-OAuth-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#10-2-naver-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%98%B8%EC%B6%9C


배운 점

  • 외부 API 요청 과 활용 방법.
  • 외부 API를 활용할 때 외부문서를 읽는 방법.

TODO

  • 로그인시 JWT 토큰을 추가적으로 발행하여 로그인상태 유지하기.
  • 객체지향적 프로그래밍을 통한 여러소셜로그인 기능을 작동시킬 수 있는 추상화.
profile
70살까지 개발하고싶은 개발자

0개의 댓글