-
Notifications
You must be signed in to change notification settings - Fork 5
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
[Feature] - 카카오 로그인 구현 #55
Changes from all commits
2583cc9
63ccece
778d1b6
11684f3
ce2e4e0
b0a0b00
20198e3
4b52f2c
8e979a2
4535fc0
c265ab2
c03e945
554e846
5d6f4c0
c42b09e
859fce1
efa9892
f4406a4
deff84e
b9ec188
2b4913b
62049b0
46bcaac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package woowacourse.touroot.authentication.controller; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import woowacourse.touroot.authentication.dto.LoginResponse; | ||
import woowacourse.touroot.authentication.service.LoginService; | ||
|
||
@RequiredArgsConstructor | ||
@RestController | ||
@RequestMapping("/api/v1/login") | ||
public class LoginController { | ||
|
||
private final LoginService loginService; | ||
|
||
@GetMapping("/oauth/kakao") | ||
public ResponseEntity<LoginResponse> login(@RequestParam(name = "code") String authorizationCode) { | ||
return ResponseEntity.ok() | ||
.body(loginService.login(authorizationCode)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package woowacourse.touroot.authentication.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
public record KakaoAccessTokenResponse( | ||
@JsonProperty("token_type") | ||
String tokenType, | ||
@JsonProperty("access_token") | ||
String accessToken, | ||
@JsonProperty("expires_in") | ||
Integer expiresIn, | ||
@JsonProperty("refresh_token") | ||
String refreshToken, | ||
@JsonProperty("refresh_token_expires_in") | ||
Integer refreshTokenExpiresIn, | ||
@JsonProperty("scope") | ||
String scope | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package woowacourse.touroot.authentication.dto; | ||
|
||
public record LoginResponse(String accessToken) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package woowacourse.touroot.authentication.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
public record OauthUserInformationResponse( | ||
@JsonProperty("id") | ||
Long socialLoginId, | ||
@JsonProperty("kakao_account") | ||
KakaoAccount kakaoAccount | ||
) { | ||
|
||
public String nickname() { | ||
return kakaoAccount.kakaoProfile.nickname; | ||
} | ||
|
||
public String profileImage() { | ||
return kakaoAccount.kakaoProfile.image; | ||
} | ||
|
||
private record KakaoAccount( | ||
@JsonProperty("profile") KakaoProfile kakaoProfile | ||
) { | ||
} | ||
|
||
private record KakaoProfile( | ||
@JsonProperty("nickname") String nickname, | ||
@JsonProperty("profile_image_url") String image | ||
) { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package woowacourse.touroot.authentication.infrastructure; | ||
|
||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
import java.util.Date; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
import woowacourse.touroot.member.domain.Member; | ||
|
||
@Component | ||
public class JwtTokenProvider { | ||
|
||
private static final String MEMBER_ID_KEY = "id"; | ||
|
||
private final String secretKey; | ||
private final long validityInMilliseconds; | ||
|
||
public JwtTokenProvider( | ||
@Value("${security.jwt.token.secret-key}") String secretKey, | ||
@Value("${security.jwt.token.expire-length}") long validityInMilliseconds | ||
) { | ||
this.secretKey = secretKey; | ||
this.validityInMilliseconds = validityInMilliseconds; | ||
} | ||
|
||
public String createToken(Member member) { | ||
Date now = new Date(); | ||
Date validity = new Date(now.getTime() + validityInMilliseconds); | ||
|
||
return Jwts.builder() | ||
.setSubject(member.getId().toString()) | ||
.claim(MEMBER_ID_KEY, member.getId()) | ||
.setExpiration(validity) | ||
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) | ||
.compact(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package woowacourse.touroot.authentication.infrastructure; | ||
|
||
import java.time.Duration; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.boot.web.client.ClientHttpRequestFactories; | ||
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.http.client.ClientHttpRequestFactory; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.LinkedMultiValueMap; | ||
import org.springframework.util.MultiValueMap; | ||
import org.springframework.web.client.RestClient; | ||
import woowacourse.touroot.authentication.dto.KakaoAccessTokenResponse; | ||
import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; | ||
|
||
@Component | ||
public class KakaoOauthClient { | ||
|
||
private final String userInformationRequestUri; | ||
private final String accessTokenRequestUri; | ||
private final String restApiKey; | ||
private final String redirectUri; | ||
private final RestClient restClient; | ||
|
||
public KakaoOauthClient( | ||
@Value("${oauth.kakao.user-information-request-uri}") String userInformationRequestUri, | ||
@Value("${oauth.kakao.access-token-request-uri}") String accessTokenRequestUri, | ||
@Value("${oauth.kakao.rest-api-key}") String restApiKey, | ||
@Value("${oauth.kakao.redirect-uri}") String redirectUri | ||
) { | ||
this.userInformationRequestUri = userInformationRequestUri; | ||
this.accessTokenRequestUri = accessTokenRequestUri; | ||
this.restApiKey = restApiKey; | ||
this.redirectUri = redirectUri; | ||
this.restClient = buildRestClient(); | ||
} | ||
|
||
private RestClient buildRestClient() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 걍 의견) build말고 |
||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생성자 로직이 많은 것 같아서 메서드로 빼면 어떨깝쇼 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋을것 같답쇼 분리했습니답쇼 |
||
.withConnectTimeout(Duration.ofSeconds(1)) | ||
.withReadTimeout(Duration.ofSeconds(3)); | ||
|
||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings); | ||
|
||
return RestClient.builder() | ||
.requestFactory(requestFactory) | ||
.build(); | ||
} | ||
|
||
public OauthUserInformationResponse requestUserInformation(String authorizationCode) { | ||
KakaoAccessTokenResponse kakaoAccessTokenResponse = requestAccessToken(authorizationCode); | ||
|
||
return restClient.get() | ||
.uri(userInformationRequestUri) | ||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + kakaoAccessTokenResponse.accessToken()) | ||
.retrieve() | ||
.toEntity(OauthUserInformationResponse.class) | ||
.getBody(); | ||
} | ||
|
||
private KakaoAccessTokenResponse requestAccessToken(String authorizationCode) { | ||
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
html form 데이터가 안정적으로 body에 바인딩 되기 위해서는 Spring에서 util로 제공하는 MultivalueMap 을 이용해야 하는 것으로 알고 있습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 신기하네요 |
||
params.add("code", authorizationCode); | ||
params.add("client_id", restApiKey); | ||
params.add("redirect_uri", redirectUri); | ||
params.add("grant_type", "authorization_code"); | ||
|
||
return restClient.post() | ||
.uri(accessTokenRequestUri) | ||
.contentType(MediaType.APPLICATION_FORM_URLENCODED) | ||
.body(params) | ||
.retrieve() | ||
.toEntity(KakaoAccessTokenResponse.class) | ||
.getBody(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package woowacourse.touroot.authentication.infrastructure; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class KakaoOauthProvider { | ||
|
||
private final KakaoOauthClient kakaoOauthClient; | ||
|
||
public OauthUserInformationResponse getUserInformation(String authorizationCode) { | ||
return kakaoOauthClient.requestUserInformation(authorizationCode); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package woowacourse.touroot.authentication.service; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import woowacourse.touroot.authentication.dto.LoginResponse; | ||
import woowacourse.touroot.authentication.dto.OauthUserInformationResponse; | ||
import woowacourse.touroot.authentication.infrastructure.JwtTokenProvider; | ||
import woowacourse.touroot.authentication.infrastructure.KakaoOauthProvider; | ||
import woowacourse.touroot.member.domain.Member; | ||
import woowacourse.touroot.member.repository.MemberRepository; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class LoginService { | ||
|
||
private final MemberRepository memberRepository; | ||
private final KakaoOauthProvider oauthProvider; | ||
private final JwtTokenProvider tokenProvider; | ||
|
||
public LoginResponse login(String code) { | ||
OauthUserInformationResponse userInformation = oauthProvider.getUserInformation(code); | ||
Member member = memberRepository.findByKakaoId(userInformation.socialLoginId()) | ||
.orElseGet(() -> signUp(userInformation)); | ||
|
||
return new LoginResponse(tokenProvider.createToken(member)); | ||
} | ||
|
||
private Member signUp(OauthUserInformationResponse userInformation) { | ||
return memberRepository.save( | ||
new Member(userInformation.socialLoginId(), userInformation.nickname(), userInformation.profileImage()) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package woowacourse.touroot.global.config; | ||
|
||
import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
@EnableEncryptableProperties | ||
public class JasyptConfig { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package woowacourse.touroot.member.domain; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import woowacourse.touroot.entity.BaseEntity; | ||
|
||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
@Getter | ||
@Entity | ||
public class Member extends BaseEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(nullable = false) | ||
private Long kakaoId; | ||
|
||
@Column(nullable = false) | ||
private String nickname; | ||
|
||
@Column(nullable = false) | ||
private String profileImageUri; | ||
|
||
public Member(Long kakaoId, String nickname, String profileImageUri) { | ||
this(null, kakaoId, nickname, profileImageUri); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package woowacourse.touroot.member.repository; | ||
|
||
import java.util.Optional; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import woowacourse.touroot.member.domain.Member; | ||
|
||
public interface MemberRepository extends JpaRepository<Member, Long> { | ||
|
||
Optional<Member> findByKakaoId(Long kakaoId); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
괄호 내려주면 다른 파일이랑 컨벤션이 더 잘 맞을 것 같습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영 완!