SpringBoot 소셜 로그인 구현하기 (with. SpringSecurity, JWT, OAuth)

신준호·2023년 11월 3일
0
post-thumbnail

이번 포스팅은 싸피 공통 프로젝트에서 SpringSecurity 환경에서 OAuth 방식으로 JWT을 주고 받으며 RESTful로 카카오와 네이버 로그인을 구현했다. 구현한 과정을 정리해보자! 위 사진은 프로젝트에서 로그인 화면이다.

카카오 로그인

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

위 주소는 카카오 로그인 api와 관한 문서다.

인가 코드 받기

처음에 카카오 인가 코드를 받아야 한다.
우리가 흔히 보는 카카오 로그인 창에서 이메일과 비밀번호를 입력하면 카카오에서 redirect_url로 주소로 보내고 url 끝에 code = xxxxxxxxxx 값을 준다.

code 값을 Spring으로 던져주면 Controller에서 code 값을 받고 시작하면 된다.

Controller

 @Operation(summary = "카카오로 로그인 및 회원가입", description = "카카오로 로그인 및 회원가입 하는 API")
    @PostMapping("/api/members/kakao/login")
    public ResponseEntity<LoginResponseDto> loginKakao(@RequestBody LoginRequestDto codeRequest) {
        LoginDto member = memberService.findKakaoMemberByAuthorizedCode(codeRequest.getCode(), RedirectUrlProperties.KAKAO_REDIRECT_URL);

        String accessToken = jwtTokenProvider.createAccessToken(member.getMemberId(), member.getSocialId(), member.getSocialType());
        String refreshToken = jwtTokenProvider.createRefreshToken(member.getMemberId());
        jwtTokenProvider.storeRefreshToken(member.getMemberId(), refreshToken);

        return ResponseEntity.ok()
                .header("Authorization", accessToken)
                .header("Authorization-Refresh",refreshToken)
                .body(LoginResponseDto.builder()
                        .message("카카오 로그인을 성공하셨습니다")
                        .memberId(member.getMemberId())
                        .nickname(member.getNickname())
                        .firstLogin(member.isFirstLogin())
                        .build());
    }

로직

  • code값을 가지고 memberservice에서 kakao에서 인증을 받고 accesstoken을 받는다.
  • accesstoken을 가지고 카카오 사용자 정보를 가져와서 우리 서비스의 db에 저장시킨다.
  • jwtTokenProvicer에서 accesstoken과 refreshtoken을 만든다.
  • refreshtoken을 redis에 저장시킨다.
  • front한테 accesstoken과 refreshtoken을 header에 담아서 보낸다.

Service

 @Transactional
    public LoginDto findKakaoMemberByAuthorizedCode(String authorizedCode, String redirectUri) {
        // 카카오 OAuth2 를 통해 카카오 사용자 정보 조회
        KakaoMemberDto kakaoUserDto = kakaoOAuth2.getMemberInfo(authorizedCode, redirectUri);
        String email = kakaoUserDto.getEmail();

        String socialId = kakaoUserDto.getSocialId();
        Optional<Member> optionalMember = memberRepository.findBySocialId(socialId);

        if(optionalMember.isPresent()){

            return LoginDto.builder()
                    .memberId(optionalMember.get().getMemberId())
                    .socialId(optionalMember.get().getSocialId())
                    .socialType(optionalMember.get().getSocialType())
                    .nickname(optionalMember.get().getNickname())
                    .firstLogin(false)
                    .build();
        }
        // 가입된 유저가 아니라면 회원가입 진행
        else {
            String profilePath = kakaoUserDto.getProfilePath();

            Member member = Member.builder()
                    .role(Role.USER)
                    .email(email)
                    .nickname(UUID.randomUUID()+"hello")
                    .profilePath(null)
                    .socialId(socialId)
                    .socialType(Social.KAKAO)
                    .build();
            Member saveMember = memberRepository.save(member);
            ShowInfo showInfo = ShowInfo.builder()
                    .member(saveMember)
                    .build();
            showInfoRepository.save(showInfo);

            return  LoginDto.builder()
                    .memberId(member.getMemberId())
                    .socialId(member.getSocialId())
                    .socialType(member.getSocialType())
                    .nickname(member.getNickname())
                    .firstLogin(true)
                    .build();
        }
    }

로직

  • code와 redirect_url을 getMemberInfo에서 처리하여 kakaoUserDto에 카카오 사용자 정보를 return한다.
  • kakaoUserDto 정보를 db에 저장한다.

