-
Notifications
You must be signed in to change notification settings - Fork 0
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#3: 카카오로그인 구현 #4
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
2572b87
chore: token record 추가
b8c7df5
chore: config 추가
f37f394
chore: auth dto 추가
ceca9bd
feat: Auth service, controller 추가
3321ca9
feat: 카카오 관련 기능 추가 - external 패키지
1edacc5
chore: user 도메인 및 엔티디 추가 및 response 수정
3180770
fix: 회원가입, 로그인 시 응답 구분
feed5ea
feat: 카카오에서 이름 가져오기 추가
193b15d
fix: userId 추가
5f70786
feat: 이메일 가져오기 기능 추가
e38ea07
refactor: repository 분리
5f9a2f9
refactor: 회원가입 로직 분리
c88cbcc
refactor: userId resolver로 변경
58fd3b5
refactor: login api 수정
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package server.poptato.auth.api; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import server.poptato.auth.api.request.TokenRequestDto; | ||
import server.poptato.auth.application.response.LoginResponseDto; | ||
import server.poptato.auth.application.service.AuthService; | ||
import server.poptato.external.kakao.resolver.KakaoCode; | ||
import server.poptato.external.kakao.resolver.OriginHeader; | ||
import server.poptato.global.dto.TokenPair; | ||
import server.poptato.global.response.BaseResponse; | ||
import server.poptato.user.resolver.UserId; | ||
|
||
import static server.poptato.global.exception.errorcode.BaseExceptionErrorCode.*; | ||
|
||
@RestController | ||
@RequestMapping("/auth") | ||
@RequiredArgsConstructor | ||
public class AuthController { | ||
|
||
private final AuthService authService; | ||
|
||
@PostMapping("/login") | ||
public BaseResponse<LoginResponseDto> login( | ||
@KakaoCode String kakaoCode, | ||
@OriginHeader String originHeader) { | ||
LoginResponseDto response = authService.login(originHeader, kakaoCode); | ||
return new BaseResponse<>(SUCCESS, response); | ||
} | ||
|
||
@PostMapping("/logout") | ||
public BaseResponse logout(@UserId Long userId) { | ||
authService.logout(userId); | ||
return new BaseResponse(SUCCESS); | ||
} | ||
@PostMapping("/refresh") | ||
public BaseResponse<TokenPair> refresh(@RequestBody final TokenRequestDto tokenRequestDto) { | ||
TokenPair response = authService.refresh(tokenRequestDto); | ||
return new BaseResponse<>(SUCCESS, response); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
src/main/java/server/poptato/auth/api/request/TokenRequestDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package server.poptato.auth.api.request; | ||
|
||
public record TokenRequestDto(String accessToken, String refreshToken) { | ||
} |
4 changes: 4 additions & 0 deletions
4
src/main/java/server/poptato/auth/application/response/LoginResponseDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package server.poptato.auth.application.response; | ||
|
||
public record LoginResponseDto(String accessToken, String refreshToken, boolean isNewUser, Long userId) { | ||
} |
71 changes: 71 additions & 0 deletions
71
src/main/java/server/poptato/auth/application/service/AuthService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package server.poptato.auth.application.service; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import server.poptato.auth.api.request.TokenRequestDto; | ||
import server.poptato.auth.application.response.LoginResponseDto; | ||
import server.poptato.external.kakao.dto.response.KakaoUserInfo; | ||
import server.poptato.external.kakao.service.KakaoSocialService; | ||
import server.poptato.global.dto.TokenPair; | ||
import server.poptato.global.exception.BaseException; | ||
import server.poptato.user.domain.entity.User; | ||
import server.poptato.user.infra.repository.JpaUserRepository; | ||
|
||
import java.util.Optional; | ||
|
||
import static server.poptato.global.exception.errorcode.BaseExceptionErrorCode.TOKEN_TIME_EXPIRED_EXCEPTION; | ||
import static server.poptato.global.exception.errorcode.BaseExceptionErrorCode.USER_NOT_FOUND_EXCEPTION; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class AuthService { | ||
private final JwtService jwtService; | ||
private final KakaoSocialService kakaoSocialService; | ||
private final JpaUserRepository userRepository; | ||
|
||
public LoginResponseDto login(final String baseUrl, final String kakaoCode) { | ||
KakaoUserInfo info = kakaoSocialService.getIdAndNickNameAndEmailFromKakao(baseUrl, kakaoCode); | ||
String kakaoId = info.kakaoId(); | ||
String name = info.nickname(); | ||
String email = info.email(); | ||
Optional<User> user = userRepository.findByKakaoId(kakaoId); | ||
if (user.isEmpty()) { | ||
return createNewUserResponse(kakaoId, name, email); | ||
} | ||
TokenPair tokenPair = jwtService.generateTokenPair(String.valueOf(user.get().getId())); | ||
return new LoginResponseDto(tokenPair.accessToken(), tokenPair.refreshToken(), false, user.get().getId()); | ||
} | ||
|
||
public void logout(final Long userId) { | ||
final User user = userRepository.findById(userId).orElseThrow(() -> new BaseException(USER_NOT_FOUND_EXCEPTION)); | ||
jwtService.deleteRefreshToken(String.valueOf(userId)); | ||
} | ||
|
||
public TokenPair refresh(final TokenRequestDto tokenRequestDto) { | ||
if (!jwtService.verifyToken(tokenRequestDto.refreshToken())) | ||
throw new BaseException(TOKEN_TIME_EXPIRED_EXCEPTION); | ||
|
||
final String userId = jwtService.getUserIdInToken(tokenRequestDto.refreshToken()); | ||
final User user = userRepository.findById(Long.parseLong(userId)).orElseThrow(() -> new BaseException(USER_NOT_FOUND_EXCEPTION)); | ||
|
||
if (!jwtService.compareRefreshToken(userId, tokenRequestDto.refreshToken())) | ||
throw new BaseException(TOKEN_TIME_EXPIRED_EXCEPTION); | ||
|
||
final TokenPair tokenPair = jwtService.generateTokenPair(userId); | ||
jwtService.saveRefreshToken(userId, tokenPair.refreshToken()); | ||
return tokenPair; | ||
} | ||
|
||
private LoginResponseDto createNewUserResponse(String kakaoId, String name, String email) { | ||
User newUser = User.builder() | ||
.kakaoId(kakaoId) | ||
.name(name) | ||
.email(email) | ||
.build(); | ||
userRepository.save(newUser); | ||
|
||
// 토큰 발급 및 응답 객체 생성 | ||
TokenPair tokenPair = jwtService.generateTokenPair(String.valueOf(newUser.getId())); | ||
return new LoginResponseDto(tokenPair.accessToken(), tokenPair.refreshToken(), true, newUser.getId()); | ||
} | ||
} |
135 changes: 135 additions & 0 deletions
135
src/main/java/server/poptato/auth/application/service/JwtService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package server.poptato.auth.application.service; | ||
|
||
|
||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Header; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.security.Keys; | ||
import jakarta.annotation.PostConstruct; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Service; | ||
import server.poptato.global.dto.TokenPair; | ||
import server.poptato.global.exception.BaseException; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.security.Key; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import static server.poptato.global.exception.errorcode.BaseExceptionErrorCode.TOKEN_TIME_EXPIRED_EXCEPTION; | ||
|
||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class JwtService { | ||
@Value("${jwt.secret}") | ||
private String jwtSecret; | ||
private static final String USER_ID = "USER_ID"; | ||
private static final String ACCESS_TOKEN = "ACCESS_TOKEN"; | ||
private static final String REFRESH_TOKEN = "REFRESH_TOKEN"; | ||
public static final int MINUTE_IN_MILLISECONDS = 60 * 1000; | ||
public static final long DAYS_IN_MILLISECONDS = 24 * 60 * 60 * 1000L; | ||
public static final int ACCESS_TOKEN_EXPIRATION_MINUTE = 10; | ||
public static final int REFRESH_TOKEN_EXPIRATION_DAYS = 14; | ||
private final RedisTemplate<String, String> redisTemplate; | ||
|
||
@PostConstruct | ||
protected void init() { | ||
jwtSecret = Base64.getEncoder() | ||
.encodeToString(jwtSecret.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
public String createAccessToken(final String userId) { | ||
final Claims claims = getAccessTokenClaims(); | ||
|
||
claims.put(USER_ID, userId); | ||
return createToken(claims); | ||
} | ||
|
||
public String createRefreshToken(final String userId) { | ||
final Claims claims = getRefreshTokenClaims(); | ||
|
||
claims.put(USER_ID, userId); | ||
return createToken(claims); | ||
} | ||
|
||
public boolean verifyToken(final String token) { | ||
try { | ||
final Claims claims = getBody(token); | ||
return true; | ||
} catch (RuntimeException e) { | ||
if (e instanceof ExpiredJwtException) { | ||
throw new BaseException(TOKEN_TIME_EXPIRED_EXCEPTION); | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
public String getUserIdInToken(final String token) { | ||
final Claims claims = getBody(token); | ||
return (String) claims.get(USER_ID); | ||
} | ||
|
||
public TokenPair generateTokenPair(final String userId) { | ||
final String accessToken = createAccessToken(userId); | ||
final String refreshToken = createRefreshToken(userId); | ||
saveRefreshToken(userId, refreshToken); | ||
return new TokenPair(accessToken, refreshToken); | ||
} | ||
|
||
public boolean compareRefreshToken(final String userId, final String refreshToken) { | ||
final String storedRefreshToken = redisTemplate.opsForValue().get(userId); | ||
if (storedRefreshToken == null) return false; | ||
return storedRefreshToken.equals(refreshToken); | ||
} | ||
|
||
public void saveRefreshToken(final String userId, final String refreshToken) { | ||
redisTemplate.opsForValue().set(userId, refreshToken, REFRESH_TOKEN_EXPIRATION_DAYS, TimeUnit.DAYS); | ||
} | ||
|
||
private String createToken(final Claims claims) { | ||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) | ||
.setClaims(claims) | ||
.signWith(getSigningKey()) | ||
.compact(); | ||
} | ||
|
||
private Claims getRefreshTokenClaims() { | ||
final Date now = new Date(); | ||
return Jwts.claims() | ||
.setSubject(REFRESH_TOKEN) | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_DAYS * DAYS_IN_MILLISECONDS)); | ||
} | ||
|
||
private Claims getAccessTokenClaims() { | ||
final Date now = new Date(); | ||
return Jwts.claims() | ||
.setSubject(ACCESS_TOKEN) | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION_MINUTE * MINUTE_IN_MILLISECONDS)); | ||
} | ||
|
||
private Claims getBody(final String token) { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(getSigningKey()) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
} | ||
|
||
private Key getSigningKey() { | ||
final byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8); | ||
return Keys.hmacShaKeyFor(keyBytes); | ||
} | ||
|
||
public void deleteRefreshToken(final String userId) { | ||
redisTemplate.delete(userId); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package server.poptato.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.http.HttpMethod; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.config.annotation.CorsRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
import server.poptato.external.kakao.resolver.KakaoCodeResolver; | ||
import server.poptato.external.kakao.resolver.OriginResolver; | ||
import server.poptato.user.resolver.UserResolver; | ||
|
||
import java.util.List; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class WebConfig implements WebMvcConfigurer { | ||
private final UserResolver userResolver; | ||
private final KakaoCodeResolver kakaoCodeResolver; | ||
private final OriginResolver originResolver; | ||
|
||
@Override | ||
public void addCorsMappings(final CorsRegistry registry) { | ||
registry.addMapping("/**") | ||
.allowedOriginPatterns("*") | ||
.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name()) | ||
.allowedHeaders("Authorization", "Content-Type") | ||
.allowCredentials(true) | ||
.maxAge(3000); | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(userResolver); | ||
resolvers.add(kakaoCodeResolver); | ||
resolvers.add(originResolver); | ||
|
||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/server/poptato/external/kakao/SocialPlatform.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package server.poptato.external.kakao; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) | ||
public enum SocialPlatform { | ||
KAKAO("카카오"); | ||
|
||
private final String value; | ||
} |
30 changes: 30 additions & 0 deletions
30
src/main/java/server/poptato/external/kakao/SocialServiceProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package server.poptato.external.kakao; | ||
|
||
|
||
|
||
import jakarta.annotation.PostConstruct; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
import server.poptato.external.kakao.service.KakaoSocialService; | ||
import server.poptato.external.kakao.service.SocialService; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class SocialServiceProvider { | ||
|
||
private static final Map<SocialPlatform, SocialService> socialServiceMap = new HashMap<>(); | ||
|
||
private final KakaoSocialService kakaoSocialService; | ||
|
||
@PostConstruct | ||
void initializeSocialServiceMap() { | ||
socialServiceMap.put(SocialPlatform.KAKAO, kakaoSocialService); | ||
} | ||
|
||
public SocialService getSocialService(SocialPlatform socialPlatform) { | ||
return socialServiceMap.get(socialPlatform); | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
src/main/java/server/poptato/external/kakao/dto/response/KakaoAccessTokenResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package server.poptato.external.kakao.dto.response; | ||
|
||
import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
|
||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
public record KakaoAccessTokenResponse(String accessToken, String refreshToken) { | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
createNewUserResponse() 로 리팩토링 하는 게 좋아보여요!