diff --git a/src/main/java/com/elice/ustory/global/jwt/JwtTokenProvider.java b/src/main/java/com/elice/ustory/global/jwt/JwtTokenProvider.java index 2d40c739..49b49d48 100644 --- a/src/main/java/com/elice/ustory/global/jwt/JwtTokenProvider.java +++ b/src/main/java/com/elice/ustory/global/jwt/JwtTokenProvider.java @@ -44,20 +44,20 @@ public String createAccessToken(Long userId) { .compact(); } - public String createAccessTokenKakao(Long userId, String kakaoAccessToken, Users.LoginType loginType) { + public String createAccessTokenSocial(Long userId, String accessToken, Users.LoginType loginType) { Claims claims = Jwts.claims(); Date now = new Date(); claims.put("userId", userId); - claims.put("kakao", kakaoAccessToken); + claims.put("socialToken", accessToken); claims.put("loginType", loginType); - log.info("[createKakaoAccessToken] access 토큰(kakao 로그인) 생성 완료"); + log.info("[createSocialAccessToken] access 토큰(소셜 로그인) 생성 완료"); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(new Date(System.currentTimeMillis() + ACCESSTOKEN_VALID_MILISECOND)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); - } //TODO: 코드 중복 되는 부분이 많아서 리팩토링 예정 + } public String createRefreshToken(){ Claims claims = Jwts.claims(); diff --git a/src/main/java/com/elice/ustory/global/jwt/JwtUtil.java b/src/main/java/com/elice/ustory/global/jwt/JwtUtil.java index 6480c979..8c77a48b 100644 --- a/src/main/java/com/elice/ustory/global/jwt/JwtUtil.java +++ b/src/main/java/com/elice/ustory/global/jwt/JwtUtil.java @@ -100,9 +100,9 @@ public long getRemainingTTL(String jwtToken) { return Math.max(remainingMillis, 0) / 1000; } - public String getKakaoToken(String jwtToken){ + public String getSocialToken(String jwtToken){ Jws claims = Jwts.parserBuilder().setSigningKey(jwtTokenProvider.getSecretKey()).build() .parseClaimsJws(jwtToken); - return claims.getBody().get("kakao").toString(); + return claims.getBody().get("socialToken").toString(); } } diff --git a/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoController.java b/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoController.java index 06477b20..802face5 100644 --- a/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoController.java +++ b/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoController.java @@ -64,8 +64,8 @@ public ResponseEntity kakaoLogin(@RequestParam String code, HttpS @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) }) - @RequestMapping(value = "/auth/logout", method = {RequestMethod.GET, RequestMethod.POST}) - public ResponseEntity KakaoLogout(HttpServletRequest request) { + @RequestMapping(value = "/auth/logout/kakao", method = {RequestMethod.GET, RequestMethod.POST}) + public ResponseEntity kakaoLogout(HttpServletRequest request) { LogoutResponse logoutResponse = kakaoService.kakaoLogout(request); return ResponseEntity.ok().body(logoutResponse); } diff --git a/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoService.java b/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoService.java index 290a0141..81553179 100644 --- a/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoService.java +++ b/src/main/java/com/elice/ustory/global/oauth/kakao/KakaoService.java @@ -17,13 +17,14 @@ import com.elice.ustory.global.jwt.JwtUtil; import com.elice.ustory.global.redis.kakao.KakaoTokenService; import com.elice.ustory.global.redis.refresh.RefreshTokenService; +import com.elice.ustory.global.util.RandomGenerator; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.util.Random; import java.util.UUID; @Service @@ -39,17 +40,20 @@ public class KakaoService { private final JwtUtil jwtUtil; private final KakaoOauth kakaoOauth; private final UserService userService; + private final RandomGenerator randomGenerator; + private final PasswordEncoder passwordEncoder; public void kakaoSignUp(String kakaoUserId, String kakaoNickname){ String randomPassword = String.valueOf(UUID.randomUUID()).substring(0,8); - String generatedNickname = kakaoNickname + "#" + generateRandomPostfix(); + String encodedPassword = passwordEncoder.encode(randomPassword); + String generatedNickname = kakaoNickname + "#" + randomGenerator.generateRandomPostfix(); Users builtUser = Users.addUserBuilder() .email(kakaoUserId+"@ustory.com") .loginType(Users.LoginType.KAKAO) .name(kakaoNickname) .nickname(generatedNickname) - .password(randomPassword) + .password(encodedPassword) .profileImgUrl("") .profileDescription("자기소개") .build(); @@ -71,7 +75,7 @@ public LoginResponse kakaoLogin(String kakaoUserId, HttpServletResponse response Users loginUser = userRepository.findByEmail(kakaoUserId+"@ustory.com") .orElseThrow(() -> new NotFoundException("해당 유저를 찾을 수 없습니다.")); - String accessToken = jwtTokenProvider.createAccessTokenKakao(loginUser.getId(), kakaoToken, loginUser.getLoginType()); + String accessToken = jwtTokenProvider.createAccessTokenSocial(loginUser.getId(), kakaoToken, loginUser.getLoginType()); String refreshToken = jwtTokenProvider.createRefreshToken(); LoginResponse loginResponse = LoginResponse.builder() @@ -91,24 +95,11 @@ public LoginResponse kakaoLogin(String kakaoUserId, HttpServletResponse response public LogoutResponse kakaoLogout(HttpServletRequest request) { String accessToken = jwtUtil.getTokenFromRequest(request); - String kakaoToken = jwtUtil.getKakaoToken(accessToken); + String kakaoToken = jwtUtil.getSocialToken(accessToken); kakaoOauth.expireKakaoToken(kakaoToken); kakaoTokenService.removeKakaoTokenInfo(accessToken); userService.logout(request); return LogoutResponse.builder().success(true).build(); } - - public String generateRandomPostfix() { - int leftLimit = 48; // 숫자 '0'의 ASCII 코드 - int rightLimit = 122; // 알파벳 'z'의 ASCII 코드 - int stringLength = 4; - Random random = new Random(); - - return random.ints(leftLimit, rightLimit + 1) // leftLimit(포함) 부터 rightLimit+1(불포함) 사이의 난수 스트림 생성 - .filter(i -> (i < 57 || i >= 65) && ( i <= 90 || i >= 97)) // ASCII 테이블에서 숫자, 대문자, 소문자만 사용함 - .limit(stringLength) // 생성된 난수를 지정된 길이로 잘라냄 - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) // 생성된 난수를 ASCII 테이블에서 대응되는 문자로 변환 - .toString(); // StringBuilder 객체를 문자열로 변환해 반환 - } } diff --git a/src/main/java/com/elice/ustory/global/oauth/naver/NaverController.java b/src/main/java/com/elice/ustory/global/oauth/naver/NaverController.java new file mode 100644 index 00000000..a2fb0d5b --- /dev/null +++ b/src/main/java/com/elice/ustory/global/oauth/naver/NaverController.java @@ -0,0 +1,75 @@ +package com.elice.ustory.global.oauth.naver; + +import com.elice.ustory.domain.user.dto.LoginResponse; +import com.elice.ustory.domain.user.dto.LogoutResponse; +import com.elice.ustory.domain.user.service.UserService; +import com.elice.ustory.global.exception.dto.ErrorResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.HashMap; + +@Tag(name = "NAVER", description = "NAVER OAUTH API") +@Controller +@Slf4j +@RequestMapping +@RequiredArgsConstructor +public class NaverController { + private final NaverOauth naverOauth; + private final NaverService naverService; + private final UserService userService; + + @Operation(summary = "NAVER LOGIN API", description = "네이버 로그인") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = "application/json", schema = @Schema(implementation = LoginResponse.class))), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + + @RequestMapping(value = "/login/oauth2/code/naver", method = {RequestMethod.GET, RequestMethod.POST}) + public ResponseEntity naverLogin(@RequestParam(name = "code") String code, + @RequestParam(name = "state") String state, + HttpServletResponse response) { + String naverAccessToken = naverOauth.getNaverToken(code, state); + HashMap userInfo = naverOauth.getUserInfoFromNaverToken(naverAccessToken); + + String nickname = (String) userInfo.get("nickname"); + String naverEmail = (String) userInfo.get("email"); + + if(!userService.checkExistByEmail(naverEmail)){ + naverService.naverSignUp(nickname, naverEmail); + } + + LoginResponse loginResponse = naverService.naverLogin(naverEmail, response, naverAccessToken); + + log.info("[naverLogin] 네이버 닉네임: {}", nickname); + return ResponseEntity.ok().body(loginResponse); + } + + @Operation(summary = "NAVER LOGOUT API", description = "네이버 로그아웃") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = "application/json", schema = @Schema(implementation = LogoutResponse.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))) + }) + @RequestMapping(value = "/auth/logout/naver", method = {RequestMethod.GET, RequestMethod.POST}) + public ResponseEntity naverLogout(HttpServletRequest request) { + LogoutResponse logoutResponse = naverService.naverLogout(request); + return ResponseEntity.ok().body(logoutResponse); + } +} diff --git a/src/main/java/com/elice/ustory/global/oauth/naver/NaverOauth.java b/src/main/java/com/elice/ustory/global/oauth/naver/NaverOauth.java new file mode 100644 index 00000000..7127982a --- /dev/null +++ b/src/main/java/com/elice/ustory/global/oauth/naver/NaverOauth.java @@ -0,0 +1,89 @@ +package com.elice.ustory.global.oauth.naver; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +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.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Objects; + +@Slf4j +@Component +@Getter +public class NaverOauth { + @Value("${naver.clientId}") + private String naverClientId; + + @Value("${naver.secret}") + private String naverSecretKey; + + @Value("${naver.loginRedirectUri}") + private String naverLoginRedirectUri; + + @Value("${naver.logoutRedirectUri}") + private String naverLogoutRedirectUri; + + @Value("${naver.tokenUri}") + private String naverTokenUri; + + @Value("${naver.userInfo}") + private String userInfoUri; + + public String getNaverToken(String code, String state){ + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", naverClientId); + params.add("client_secret", naverSecretKey); + params.add("code", code); + params.add("state", state); + + HttpEntity> naverTokenRequest = new HttpEntity<>(params, headers); + ResponseEntity response = restTemplate.exchange(naverTokenUri, HttpMethod.POST, naverTokenRequest, String.class); + + String responseBody = response.getBody(); + JsonObject asJsonObject = null; + if(responseBody != null) asJsonObject = JsonParser.parseString(responseBody).getAsJsonObject(); + return asJsonObject.get("access_token").getAsString(); + } + + public HashMap getUserInfoFromNaverToken(String accessToken){ + HashMap userInfo = new HashMap<>(); + + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + + HttpEntity> naverUserInfoRequest = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(userInfoUri, HttpMethod.POST, naverUserInfoRequest, String.class); + log.info("response = {}", response); + String responseBody = response.getBody(); + if(responseBody != null){ + responseBody = new String(responseBody.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + } + JsonElement element = JsonParser.parseString(Objects.requireNonNull(responseBody)); + + String nickname = element.getAsJsonObject().get("response").getAsJsonObject().get("nickname").getAsString(); + String email = element.getAsJsonObject().get("response").getAsJsonObject().get("email").getAsString(); + + userInfo.put("nickname", nickname); + userInfo.put("email", email); + + return userInfo; + } +} diff --git a/src/main/java/com/elice/ustory/global/oauth/naver/NaverService.java b/src/main/java/com/elice/ustory/global/oauth/naver/NaverService.java new file mode 100644 index 00000000..8aa5836c --- /dev/null +++ b/src/main/java/com/elice/ustory/global/oauth/naver/NaverService.java @@ -0,0 +1,102 @@ +package com.elice.ustory.global.oauth.naver; + +import com.elice.ustory.domain.diary.entity.Color; +import com.elice.ustory.domain.diary.entity.Diary; +import com.elice.ustory.domain.diary.entity.DiaryCategory; +import com.elice.ustory.domain.diary.repository.DiaryRepository; +import com.elice.ustory.domain.diaryUser.entity.DiaryUser; +import com.elice.ustory.domain.diaryUser.entity.DiaryUserId; +import com.elice.ustory.domain.diaryUser.repository.DiaryUserRepository; +import com.elice.ustory.domain.user.dto.LoginResponse; +import com.elice.ustory.domain.user.dto.LogoutResponse; +import com.elice.ustory.domain.user.entity.Users; +import com.elice.ustory.domain.user.repository.UserRepository; +import com.elice.ustory.domain.user.service.UserService; +import com.elice.ustory.global.exception.model.NotFoundException; +import com.elice.ustory.global.jwt.JwtTokenProvider; +import com.elice.ustory.global.jwt.JwtUtil; +import com.elice.ustory.global.redis.naver.NaverTokenService; +import com.elice.ustory.global.redis.refresh.RefreshTokenService; +import com.elice.ustory.global.util.RandomGenerator; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class NaverService { + private final UserRepository userRepository; + private final DiaryRepository diaryRepository; + private final DiaryUserRepository diaryUserRepository; + private final UserService userService; + private final RefreshTokenService refreshTokenService; + private final NaverTokenService naverTokenService; + private final JwtTokenProvider jwtTokenProvider; + private final JwtUtil jwtUtil; + private final RandomGenerator randomGenerator; + private final PasswordEncoder passwordEncoder; + + public void naverSignUp(String naverNickname, String naverEmail){ + String randomPassword = String.valueOf(UUID.randomUUID()).substring(0,8); + String encodedPassword = passwordEncoder.encode(randomPassword); + String generatedNickname = naverNickname + "#" + randomGenerator.generateRandomPostfix(); + + Users builtUser = Users.addUserBuilder() + .email(naverEmail) + .loginType(Users.LoginType.NAVER) + .name(naverNickname) + .nickname(generatedNickname) + .password(encodedPassword) + .profileImgUrl("") + .profileDescription("자기소개") + .build(); + + userRepository.save(builtUser); + + Diary userDiary = new Diary( + String.format("%s의 다이어리", builtUser.getNickname()), + "기본 DiaryImgUrl", + DiaryCategory.INDIVIDUAL, + String.format("%s의 개인 다이어리", builtUser.getNickname()), + Color.RED + ); + diaryRepository.save(userDiary); + diaryUserRepository.save(new DiaryUser(new DiaryUserId(userDiary,builtUser))); + } + + public LoginResponse naverLogin(String naverEmail, HttpServletResponse response, String naverToken){ + Users loginUser = userRepository.findByEmail(naverEmail) + .orElseThrow(() -> new NotFoundException("해당 유저를 찾을 수 없습니다.")); + + String accessToken = jwtTokenProvider.createAccessTokenSocial(loginUser.getId(), naverToken, loginUser.getLoginType()); + String refreshToken = jwtTokenProvider.createRefreshToken(); + + LoginResponse loginResponse = LoginResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + log.info("[getLogInResult] LogInResponse 객체에 값 주입"); + response.addHeader("Authorization", accessToken); + + refreshTokenService.saveTokenInfo(loginUser.getId(), refreshToken, accessToken, 60 * 60 * 24 * 7); + naverTokenService.saveNaverTokenInfo(loginUser.getId(), naverToken, accessToken); + + log.info("[logIn] 정상적으로 로그인되었습니다. id : {}, token : {}", loginUser.getId(), loginResponse.getAccessToken()); + return loginResponse; + } + + public LogoutResponse naverLogout(HttpServletRequest request) { + String accessToken = jwtUtil.getTokenFromRequest(request); + naverTokenService.removeNaverTokenInfo(accessToken); + userService.logout(request); + + return LogoutResponse.builder().success(true).build(); + } +} diff --git a/src/main/java/com/elice/ustory/global/redis/naver/NaverToken.java b/src/main/java/com/elice/ustory/global/redis/naver/NaverToken.java new file mode 100644 index 00000000..d3268fe2 --- /dev/null +++ b/src/main/java/com/elice/ustory/global/redis/naver/NaverToken.java @@ -0,0 +1,21 @@ +package com.elice.ustory.global.redis.naver; + +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "naverToken", timeToLive = 60 * 60 * 24 * 7) +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class NaverToken { + @Id + private String id; + private String naverToken; + + @Indexed + private String accessToken; +} diff --git a/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenRepository.java b/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenRepository.java new file mode 100644 index 00000000..06e5e922 --- /dev/null +++ b/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenRepository.java @@ -0,0 +1,9 @@ +package com.elice.ustory.global.redis.naver; + +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface NaverTokenRepository extends CrudRepository { + Optional findByAccessToken(String accessToken); +} diff --git a/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenService.java b/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenService.java new file mode 100644 index 00000000..ef71e3f4 --- /dev/null +++ b/src/main/java/com/elice/ustory/global/redis/naver/NaverTokenService.java @@ -0,0 +1,25 @@ +package com.elice.ustory.global.redis.naver; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class NaverTokenService { + private final NaverTokenRepository naverTokenRepository; + + public void saveNaverTokenInfo(Long userId, String naverToken, String accessToken) { + naverTokenRepository.save(new NaverToken(String.valueOf(userId), naverToken, accessToken)); + } + + public void removeNaverTokenInfo(String accessToken) { + naverTokenRepository.findByAccessToken(accessToken) + .ifPresent(naverTokenRepository::delete); + } + + public Optional getByAccessToken(String accessToken){ + return naverTokenRepository.findByAccessToken(accessToken); + } +} diff --git a/src/main/java/com/elice/ustory/global/util/RandomGenerator.java b/src/main/java/com/elice/ustory/global/util/RandomGenerator.java new file mode 100644 index 00000000..b0957aa7 --- /dev/null +++ b/src/main/java/com/elice/ustory/global/util/RandomGenerator.java @@ -0,0 +1,21 @@ +package com.elice.ustory.global.util; + +import org.springframework.stereotype.Component; + +import java.util.Random; + +@Component +public class RandomGenerator { + public String generateRandomPostfix() { + int leftLimit = 48; // 숫자 '0'의 ASCII 코드 + int rightLimit = 122; // 알파벳 'z'의 ASCII 코드 + int stringLength = 4; + Random random = new Random(); + + return random.ints(leftLimit, rightLimit + 1) // leftLimit(포함) 부터 rightLimit+1(불포함) 사이의 난수 스트림 생성 + .filter(i -> (i < 57 || i >= 65) && ( i <= 90 || i >= 97)) // ASCII 테이블에서 숫자, 대문자, 소문자만 사용함 + .limit(stringLength) // 생성된 난수를 지정된 길이로 잘라냄 + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) // 생성된 난수를 ASCII 테이블에서 대응되는 문자로 변환 + .toString(); // StringBuilder 객체를 문자열로 변환해 반환 + } +}