getMemberInfo

public KakaoMemberDto getMemberInfo(String authorizedCode, String redirectUri) {
        // 1. 인가코드 -> 액세스 토큰
        String accessToken = getAccessToken(authorizedCode, redirectUri);
        // 2. 액세스 토큰 -> 카카오 사용자 정보
        return getMemberByAccessToken(accessToken);
    }

로직

  • 인가코드와 redirect_uri를 통해 getAccessToken 함수에서 엑세스 토큰을 kakao에서 가져온다.
  • 엑세트 토큰을 이용하여 getMemberByAccessToken 함수에서 카카오 사용자 정보를 가져온다.

getAccessToken

private String getAccessToken(String authorizedCode, String redirectUri) {
        // HttpHeader 오브젝트 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HttpBody 오브젝트 생성
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("client_id", clientId);
        params.add("redirect_uri", redirectUri);
        params.add("code", authorizedCode);

        // HttpHeader와 HttpBody를 하나의 오브젝트에 담기
        RestTemplate rt = new RestTemplate();
        HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest =
                new HttpEntity<>(params, headers);
        // Http 요청
        ResponseEntity<String> response = rt.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                kakaoTokenRequest,
                String.class
        );
        // JSON -> 액세스 토큰 파싱
        String tokenJson = response.getBody();
        JSONObject json = new JSONObject(tokenJson);
        return json.getString("access_token");
    }

로직

  • 코드와 redirect_uri등 필요한 파라미터를 카카오에게 Http요청을 통해서 를 response에 받는다.
  • resposne를 파싱을 하면 accesstoken를 갖고 올 수 있다.

getMemberByAccessToken

private KakaoMemberDto getMemberByAccessToken(String accessToken) {
        JSONObject body = requestMemberInfoJsonObject(accessToken);
        System.out.println("body = " + body);
        // 유저 정보 파싱

        JSONObject kakaoAccount = body.getJSONObject("kakao_account");
        JSONObject profile = kakaoAccount.getJSONObject("profile");
        String email = kakaoAccount.getString("email");
        String nickname = body.getJSONObject("properties").getString("nickname");
        String profilePhoto = profile.getString("profile_image_url");


        return KakaoMemberDto.builder()
                .socialId(Long.toString((Long) body.get("id")))
                .email(email)
                .name(nickname)
                .profilePath(profilePhoto)
                .build();
    }

로직

  • 카카오 엑세스 토큰을 가지고 requestMemberInfoJsonObject 함수를 통해서 카카오 사용자 정보를 json 형태로 가져온다
  • 카카오 사용자 정보을 필요한 형태로 바꾸고 KakaoMemberDto에 담는다

requestMemberInfoJsonObject

private JSONObject requestMemberInfoJsonObject(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HttpHeader와 HttpBody를 하나의 오브젝트에 담기
        RestTemplate rt = new RestTemplate();
        HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers);

        // Http 요청하기
        ResponseEntity<String> response = rt.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.POST,
                kakaoProfileRequest,
                String.class
        );

로직

  • 엑세스 토큰을 가지고 카카오에게 Http 요청을 보내서 카카오 사용자 정보를 가져온다.

JwtTokenProvider

public String createAccessToken(Integer id, String userPk, Social socialType) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
        claims.put("id", id);
        claims.put("socialType",socialType);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    public String createRefreshToken(Integer id) {

        Date now = new Date();
        return Jwts.builder()
                .setId(Integer.toString(id)) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }
    
    public void storeRefreshToken(int id, String refreshToken) {
        Member member = memberRepository.findById(id).orElse(null);
        if (member != null) {
            redisTemplate.opsForValue().set(
                    Integer.toString(id),
                    refreshToken,
                    refreshTokenValidTime,
                    TimeUnit.MILLISECONDS

            );
        }
    }

다시 controller로 돌아와서 카카오 사용자 정보 Dto를 return 받았고, JwtTokenProvider에서 엑세스 토큰과 리프레시 토큰을 생성한다.
생성한 리프레시 토큰은 redis에 저장시킨다.
생성한 엑세스 토큰과 리프레시 토큰은 header에 담아서 client에게 응답해준다.

이렇게 카카오 소셜 로그인 구현을 완료했고 네이버도 똑같이 구현했다.
다음 포스팅은 리프레시 토큰을 이용해서 토큰을 재발급 받는 과정을 정리해보자!!

profile
개발 공부 일지

0개의 댓글