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

Naver 로그인 작업 완료 PR(ver.2) #10

Merged
merged 18 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e5184c5
Feat: NaverOauth 클래스 생성
wambatcodeeee Jul 3, 2024
0d57692
Feat: NaverOauth 내부 로직 작성
wambatcodeeee Jul 3, 2024
ae01946
Feat: NaverOauth getUserInfo 메서드 작성
wambatcodeeee Jul 3, 2024
26e294f
Refactor: generateRandomPostFix 클래스화
wambatcodeeee Jul 3, 2024
5e3c546
Fix: 소셜 로그인 증설에 따른 createAccessTokenKakao -> createAccessTokenSocial 수정
wambatcodeeee Jul 3, 2024
a7fc065
Feat: Redis 패키지 내 NaverToken 엔티티 파일 생성
wambatcodeeee Jul 3, 2024
353df81
Feat: Redis 패키지 내 NaverToken repository 파일 생성
wambatcodeeee Jul 3, 2024
3ab1782
Feat: Redis 패키지 내 NaverToken service 파일 생성
wambatcodeeee Jul 3, 2024
6140582
Feat: NaverService 작성 완료
wambatcodeeee Jul 3, 2024
da06f00
Fix: 불필요 import문 제거
wambatcodeeee Jul 3, 2024
9732178
Feat: NaverController 작성 완료
wambatcodeeee Jul 3, 2024
185e7ee
Fix: 오타 수정 및 유저정보 받을 시 인코딩 처리
wambatcodeeee Jul 4, 2024
0f76b07
Feat: Naver 사용자 정보 중 이메일 추출 코드 추가
wambatcodeeee Jul 5, 2024
755b78d
Fix: Naver 사용자 정보(이메일) 추가로 인한 코드 수정
wambatcodeeee Jul 5, 2024
a8fe284
Fix: 클래스 파일 내 오타 수정
wambatcodeeee Jul 5, 2024
6d26914
Refactor: 소셜 로그인 비밀번호 인코딩
wambatcodeeee Jul 5, 2024
0ea4ef1
Fix: 로컬 개발 완료로 인한 Model 메소드 제거(Thymeleaf 관련)
wambatcodeeee Jul 5, 2024
eb4fa8b
Fix: 클래스 파일 내 오타 수정
wambatcodeeee Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/elice/ustory/global/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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> claims = Jwts.parserBuilder().setSigningKey(jwtTokenProvider.getSecretKey()).build()
.parseClaimsJws(jwtToken);
return claims.getBody().get("kakao").toString();
return claims.getBody().get("socialToken").toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public ResponseEntity<LoginResponse> 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<LogoutResponse> KakaoLogout(HttpServletRequest request) {
@RequestMapping(value = "/auth/logout/kakao", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<LogoutResponse> kakaoLogout(HttpServletRequest request) {
LogoutResponse logoutResponse = kakaoService.kakaoLogout(request);
return ResponseEntity.ok().body(logoutResponse);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

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

엇 아 이 로직 살아있었군요? 카카오에서 자동으로 해주는 줄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

어차피 소셜 로그인은 저희 어스토리 로그인창에 아이디 비번을 쳐서 들어갈 필요도 없을 뿐더러, 그냥 그냥 UUID인채로 냅두면 극악의 확률로 뚫릴 수 있습니다. 그래서 디코드 과정은 없이 인코딩먼 해둔겁니다(2중 보안 느낌으로)

Copy link
Contributor

Choose a reason for hiding this comment

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

@yungic 우리 로직에서 소셜로그인은 비밀번호를 입력하는 과정이 따로 없어서, DB에 형식적으로 랜덤 생성한 비밀번호를 넣고 있어요. 그런데 나름대로 랜덤값이라고 해도 비밀번호는 안전한 게 좋으니까 한 번 더 인코딩 거친 듯한..?

소셜로그인 시에도 회원가입 과정을 추가해서 비밀번호를 입력받는 방법도 있긴 한데 소셜로그인 유저한테 아이디 비밀번호가 따로 있는 게 더 헷갈릴 거 같고 번거로워질 거 같아서 이쪽 방법으로 왔슴당

정확한 답변은 누오가 해줄 거 같은디 초벌 한 번 해봤어유

Copy link
Contributor

Choose a reason for hiding this comment

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

그러니까 인코딩만 필요한 상황이라 랜덤 변수를 생성해주는거고 (보안상) 그걸 다시 인코딩 돌린거라는거지? 맞게 이해?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그런 셈이죠


Users builtUser = Users.addUserBuilder()
.email(kakaoUserId+"@ustory.com")
.loginType(Users.LoginType.KAKAO)
.name(kakaoNickname)
.nickname(generatedNickname)
.password(randomPassword)
.password(encodedPassword)
.profileImgUrl("")
.profileDescription("자기소개")
.build();
Expand All @@ -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()
Expand All @@ -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 객체를 문자열로 변환해 반환
}
}
Original file line number Diff line number Diff line change
@@ -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<LoginResponse> naverLogin(@RequestParam(name = "code") String code,
@RequestParam(name = "state") String state,
HttpServletResponse response) {
String naverAccessToken = naverOauth.getNaverToken(code, state);
HashMap<String, Object> 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);
}
wambatcodeeee marked this conversation as resolved.
Show resolved Hide resolved

@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})
Copy link
Contributor

Choose a reason for hiding this comment

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

오와 이렇게 하면 GET POST 동시에 되는구나아 근데 두가지 방향으로 요청이 와요?

public ResponseEntity<LogoutResponse> naverLogout(HttpServletRequest request) {
LogoutResponse logoutResponse = naverService.naverLogout(request);
return ResponseEntity.ok().body(logoutResponse);
}
}
89 changes: 89 additions & 0 deletions src/main/java/com/elice/ustory/global/oauth/naver/NaverOauth.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<MultiValueMap<String, String>> naverTokenRequest = new HttpEntity<>(params, headers);
ResponseEntity<String> 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<String, Object> getUserInfoFromNaverToken(String accessToken){
HashMap<String, Object> userInfo = new HashMap<>();

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);

HttpEntity<MultiValueMap<String, String>> naverUserInfoRequest = new HttpEntity<>(headers);

ResponseEntity<String> 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;
}
}
102 changes: 102 additions & 0 deletions src/main/java/com/elice/ustory/global/oauth/naver/NaverService.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading