diff --git a/cakey-api/src/main/java/com/cakey/common/auth/CustomAccessDeniedHandler.java b/cakey-api/src/main/java/com/cakey/common/auth/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..7bd2a3a --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/CustomAccessDeniedHandler.java @@ -0,0 +1,32 @@ +package com.cakey.common.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) throws IOException { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + ResponseEntity responseEntity = new ResponseEntity(HttpStatus.NOT_FOUND); + + response.getWriter().write(mapper.writeValueAsString(responseEntity)); + } +} \ No newline at end of file diff --git a/cakey-api/src/main/java/com/cakey/common/auth/CustomJwtAuthenticationEntryPoint.java b/cakey-api/src/main/java/com/cakey/common/auth/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..b269d5c --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,20 @@ +package com.cakey.common.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException){ + setResponse(response); + } + + private void setResponse(HttpServletResponse response){ + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } +} + diff --git a/cakey-api/src/main/java/com/cakey/common/auth/JwtTokenProvider.java b/cakey-api/src/main/java/com/cakey/common/auth/JwtTokenProvider.java new file mode 100644 index 0000000..4cca03a --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/JwtTokenProvider.java @@ -0,0 +1,92 @@ +package com.cakey.common.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Component +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 2 * 60 * 60 * 1000L; //2시간 + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 14* 24 * 60 * 60 * 1000L; //2주 + + @Value("${jwt.secret}") + private String JWT_SECRET; + + @PostConstruct + protected void init() { + JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + } + + public String issueAccessToken(final Authentication authentication) { + return issueToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); + } + + public String issueRefreshToken(final Authentication authentication) { + return issueToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } + + private String issueToken(final Authentication authentication, final Long expireTime) { + final Date now = new Date(); + + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + expireTime)); + + claims.put(USER_ID, authentication.getPrincipal()); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateToken(final String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException e) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException e) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException e) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException e) { + return JwtValidationType.EMPTY_JWT; + } + + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(final String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } + +} diff --git a/cakey-api/src/main/java/com/cakey/common/auth/JwtValidationType.java b/cakey-api/src/main/java/com/cakey/common/auth/JwtValidationType.java new file mode 100644 index 0000000..93272fc --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/JwtValidationType.java @@ -0,0 +1,16 @@ +package com.cakey.common.auth; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum JwtValidationType { + VALID_JWT("VALID_JWT"), + INVALID_JWT_SIGNATURE("INVALID_JWT_SIGNATURE"), + INVALID_JWT_TOKEN("INVALID_JWT_TOKEN"), + EXPIRED_JWT_TOKEN("EXPIRED_JWT_TOKEN"), + UNSUPPORTED_JWT_TOKEN("UNSUPPORTED_JWT_TOKEN"), + EMPTY_JWT("EMPTY_JWT") + ; + + private String valdationType; +} \ No newline at end of file diff --git a/cakey-api/src/main/java/com/cakey/common/auth/UserAuthentication.java b/cakey-api/src/main/java/com/cakey/common/auth/UserAuthentication.java new file mode 100644 index 0000000..117063b --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/UserAuthentication.java @@ -0,0 +1,11 @@ +package com.cakey.common.auth; + +import java.util.Collection; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +public class UserAuthentication extends UsernamePasswordAuthenticationToken { + public UserAuthentication(Object principal, Object credentials, Collection authorities) { + super(principal, credentials, authorities); + } +} \ No newline at end of file diff --git a/cakey-api/src/main/java/com/cakey/common/auth/filter/CustomAuthenticationFilter.java b/cakey-api/src/main/java/com/cakey/common/auth/filter/CustomAuthenticationFilter.java new file mode 100644 index 0000000..e794294 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/filter/CustomAuthenticationFilter.java @@ -0,0 +1,55 @@ +package com.cakey.common.auth.filter; + +import com.cakey.common.auth.JwtTokenProvider; +import com.cakey.common.auth.UserAuthentication; +import com.cakey.jwt.service.TokenService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class CustomAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; +// private final TokenService tokenService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + try { + final String accessToken = getAccessTokenFromCookie(request); + if (accessToken != null) { + final Long userId = jwtTokenProvider.getUserFromJwt(accessToken); + SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(userId, null, null)); + } else { + SecurityContextHolder.clearContext(); + } + } catch (Exception e) { + } + filterChain.doFilter(request, response); + } + + public String getAccessTokenFromCookie(@NonNull HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("access_token")) { + return cookie.getValue(); + } + } + } + return null; + } +} diff --git a/cakey-api/src/main/java/com/cakey/common/auth/filter/JwtAuthenticationFilter.java b/cakey-api/src/main/java/com/cakey/common/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..c3aae55 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/common/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,62 @@ +package com.cakey.common.auth.filter; + +import com.cakey.common.auth.JwtTokenProvider; +import com.cakey.common.auth.UserAuthentication; +import com.cakey.common.auth.JwtValidationType; +import com.cakey.exception.CakeyException; +import com.cakey.exception.ErrorCode; +import com.cakey.jwt.service.TokenService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + try { + final String token = getAccessTokenFromCookie(request); + if (token == null) { + //todo: exception 처리 + } + + if (jwtTokenProvider.validateToken(token) == JwtValidationType.INVALID_JWT_TOKEN) { + //todo: exception 처리 + } + + final Long userId = jwtTokenProvider.getUserFromJwt(token); + SecurityContextHolder.getContext().setAuthentication(new UserAuthentication(userId, null, null)); + + } catch (Exception e) { + } + filterChain.doFilter(request, response); + } + + private String getAccessTokenFromCookie(final HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for(Cookie cookie : cookies) { + if (cookie.getName().equals("accessToken")) { + return cookie.getValue(); + } + } + } + return null; + } +} \ No newline at end of file diff --git a/cakey-api/src/main/java/com/cakey/user/controller/UserController.java b/cakey-api/src/main/java/com/cakey/user/controller/UserController.java new file mode 100644 index 0000000..c181452 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/user/controller/UserController.java @@ -0,0 +1,37 @@ +package com.cakey.user.controller; + +import com.cakey.client.dto.LoginReq; +import com.cakey.common.resolver.user.UserId; +import com.cakey.common.response.ApiResponseUtil; +import com.cakey.common.response.BaseResponse; +import com.cakey.common.response.SuccessCode; +import com.cakey.jwt.service.TokenService; +import com.cakey.user.dto.LoginSuccessRes; +import com.cakey.user.service.UserService; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/user") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + private final TokenService tokenService; + + @PostMapping("/login") + public ResponseEntity> login(@RequestHeader(value = "Authorization") final String authorization, + @RequestBody final LoginReq loginReq, + HttpServletResponse response) { + LoginSuccessRes loginSuccessRes = userService.create(authorization, loginReq); + response.addHeader("Set-Cookie", userService.accessCookie(loginSuccessRes).toString()); + response.addHeader("Set-Cookie", userService.refreshCookie(loginSuccessRes).toString()); + return ApiResponseUtil.success(SuccessCode.OK); + } +} diff --git a/cakey-api/src/main/java/com/cakey/user/service/UserService.java b/cakey-api/src/main/java/com/cakey/user/service/UserService.java new file mode 100644 index 0000000..2328dd3 --- /dev/null +++ b/cakey-api/src/main/java/com/cakey/user/service/UserService.java @@ -0,0 +1,132 @@ +package com.cakey.user.service; + +import com.cakey.client.dto.LoginReq; +import com.cakey.client.dto.UserInfoRes; +import com.cakey.client.kakao.api.KakaoSocialService; +import com.cakey.common.auth.JwtTokenProvider; +import com.cakey.common.auth.UserAuthentication; +import com.cakey.jwt.domain.UserRole; +import com.cakey.jwt.service.TokenService; +import com.cakey.client.SocialType; +import com.cakey.user.domain.User; +import com.cakey.user.dto.AccessTokenGetSuccess; +import com.cakey.user.dto.LoginSuccessRes; +import com.cakey.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; + private final KakaoSocialService kakaoSocialService; + + private final static String ACCESS_TOKEN = "accessToken"; + private final static String REFRESH_TOKEN = "refreshToken"; + + + public LoginSuccessRes create( + final String authorizationCode, + final LoginReq loginReq + ) { + return getTokenDto(kakaoSocialService.login(authorizationCode, loginReq)); + } + + public ResponseCookie accessCookie(LoginSuccessRes loginSuccessRes) { + ResponseCookie accessCookie = ResponseCookie.from(ACCESS_TOKEN, loginSuccessRes.accessToken()) + .maxAge(60 * 60 * 7 * 24) + .path("/") + .secure(true) + .sameSite("None") + .httpOnly(true) + .build(); + return accessCookie; + } + + public ResponseCookie refreshCookie(LoginSuccessRes loginSuccessRes) { + ResponseCookie refreshCookie = ResponseCookie.from(REFRESH_TOKEN,loginSuccessRes.refreshToken()) + .maxAge(60 * 60 * 7 * 24) + .path("/") + .secure(true) + .sameSite("None") + .httpOnly(true) + .build(); + return refreshCookie; + } + + public UserInfoRes getUserInfo(final String authorizationCode, + final LoginReq loginReq + ) { + switch (loginReq.socialType()){ + case KAKAO: + return kakaoSocialService.login(authorizationCode, loginReq); + default: + throw new RuntimeException("Social type not supported"); + } + } + + public Long createUser(final UserInfoRes userInfoRes) { + User user = User.createUser( + userInfoRes.name(), + UserRole.USER, + userInfoRes.socialType(), + userInfoRes.socialId() + ); + return userRepository.save(user).getId(); + } + + public User getBySocialId( + final String socialId, + final SocialType socialType + ) + { + User user = userRepository.findBySocialTypeAndSocialId(socialType, socialId) + .orElseThrow(()-> new RuntimeException("User not found")); + return user; + } + + public AccessTokenGetSuccess refreshToken(final String refreshToken) { + Long userId = jwtTokenProvider.getUserFromJwt(refreshToken); + if(!userId.equals(tokenService.findIdByRefreshToken(refreshToken))){ + throw new RuntimeException("Invalid refresh token"); + } + + UserAuthentication userAuthentication = new UserAuthentication(userId, null, null); + return AccessTokenGetSuccess.of( + jwtTokenProvider.issueAccessToken(userAuthentication) + ); + } + + public boolean isExistingUser( + final String socialId, + final SocialType socialType + ) { + return userRepository.findBySocialTypeAndSocialId(socialType, socialId).isPresent(); + } + + public LoginSuccessRes getTokenByUserId( + final Long id + ) { + UserAuthentication userAuthentication = new UserAuthentication(id, null, null); + String refreshToken = jwtTokenProvider.issueRefreshToken(userAuthentication); + tokenService.saveRefreshToken(id, refreshToken); + return LoginSuccessRes.of( + jwtTokenProvider.issueAccessToken(userAuthentication), + refreshToken + ); + } + private LoginSuccessRes getTokenDto(final UserInfoRes userInfoRes) { + if(isExistingUser(userInfoRes.socialId(), userInfoRes.socialType())){ + return getTokenByUserId(getBySocialId(userInfoRes.socialId(), userInfoRes.socialType()).getId()); + } + else { + Long id = createUser(userInfoRes); + return getTokenByUserId(id); + } + } + +} \ No newline at end of file diff --git a/cakey-auth/src/main/java/com/cakey/client/SocialType.java b/cakey-auth/src/main/java/com/cakey/client/SocialType.java new file mode 100644 index 0000000..6a0e46d --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/SocialType.java @@ -0,0 +1,12 @@ +package com.cakey.client; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SocialType { + KAKAO("KAKAO"), + ; + private String socialType; +} \ No newline at end of file diff --git a/cakey-auth/src/main/java/com/cakey/client/dto/LoginReq.java b/cakey-auth/src/main/java/com/cakey/client/dto/LoginReq.java new file mode 100644 index 0000000..d5743ed --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/dto/LoginReq.java @@ -0,0 +1,8 @@ +package com.cakey.client.dto; + +import com.cakey.client.SocialType; + +public record LoginReq( + SocialType socialType +) { +} diff --git a/cakey-auth/src/main/java/com/cakey/client/dto/UserInfoRes.java b/cakey-auth/src/main/java/com/cakey/client/dto/UserInfoRes.java new file mode 100644 index 0000000..fced34a --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/dto/UserInfoRes.java @@ -0,0 +1,20 @@ +package com.cakey.client.dto; + +import com.cakey.client.SocialType; +import com.cakey.jwt.domain.UserRole; + +public record UserInfoRes( + String socialId, + String name, + SocialType socialType, + UserRole userRole +) { + public static UserInfoRes of( + final String socialId, + final String name, + final SocialType socialType, + final UserRole userRole + ) { + return new UserInfoRes(socialId, name, socialType, userRole); + } +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoApiClient.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoApiClient.java new file mode 100644 index 0000000..912c512 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoApiClient.java @@ -0,0 +1,19 @@ +package com.cakey.client.kakao.api; + +import com.cakey.client.kakao.api.dto.KakaoUserRes; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") +public interface KakaoApiClient { + + @GetMapping(value = "/v2/user/me", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoUserRes getUserInformation( + @RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken, + @RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType + ); +} \ No newline at end of file diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoAuthApiClient.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoAuthApiClient.java new file mode 100644 index 0000000..d9add11 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoAuthApiClient.java @@ -0,0 +1,20 @@ +package com.cakey.client.kakao.api; + +import com.cakey.client.kakao.api.dto.KakaoAccessTokenRes; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") +public interface KakaoAuthApiClient { + + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoAccessTokenRes getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code + ); +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java new file mode 100644 index 0000000..bd4e530 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/KakaoSocialService.java @@ -0,0 +1,78 @@ +package com.cakey.client.kakao.api; + +import com.cakey.client.dto.LoginReq; +import com.cakey.client.dto.UserInfoRes; +import com.cakey.client.kakao.api.dto.KakaoAccessTokenRes; +import com.cakey.client.kakao.api.dto.KakaoUserRes; +import com.cakey.client.SocialType; +import com.cakey.jwt.domain.UserRole; +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class KakaoSocialService implements SocialService { + private static final String AUTH_CODE = "authorization_code"; + private static final String REDIRECT_URI = "http://localhost:5173/kakao"; + + @Value("${kakao.clientId}") + private String clientId; + + private final KakaoApiClient kakaoApiClient; + private final KakaoAuthApiClient kakaoAuthApiClient; + + @Transactional + @Override + public UserInfoRes login( + final String authorizationCode, + final LoginReq loginReq + ) { + String accessToken; + try { + // 인가 코드로 Access Token + Refresh Token 받아오기 + accessToken = getOAuth2Authentication(authorizationCode); + } catch (FeignException e) { + throw new RuntimeException("authentication code expired"); + } + String contentType = MediaType.APPLICATION_FORM_URLENCODED.toString(); + // Access Token으로 유저 정보 불러오기 + return getLoginDto(loginReq.socialType(), getUserInfo(accessToken, contentType), UserRole.USER); + } + + private String getOAuth2Authentication( + final String authorizationCode + ) { + KakaoAccessTokenRes response = kakaoAuthApiClient.getOAuth2AccessToken( + AUTH_CODE, + clientId, + REDIRECT_URI, + authorizationCode + ); + return response.accessToken(); + } + + private KakaoUserRes getUserInfo( + final String accessToken, + final String contentType + ) { + System.out.println("accessToken:" + accessToken); + return kakaoApiClient.getUserInformation("Bearer " + accessToken, contentType); + } + + private UserInfoRes getLoginDto( + final SocialType socialType, + final KakaoUserRes userResponse, + final UserRole userRole + ) { + return UserInfoRes.of( + userResponse.id().toString(), + userResponse.kakaoAccount().profile().nickname(), + socialType, + userRole + ); + } +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/SocialService.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/SocialService.java new file mode 100644 index 0000000..818396b --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/SocialService.java @@ -0,0 +1,8 @@ +package com.cakey.client.kakao.api; + +import com.cakey.client.dto.LoginReq; +import com.cakey.client.dto.UserInfoRes; + +public interface SocialService { + UserInfoRes login(final String authorizationToken, final LoginReq loginReq); +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccessTokenRes.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccessTokenRes.java new file mode 100644 index 0000000..4efecaa --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccessTokenRes.java @@ -0,0 +1,13 @@ +package com.cakey.client.kakao.api.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccessTokenRes( + String accessToken +) { + public static KakaoAccessTokenRes of(final String accessToken) { + return new KakaoAccessTokenRes(accessToken); + } +} \ No newline at end of file diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccount.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccount.java new file mode 100644 index 0000000..69c493c --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoAccount.java @@ -0,0 +1,6 @@ +package com.cakey.client.kakao.api.dto; + +public record KakaoAccount( + Profile profile +) { +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoUserRes.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoUserRes.java new file mode 100644 index 0000000..2404c68 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/KakaoUserRes.java @@ -0,0 +1,11 @@ +package com.cakey.client.kakao.api.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserRes( + Long id, + KakaoAccount kakaoAccount +) { +} diff --git a/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/Profile.java b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/Profile.java new file mode 100644 index 0000000..fe297d7 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/client/kakao/api/dto/Profile.java @@ -0,0 +1,6 @@ +package com.cakey.client.kakao.api.dto; + +public record Profile( + String nickname +) { +} diff --git a/cakey-auth/src/main/java/com/cakey/jwt/domain/UserRole.java b/cakey-auth/src/main/java/com/cakey/jwt/domain/UserRole.java new file mode 100644 index 0000000..fc1a7b8 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/jwt/domain/UserRole.java @@ -0,0 +1,14 @@ +package com.cakey.jwt.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserRole { + USER("USER"), + ADMIN("ADMIN"), + ; + + private String role; +} diff --git a/cakey-auth/src/main/java/com/cakey/jwt/repository/TokenRepository.java b/cakey-auth/src/main/java/com/cakey/jwt/repository/TokenRepository.java new file mode 100644 index 0000000..363cc0d --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/jwt/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package com.cakey.jwt.repository; + +import com.cakey.jwt.domain.Token; +import java.util.Optional; +import org.springframework.data.repository.CrudRepository; + +public interface TokenRepository extends CrudRepository { + Optional findByRefreshToken(final String refreshToken); + Optional findById(final Long id); +} diff --git a/cakey-auth/src/main/java/com/cakey/jwt/service/TokenService.java b/cakey-auth/src/main/java/com/cakey/jwt/service/TokenService.java new file mode 100644 index 0000000..8bba611 --- /dev/null +++ b/cakey-auth/src/main/java/com/cakey/jwt/service/TokenService.java @@ -0,0 +1,39 @@ +package com.cakey.jwt.service; + +import com.cakey.jwt.domain.Token; +import com.cakey.jwt.repository.TokenRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + + @Transactional + public void saveRefreshToken(final Long userId, final String refreshToken) { + tokenRepository.save(Token.of(userId, refreshToken)); + } + + public Long findIdByRefreshToken( + final String refreshToken + ) { + Token token = tokenRepository.findByRefreshToken(refreshToken) + .orElseThrow( + () -> new RuntimeException("Refresh token not found") + ); + return token.getId(); + } + + @Transactional + public void deleteRefreshToken( + final Long userId + ) { + Token token = tokenRepository.findById(userId).orElseThrow( + () -> new RuntimeException("Refresh token not found") + ); + tokenRepository.delete(token); + } +} \ No newline at end of file diff --git a/cakey-domain/src/main/java/com/cakey/user/domain/User.java b/cakey-domain/src/main/java/com/cakey/user/domain/User.java index bd78412..d95167e 100644 --- a/cakey-domain/src/main/java/com/cakey/user/domain/User.java +++ b/cakey-domain/src/main/java/com/cakey/user/domain/User.java @@ -1,5 +1,7 @@ package com.cakey.user.domain; +import com.cakey.client.SocialType; +import com.cakey.jwt.domain.UserRole; import jakarta.persistence.*; import lombok.*; @@ -20,7 +22,7 @@ public class User { @Column(name = "user_role", nullable = false) @Enumerated(EnumType.STRING) - private UserRole role; + private UserRole role; @Column(name = "social_type", nullable = false) @Enumerated(EnumType.STRING) diff --git a/cakey-domain/src/main/java/com/cakey/user/domain/UserRole.java b/cakey-domain/src/main/java/com/cakey/user/domain/UserRole.java deleted file mode 100644 index 2781d79..0000000 --- a/cakey-domain/src/main/java/com/cakey/user/domain/UserRole.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.cakey.user.domain; - -public enum UserRole { - USER, - ADMIN, - -} diff --git a/cakey-domain/src/main/java/com/cakey/user/dto/AccessTokenGetSuccess.java b/cakey-domain/src/main/java/com/cakey/user/dto/AccessTokenGetSuccess.java new file mode 100644 index 0000000..ee29a54 --- /dev/null +++ b/cakey-domain/src/main/java/com/cakey/user/dto/AccessTokenGetSuccess.java @@ -0,0 +1,9 @@ +package com.cakey.user.dto; + +public record AccessTokenGetSuccess( + String accessToken +) { + public static AccessTokenGetSuccess of(String accessToken) { + return new AccessTokenGetSuccess(accessToken); + } +} diff --git a/cakey-domain/src/main/java/com/cakey/user/dto/LoginSuccessRes.java b/cakey-domain/src/main/java/com/cakey/user/dto/LoginSuccessRes.java new file mode 100644 index 0000000..46cf53e --- /dev/null +++ b/cakey-domain/src/main/java/com/cakey/user/dto/LoginSuccessRes.java @@ -0,0 +1,12 @@ +package com.cakey.user.dto; + +public record LoginSuccessRes( + String accessToken, + String refreshToken +) { + public static LoginSuccessRes of( + final String accessToken, + final String refreshToken) { + return new LoginSuccessRes(accessToken, refreshToken); + } +} \ No newline at end of file diff --git a/cakey-domain/src/main/java/com/cakey/user/repository/UserRepository.java b/cakey-domain/src/main/java/com/cakey/user/repository/UserRepository.java new file mode 100644 index 0000000..41ef3b1 --- /dev/null +++ b/cakey-domain/src/main/java/com/cakey/user/repository/UserRepository.java @@ -0,0 +1,11 @@ +package com.cakey.user.repository; + +import com.cakey.client.SocialType; +import com.cakey.user.domain.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + Optional findBySocialTypeAndSocialId(SocialType socialType, String socialId); +}