티스토리 뷰

스프링 부트 REST API WEB 프로젝트

깃헙 링크

https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp

수행 목록

  1. 환경구성 및 helloworld 출력
  2. H2 DB 연동
  3. Swagger API 문서 연동
  4. REST API 설계
  5. RestControllerAdvice를 이용한 통합 예외 처리
  6. Entity - DTO 분리
  7. MessageSource를 이용해 예외 메시지 다국화
  8. JPA Aduting을 이용해 객체 생성시간/수정시간 적용
  9. 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
  10. 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
  11. Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
  12. JUnit Test (단위 테스트)
  13. JUnit Test (통합 테스트)
  14. OAuth 2.0 정리
  15. OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
  16. OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
  17. OAuth 2.0 카카오 로그인 테스트 검증
  18. 환경별 설정을 위해서profile 분리하기 

1. 카카오가 준 Token을 이용해서 회원가입 / 로그인 구현

AccessToken으로 로그인할 경우

  1. AccessToken으로 카카오 API서버의 profile api에 사용자 정보 요청
  2. 해당 카카오 정보를 통해 나의 서비스에 가입되어있는지 확인
    • 가입되어 있지 않았다면 로그인 실패
    • 가입되어 있다면 JWT 발급

AccessToken으로 가입할 경우

  1. AccessToken으로 카카오 API서버에 profile api를 통해 사용자 정보 요청
  2. 해당 카카오 정보를 통해 나의 서비스에 가입되어있는지 확인
    • 가입되어 있지 않았다면 새로 가입시키고 JWT 발급
    • 이미 가입되어 있다면 가입 실패, 로그인 하도록 함

 

User Entity - Social 가입이 가능하도록 수정

UserEntity에 OAuth Social Login 제공자를 구분하기 위해 provider 값을 추가한다. 또한 OAuth로 로그인 / 가입시 패스워드는 따로 필요없으므로 null을 허용한다.

public class User extends BaseTimeEntity implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Column(length = 100) // nullable을 켬
    private String password;

    @Column(nullable = false, unique = true, length = 30)
    private String email;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, length = 20)
    private String nickName;

    @Column(length = 100) // provider 추가 (kakao, naver, google etc.)
    private String provider;
    
    ...
}

 

UserJapRepo에 provideremail 두가지 값을 충족하는 회원을 조회하는 메서드를 추가.

Optional<User> findByEmailAndProvider(String email, String provider);

 

2. 사용자 정보 가져오기

👉 사용자가 로그인을 했고, 토큰값도 받았다. 이제 이 토큰값으로 사용자의 정보를 카카오 서버에 요청한다.

 

 

2.1 사용자 정보 가져오기 요청 보내기 : KakaoService.class - getKakaoProfile(액세스 토큰)

카카오 프로필 요청 메소드
    @Value("${url.base}")
    private String baseUrl;

    @Value("${social.kakao.client-id}")
    private String kakaoClientId;

    @Value("${social.kakao.redirect}")
    private String kakaoRedirectUri;

    public KakaoProfile getKakaoProfile(String kakaoAccessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set("Authorization", "Bearer " + kakaoAccessToken);

        String requestUrl = env.getProperty("social.kakao.url.profile");
        if (requestUrl == null) throw new CCommunicationException();

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(null, headers);
        try {
            ResponseEntity<String> response = restTemplate.postForEntity(requestUrl, request, String.class);
            if (response.getStatusCode() == HttpStatus.OK)
                return gson.fromJson(response.getBody(), KakaoProfile.class);
            log.error("header : " + response.getHeaders());
        } catch (Exception e) {
            log.error(e.toString());
            throw new CCommunicationException();
        }
        throw new CCommunicationException();
    }

restTemplate의 postForEntity()로 헤더에 액세스 토큰을 포함해서 POST 요청을 보낸다.

  • 카카오는 응답을 Json형식으로 아래와 같이 보내준다.

 

Response JSON에 properiteskakao_account에 json객체가 또 있는데 뜯어보면

