프로젝트 Motivation - 소셜 로그인

youngkyu MIn·2023년 11월 14일
0

소셜로그인을 꼭 만들어보 싶었는데 오늘에야말로 만들어보자!

나는 초콜릿도 카카오만 먹으니까 카카오로그인을 구현해보자

로그인 템플릿에 적당히 귀엽게 카카오 로그인을 추가해줬다. 썩 귀엽게 생긴 것 같다.


apring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            clientId: (비밀 ^_^)
            scope: profile_nickname, profile_image
            client-name: Kakao
            authorization-grant-type: authorization_code
            redirect-uri: '${custom.site.baseUrl}/{action}/oauth2/code/{registrationId}'
        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me
            user-name-attribute: id

먼저 application.yml 설정이다.
우리 motivation 엔 프로필 이미지가 존재하니 nickname 과 profile image 정도만 받아오자
(KaKao Developers 에서 앱 등록과 Redirect URL 등록 등은 포스팅에서 생략하겠다)


@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    private final MemberService memberService;

    // 카카오톡 로그인이 성공할 때 이 함수가 실행된다.
    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest); // 요청을 토대로 OAuth2 사용자 로드

        String oauthId = oAuth2User.getName();
        Map<String, Object> attributes = oAuth2User.getAttributes(); // OAuth2User 에서  Map 형태로 attribute 가져옴

        Map attributesProperties = (Map) attributes.get("properties"); // attribute 에서 properties 의 value Map 형태로 가져옴
        String nickname = (String) attributesProperties.get("nickname"); // properties 에서 nickname 의 value 가져옴
        String profileImgUrl = (String) attributesProperties.get("profile_image"); // properties 에서 profile_image 의 value 가져옴

        String providerTypeCode = userRequest.getClientRegistration().getRegistrationId().toUpperCase(); // provider 의 Id 대문자로 가져옴 ( KAKAO )

        String username = providerTypeCode + "__%s".formatted(oauthId); // (KAKAO__oauthId)

        Member member = memberService.whenSocialLogin(providerTypeCode, username, nickname, profileImgUrl); // 현재 회원 인지 조회, 없으면 가입

        return new CustomOAuth2User(member.getUsername(), member.getPassword(), member.getGrantedAuthorities());
    }
}

다음은 사용자가 카카오로그인을 통해 인증을 수행한 뒤 redirect 되고 애플리케이션은 URL에 포함된 인증 코드를 사용하여 카카오 서버로부터 사용자 정보를 요청하는 단계이다.

카카오계정의 nickname 과 profile image 의 url 을 얻어왔고 Motivation 에서 사용할 username 을 만들었다.


@Transactional
public Member whenSocialLogin(String providerTypeCode, String username, String nickname, String profileImgUrl) {
    Optional<Member> opMember = findByUsername(username); // providerCode__~~~~ 로 member 조회

    if (opMember.isPresent()) return opMember.get(); // 이미 회원이면 member 객체 return

    // Oauth2User 에서 가져온 profileImgUrl 있으면 실행
    // profileImgUrl 에서 download 해서 temp 에 저장, 그 path 를 return
    String filePath = Ut.str.hasLength(profileImgUrl) ? Ut.file.downloadFileByHttp(profileImgUrl, AppConfig.getTempDirPath()) : "";

    return join(username, "1234", nickname, "", filePath).getData();
}

그 후 회원데이터를 조회하여 이미 존재하면 그 객체를 return 하고, 데이터가 없으면 회원가입을 진행하게 했다.


// fileUrl 에서 파일을 다운로드 하여 filePath(outputDir/tempFileName) 에 저장
public static String downloadFileByHttp(String fileUrl, String outputDir) {
    String originFileName = getFileNameFromUrl(fileUrl); // filename.ext
    String fileExt = getFileExt(originFileName); // ext

    if (fileExt.isEmpty()) {
        fileExt = "tmp";
    }

    new File(outputDir).mkdirs(); // 경로 없으면 생성 (tem Directory)

    String tempFileName = UUID.randomUUID() + ORIGIN_FILE_NAME_SEPARATOR + originFileName + "." + fileExt;
    String filePath = outputDir + "/" + tempFileName;

    // 'try-with-resources' 문을 사용하여 파일 출력 스트림을 생성
    // 이렇게 하면 스트림이 사용 후 자동으로 닫음
    try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {

        // 주어진 파일 URL에서 입력 스트림을 열어 읽기 가능한 바이트 채널을 생성
        ReadableByteChannel readableByteChannel = Channels.newChannel(new URI(fileUrl).toURL().openStream());

        // FileOutputStream 객체에서 파일 채널을 가져옴
        FileChannel fileChannel = fileOutputStream.getChannel();

        // readableByteChannel에서 데이터를 읽어와 fileChannel을 통해 파일에 기록
        // 시작 위치는 0이며, Long.MAX_VALUE는 최대 복사 가능한 바이트 수를 나타냄
        // 실제로는 EOF(End-Of-File) 또는 파일의 크기에 도달할 때까지 데이터를 복사
        fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);

    } catch (Exception e) {
        throw new DownloadFileFailException();
    }

    File file = new File(filePath);

    if (file.length() == 0) {
        throw new DownloadFileFailException();
    }

    if (fileExt.equals("tmp")) {
        String ext = getFileExt(file);

        if (ext == null || ext.isEmpty()) {
            throw new DownloadFileFailException();
        }

        String newFilePath = filePath.replace(".tmp", "." + ext);
        moveFile(filePath, newFilePath);
        filePath = newFilePath;
    }

    return filePath;
}

다음은 카카오로 부터 얻어 온 profile image 의 url 을 통해 profile image 를 다운받아 서버의 로컬환경에 저장하고 있다.


!여기서 잠깐!

여기서 발생한 이슈가 있다.

Motivation 의 서버에는 다양한 경로로 원본 파일이 전해지고있다.
리소스 url 을 통해 파일이 전해지기도 하고, MultipartFile 객체로 파일 자체가 전해지기도 한다. 혹은 url 에 접근해 서버가 파일을 다운로드 해야 하는 경우도 있다.
이를 위해 앞서 만들었던 GenFileService 를 통해 어떤 방식으로 파일을 전해받아도 일단은 Temp 경로에 파일을 저장하도록 했다. 그 후, 파일이 저장 된 temp 경로를 통해 모든 로직이 실행될 수 있도록 만들어줬다. 물론 temp 경로의 원본파일이 사용되면 실제 존재해야 하는 위치로 파일을 이동시키고 temp 위치의 파일은 삭제 해주고있다.

!이슈 끝!

우리의 회원가입도 현재 2가지 경우가 존재한다.

일반 회원가입과정에서 프로필이미지의 원본파일이 MultipartFile 객체로 전해지는경우,
소셜 로그인 과정에서 프로필이미지가 url 로 전해진 경우

두 경우 모두 일단 Temp 경로에 원본 이미지파일을 다운로드하고 그 경로를 이용해 서버가 파일에 접근하여 회원가입을 진행하게 했다.


카카오 로그인 페이지로 잘 이동되는 모습이다


실제 로그인을 통해 확인해보면

내 카카오톡 프로필 상의 별명 (민영규) 와 프로필 이미지 (기본 이미지) 가 잘 등록된 모습이다!

우리의 Motivation, 이제 소셜 로그인도 지원한다! ㅎㅎ

profile
한 줄 소개

0개의 댓글