Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: 카카오 로그인 구현 #172

Merged
merged 1 commit into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers("/members/sign-up", "/members/sign-up/check-email", "/members/sign-up/check-nickname").permitAll()
.requestMatchers("/members/login", "members/logout", "members/posts").permitAll()
.requestMatchers("/members/**").permitAll()
.requestMatchers("oauth/login/**").permitAll()
// Post 관련 접근
.requestMatchers("/posts","/posts/{postId}", "/posts/visibility/{postId}", "/posts/coauthors/{postId}", "/posts/categories/{postId}").permitAll()
.requestMatchers("/posts/title/paging", "/posts/team/{teamId}/{postId}", "/posts/team/{teamId}/member/{memberId}/paging", "/posts/project/{projectId}/team/{teamId}/paging", "/posts/project/{projectId}/member/{memberId}/paging", "/posts/member/{memberId}/paging", "/posts/categories/paging", "/posts/{postId}/adjacent").permitAll()
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/codiary/backend/global/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ public TokenInfo generateToken(Authentication authentication, Long memberId) {
.build();
}

// 이메일로 토큰 생성
public TokenInfo generateToken(String email, Long memberId) {
long now = (new Date()).getTime();

// Access Token 생성
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
String accessToken = Jwts.builder()
.setSubject(email)
.claim(AUTHORITIES_KEY, "ROLE_USER")
.claim("memberId", memberId)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

// Refresh Token 생성
String refreshToken = Jwts.builder()
.setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

// 생성된 토큰 정보를 반환
return TokenInfo.builder()
.grantType(BEARER_TYPE)
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}


// 토큰을 복호화하여 정보 추출
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.codiary.backend.global.service.MemberService;

import com.codiary.backend.global.apiPayload.ApiResponse;
import com.codiary.backend.global.apiPayload.code.status.SuccessStatus;
import com.codiary.backend.global.domain.entity.Member;
import com.codiary.backend.global.jwt.JwtTokenProvider;
import com.codiary.backend.global.jwt.TokenInfo;
import com.codiary.backend.global.repository.MemberRepository;
import com.codiary.backend.global.web.dto.Member.MemberResponseDTO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
public class SocialLoginService {

private final MemberRepository memberRepository;
private final JwtTokenProvider jwtTokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;

@Value("${kakao.redirect.url}")
private String kakaoRedirectUrl;

@Value("${kakao.cliend.id}")
private String kakaoClientId;


public String getRedirectUrl() {
String path = "https://kauth.kakao.com/oauth/authorize?response_type=code";
String clientId = "&client_id=" + kakaoClientId;
String redirectUrl = "&redirect_uri=" + kakaoRedirectUrl;

return path + clientId + redirectUrl;
}

public ApiResponse<MemberResponseDTO.MemberTokenResponseDTO> kakaoLogin(String code) {

String kakaoAccessToken = getKakaoToken(code);
String userEmail = getKakaoUserEmail(kakaoAccessToken);

if (!memberRepository.existsByEmail(userEmail)) {
Member member = Member.builder()
.email(userEmail)
.password("")
.nickname(userEmail)
.birth(new LocalDate().toString())
.gender(Member.Gender.Male)
.github("")
.linkedin("")
.discord("")
.image(null)
.build();
memberRepository.save(member);
}
Member member = memberRepository.findByEmail(userEmail).get();


TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getEmail(), member.getMemberId());

return ApiResponse.of(SuccessStatus.MEMBER_OK, MemberResponseDTO.MemberTokenResponseDTO.builder()
.email(member.getEmail())
.nickname(member.getNickname())
.tokenInfo(tokenInfo)
.memberId(member.getMemberId())
.build());
}

private String getKakaoToken(String code) {
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", kakaoClientId);
body.add("redirect_uri", kakaoRedirectUrl);
body.add("code", code);

HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(body, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);

String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode;
try {
jsonNode = objectMapper.readTree(responseBody);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

return jsonNode.get("access_token").asText();
}


private String getKakaoUserEmail(String kakaoAccessToken) {

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + kakaoAccessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoUserInfoRequest,
String.class
);

String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode;
try {
jsonNode = objectMapper.readTree(responseBody);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

Long id = jsonNode.get("id").asLong();
String email = jsonNode.get("kakao_account").get("email").asText();

return email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.codiary.backend.global.web.controller;

import com.codiary.backend.global.apiPayload.ApiResponse;
import com.codiary.backend.global.apiPayload.code.status.SuccessStatus;
import com.codiary.backend.global.service.MemberService.SocialLoginService;
import com.codiary.backend.global.web.dto.Member.MemberResponseDTO;
import com.codiary.backend.global.web.dto.SocialLogin.Oauth2ResponseDTO;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class SocialLoginController {

private final SocialLoginService socialLoginService;

@PostMapping("/login/kakao")
@Operation(summary = "카카오 로그인")
public ApiResponse<Oauth2ResponseDTO.kakaoLoginDTO> kakaoLogin() {
String url = socialLoginService.getRedirectUrl();
return ApiResponse.onSuccess(SuccessStatus.MEMBER_OK, new Oauth2ResponseDTO.kakaoLoginDTO(url));
}

@GetMapping("/login/kakao")
@Operation(summary = "카카오 서버에서 요청하는 api")
public ApiResponse<MemberResponseDTO.MemberTokenResponseDTO> kekaoToken(@RequestParam String code) {
return socialLoginService.kakaoLogin(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.codiary.backend.global.web.dto.SocialLogin;

import lombok.*;

public class Oauth2ResponseDTO {

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class kakaoLoginDTO {
String redirectUrl;
}
}