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#3: 카카오로그인 구현 #4

Merged
merged 14 commits into from
Oct 9, 2024
14 changes: 12 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
}

group = 'server'
Expand Down Expand Up @@ -48,6 +48,16 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'com.h2database:h2'

implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.9.0'

//JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'
}

tasks.named('test') {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/server/poptato/PoptatoApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class PoptatoApplication {

public static void main(String[] args) {
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/server/poptato/auth/api/AuthController.java
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);
}
}
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) {
}
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) {
}
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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createNewUserResponse() 로 리팩토링 하는 게 좋아보여요!

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 src/main/java/server/poptato/auth/application/service/JwtService.java
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);
}
}
39 changes: 39 additions & 0 deletions src/main/java/server/poptato/config/WebConfig.java
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 src/main/java/server/poptato/external/kakao/SocialPlatform.java
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;
}
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);
}
}
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) {
}
Loading