{
    "id":123456789,
    "connected_at":"2021-08-22T15:22:52Z",
    "properties":
        {
            "nickname":"최운식"
        },
    "kakao_account":
        {
            "profile_nickname_needs_agreement":false,
            "profile":{"nickname":"최운식"},
            "has_email":true,
            "email_needs_agreement":false,
            "is_email_valid":true,
            "is_email_verified":true,
            "email":"groom@kakao.com"
        }
}

이런식으로 많은 데이터가 들어있다. 현재는 필요한 값이 [id, nickname, email] 이므로 해당 값만 담을 KakaoProfile 객체를 만들어 준다.

 

카카오 프로필 매핑용 DTO 클래스
@Getter
public class KakaoProfile {
    private Long id;
    private Properties properties;
    private KakaoAccount kakao_account;

    @Getter
    @ToString
    public static class KakaoAccount {
        private String email;
    }

    @Getter
    @ToString
    public static class Properties {
        private String nickname;
    }
}

👉 카카오가 전달해주는 Json 형식과 동일하게 만들어준다.

 

2.2 카카오로 회원가입 구현 - 사용자 정보 가져오기 메소드 사용

이제 위에서 구현한 사용자 정보 가져오기를 통해 KakaoProfile 객체를 만들 수 있다. 해당 객체에는 사용자 정보가 들어있으므로 해당 정보로 회원가입을 한다.

    @ApiOperation(
            value = "소셜 회원가입 - kakao",
            notes = "카카오로 회원가입을 합니다."
    )
    @PostMapping("/social/signup/kakao")
    public CommonResult signupBySocial(
            @ApiParam(value = "소셜 회원가입 dto", required = true)
            @RequestBody UserSocialSignupRequestDto socialSignupRequestDto) {

        // 카카오에게서 사용자 정보 요청
        KakaoProfile kakaoProfile =
                kakaoService.getKakaoProfile(socialSignupRequestDto.getAccessToken());
        if (kakaoProfile == null) throw new CUserNotFoundException();
        if (kakaoProfile.getKakao_account().getEmail() == null) {
            kakaoService.kakaoUnlink(socialSignupRequestDto.getAccessToken());
            throw new CSocialAgreementException();
        }

        Long userId = signService.socialSignup(UserSignupRequestDto.builder()
                .email(kakaoProfile.getKakao_account().getEmail())
                .name(kakaoProfile.getProperties().getNickname())
                .nickName(kakaoProfile.getProperties().getNickname())
                .provider("kakao")
                .build());

        return responseService.getSingleResult(userId);
    }
  • 사용자 정보를 가져오지 못했다면 (== null) : 토큰에러이므로 가입 중단
  • 사용자의 이메일을 가져오지 못했다면 : 이메일 정보 제공을 거절했으므로 가입 중단 -> 해당 사용자와 우리 서비스의 연결을 끊는다.

이 외의 경우 가져온 프로필 정보를 통해 소셜 회원가입을 진행한다.

 

소셜 회원가입을 위한 메소드 추가 구현 : SignService.class - socialSignup()

    @Transactional // 기존 회원가입
    public Long signup(UserSignupRequestDto userSignupDto) {
        if (userJpaRepo.findByEmail(userSignupDto.getEmail()).isPresent())
            throw new CEmailSignupFailedException();
        return userJpaRepo.save(userSignupDto.toEntity(passwordEncoder)).getUserId();
    }

    @Transactional // 소셜 회원가입 - 패스워드가 필요없으므로 따로 만듬
    public Long socialSignup(UserSignupRequestDto userSignupRequestDto) {
        if (userJpaRepo
                .findByEmailAndProvider(userSignupRequestDto.getEmail(), userSignupRequestDto.getProvider())
                .isPresent()
        ) throw new CUserExistException();
        return userJpaRepo.save(userSignupRequestDto.toEntity()).getUserId();
    }
  • 카카오에서 받은 이메일 + provider (kakao)로 가입된 회원이 존재하지 않는다면 저장한다.
  • 존재하면 이미 가입된 유저이므로 예외를 발생시킨다. (로그인하도록 요청)

 

