티스토리 뷰
spring boot REST API Web 프로젝트 (12 - 3) - OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
구름뭉치 2021. 8. 31. 14:52스프링 부트 REST API WEB 프로젝트
깃헙 링크
https://github.com/choiwoonsik/springboot_RestApi_App_Project/tree/main/restApiSpringBootApp
수행 목록
- 환경구성 및 helloworld 출력
- H2 DB 연동
- Swagger API 문서 연동
- REST API 설계
- RestControllerAdvice를 이용한 통합 예외 처리
- Entity - DTO 분리
- MessageSource를 이용해 예외 메시지 다국화
- JPA Aduting을 이용해 객체 생성시간/수정시간 적용
- 스프링 시큐리티 + Jwt를 이용해서 인증 및 권한 체크
- 스프링 시큐리티 AuthenticationEntryPoint, AccessDenied로 인증 및 인가 예외처리
- Jwt AccessToken + RefreshToken으로 보안성과 사용자 편의성 고도화하기
- JUnit Test (단위 테스트)
- JUnit Test (통합 테스트)
- OAuth 2.0 정리
- OAuth 2.0 카카오 로그인 part.1 Authorization code + Token 발급
- OAuth 2.0 카카오 로그인 part.2 토큰으로 회원 가입 / 로그인
- OAuth 2.0 카카오 로그인 테스트 검증
- 환경별 설정을 위해서profile 분리하기
1. 카카오가 준 Token을 이용해서 회원가입 / 로그인 구현
AccessToken으로 로그인할 경우
- AccessToken으로 카카오 API서버의 profile api에 사용자 정보 요청
- 해당 카카오 정보를 통해 나의 서비스에 가입되어있는지 확인
- 가입되어 있지 않았다면 로그인 실패
- 가입되어 있다면 JWT 발급
AccessToken으로 가입할 경우
- AccessToken으로 카카오 API서버에 profile api를 통해 사용자 정보 요청
- 해당 카카오 정보를 통해 나의 서비스에 가입되어있는지 확인
- 가입되어 있지 않았다면 새로 가입시키고 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에 provider와 email 두가지 값을 충족하는 회원을 조회하는 메서드를 추가.
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에 properites와 kakao_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 요청]
카카오 정보로 회원가입되어있는 것을 확인할 수 있다.
'스프링 > 스프링부트 RestAPI 프로젝트' 카테고리의 다른 글
- Total
- Today
- Yesterday