소셜 회원가입을 위해 UserSignupRequestDto에 toEntity() 메소드 추가 구현

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserSignupRequestDto {
    private String email;
    private String password;
    private String name;
    private String nickName;
    private String provider;

    public User toEntity(PasswordEncoder passwordEncoder) {
        return User.builder()
                .email(email)
                .password(passwordEncoder.encode(password))
                .nickName(nickName)
                .name(name)
                .roles(Collections.singletonList("ROLE_USER"))
                .build();
    }

    public User toEntity() {
        return User.builder()
                .email(email)
                .nickName(nickName)
                .name(name)
                .provider(provider)
                .roles(Collections.singletonList("ROLE_USER"))
                .build();
    }
}
  • 소셜 회원가입은 패스워드가 필요없으므로 passwordEncoder없이 가입할 수 있도록 함
  • Dto 객체 프로퍼티에 provider를 추가

User객체를 UserJpaRepo에 저장하게 됨으로서 소셜 회원가입을 완료한다.

 

2.3 카카오로 로그인 구현 - 사용자 정보 가져오기 메소드 사용

회원가입은 됐으니 이제 카카오에게서 사용자 정보 가져오기를 요청해서 로그인을 하도록 구현한다.

  • 가져온 사용자 정보의 이메일 + 제공자 (kakao)로 존재하는 유저가 없다면 로그인 실패
  • 존재한다면 JWT를 발급시켜준다. 이제 해당 유저는 Authentication된 유저가 되고 우리 서비스의 AccessToken을 발급받아서 해당 토큰으로 서비스를 요청할 수 있게된다.
    @ApiOperation(
            value = "소셜 로그인 - kakao",
            notes = "카카오로 로그인을 합니다.")
    @PostMapping("/social/login/kakao")
    public SingleResult<TokenDto> loginByKakao(
            @ApiParam(value = "소셜 로그인 dto", required = true)
            @RequestBody UserSocialLoginRequestDto socialLoginRequestDto) {

        KakaoProfile kakaoProfile = kakaoService.getKakaoProfile(socialLoginRequestDto.getAccessToken());
        if (kakaoProfile == null) throw new CUserNotFoundException();

        User user = userJpaRepo.findByEmailAndProvider(kakaoProfile.getKakao_account().getEmail(), "kakao")
                .orElseThrow(CUserNotFoundException::new);
        return responseService.getSingleResult(jwtProvider.createTokenDto(user.getUserId(), user.getRoles()));
    }

 

2.4 카카오 유저와의 연결 끊기

회원이 정보 제공 동의를 제대로 안해줘서 정상적인 값을 받지 못하거나, 탈퇴 등의 이유로 연결을 끊어야 할 필요가 있다. 이를 위해 unlink를 구현한다.

  • kakaoService.kakaoUnlink(socialSignupRequestDto.getAccessToken());
  • 회원가입 구현부에서도 kakaoProfile의 email이 null이면 unlink하도록 하고 있다.

카카오 유저와 연결 끊기
    public void kakaoUnlink(String accessToken) {
        String unlinkUrl = env.getProperty("social.kakao.url.unlink");
        if (unlinkUrl == null) throw new CCommunicationException();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set("Authorization", "Bearer " + accessToken);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(null, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(unlinkUrl, request, String.class);

        if (response.getStatusCode() == HttpStatus.OK) {
            log.info("unlink " + response.getBody());
            return;
        }
        throw new CCommunicationException();
    }

 

3. [카카오 로그인 -> 인가코드 -> 액세스 토큰 -> 사용자 정보 -> 회원가입 -> 로그인] 테스트

[카카오 로그인 버튼]

[카카오 로그인]

[카카오 Token 정보]

[카카오 AccessToken으로 사용자 정보 받아와서 회원가입]

[카카오 AccessToken으로 사용자 정보 받아와서 로그인]

[로그인 한 사용자에게 발급한 JWT의 AccessToken으로 전체유저조회 API 요청]

카카오 정보로 회원가입되어있는 것을 확인할 수 있다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday