OAuth.. 오래 전에 만들어둔 고대의(?) 코드다.
이미 회원가입과 로그인 기능을 구현해놨고, 이제는 소셜로그인으로 회원가입이 아니라 기존계정과 연동하는 거 정도로 만들 생각이다. 그리고 로그인 기능을 만들기

이런방식으로 진행할 예정이다.
컨트롤러는 나중에 만들거니까 서비스부터 만들자 그리고 oauthprovider = 4일 때 홍익대 재학생로그인을하려고 했는데 그건 없앨 예정. 어차피 회원가입 기능으로 이미 만들어두었다.
우선 그러면 OAuth 기능에서 '로그인'과 'SNS 계정연동을 위한 로그인'을 분리해야한다. '로그인' 기능은 로그인 후 login/success로 연결시켜서 JWT토큰을 발급받으면 되지만, 'SNS 계정연동을 위한 로그인' 기능은 로그인 후 DB에서 유저정보를 찾아서 또 저장하는 일을 해야하기 때문
그렇기 위해서는 SecurityConfig에서 조건에 따라서 로그인 후 엔드포인트를 다르게 설정해야하는데, 나는 '로그인'과 '계정연동' 단 두가지 경우 밖에 없으므로 불리언 값을 이용할까한다.
package com.hongchelin.dto;
import lombok.Builder;
import lombok.Getter;
public class whatForSignupDTO {
private static ThreadLocal<Boolean> isForLogin;
public ThreadLocal<Boolean> isForLogin() {
return isForLogin; //로그인일 경우에는 true, 회원 DB 연동을 위해서는 false 로 설정됩니다.
}
public void setForLogin(Boolean value) {
isForLogin.set(value);
}
}
다음과 같이 정적 변수를 활용하여 isForLogin을 만들었다. 로그인을 할 때는 true, 회원 DB 연동을 위해선 false로 설정되며, 각 메서드에서 OAuth로 로그인 요청을 넘길 때마다 저 정적 변수를 설정하고 넘어간다.
또한 로그인 요청이 겹쳐서 A가 true로 설정한 것을 B가 false로 바꾼 뒤에 다시 A가 정적변수의 값에 따라 조건문을 나누면 오류가 생기기 때문에 ThreadLocal로 값을 선언했고, 게터와 빌더는 정적변수에서는 사용할 수 없어서 게터세터를 만들어주었다.
.oauth2Login(oauth -> oauth
.successHandler((request, response, authentication) -> {
Boolean isforlogin = whatForSignupDTO.isForLogin().get();
if (isforlogin != null && isforlogin) {
response.sendRedirect("/api/login/success");
} else {
response.sendRedirect("/regisInDB");
}
})
)
따라서 이제는 저 URL의 GET 메서드를 받는 클래스와 서비스를 만들면 된다.
사실 http 메서드를 살리면서 if문을 사용해야하기에 저렇게 만들었다.
@Service
public class registerOAuthService extends loginService {
private final JWTFilter jwtFilter;
private final MemberRepositoryInterface memberRepository;
public registerOAuthService(JWTFilter jwtFilter,
MemberRepositoryInterface memberRepository) {
this.jwtFilter = jwtFilter;
this.memberRepository = memberRepository;
}
@Override
public void setLoginFor() {
whatForSignupDTO.setForLogin(false);
}
}
우선 회원DB 연동 요청 URL로 요청이 들어온 경우, 기존 loginService 기능을 상속하고 생성자에다만 setForLogin을 false로 설정한다. (부모클래스인 loginService, 즉 일반 로그인 요청에는 setForLogin이 true로 맞춰진다.)
우선 기존에 일반 로그인 기능, 그니까 /api/login/success 엔드포인트에서의 유저 정보를 대조하고 JWT토큰을 반환하는 기능부터 만들어보자면
public ResponseEntity<ResponseDTO> login(String secret, MemberRequestDTO memberRequestDTO) {
String userId = memberRequestDTO.getUserId();
String password = memberRequestDTO.getPassword();
Integer count = memberRepository.countByUserIdAndPassword(userId, password);
if (count == 1) { //정보 있음. 로그인
String refreshToken = jwtFilter.createRefreshToken(secret, userId);
ResponseDTO responseDTO = ResponseDTO.builder()
.status(200)
.message("성공")
.accessToken(jwtFilter.createToken(secret, userId))
.refreshToken(refreshToken)
.build();
Token token = Token.builder()
.userId(userId)
.refreshToken(refreshToken)
.build();
tokenRepository.save(token);
return ResponseEntity.status(HttpStatus.OK).body(responseDTO);
} else { //로그인 정보 없음
ResponseDTO responseDTO = ResponseDTO.builder()
.status(400)
.message("아이디 또는 비밀번호가 올바르지 않습니다.")
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseDTO);
}
}
}
우선 일반로그인을 처리하는 코드
public class loginSuccessService extends LoginMainService {
private final TokenRepositoryInterface tokenRepositoryInterface;
private JWTFilter jwtFilter;
private MemberRepositoryInterface memberRepository;
@Autowired
public loginSuccessService(JWTFilter jwtFilter,
MemberRepositoryInterface memberRepository,
TokenRepositoryInterface tokenRepositoryInterface) {
super(memberRepository, jwtFilter, tokenRepositoryInterface);
this.tokenRepositoryInterface = tokenRepositoryInterface;
}
public ResponseEntity<ResponseDTO> checkUserInformation(String secret,
OAuth2User oauth2user,
HttpServletRequest request,
OAuth2AuthenticationToken OAuth2request) {
String registrationId = OAuth2request.getAuthorizedClientRegistrationId();
if (registrationId.equals("google")) {
String email = oauth2user.getAttribute("email");
return loginhelp(secret, email);
} else if (registrationId.equals("naver")) {
Map<String, Object> identifiers = (Map<String, Object>) (oauth2user.getAttributes().get("response"));
String identifier = identifiers.get("id").toString();
return loginhelp(secret, identifier);
} else {
String identifier = oauth2user.getAttribute("id").toString();
return loginhelp(secret, identifier);
}
}
public ResponseEntity<ResponseDTO> loginhelp(String secret, String identifier) {
Optional<Member> memberOptional = memberRepository.findBySocialEmail(identifier);
if (memberOptional.isPresent()) {
MemberRequestDTO memberRequestDTO = MemberRequestDTO.builder()
.userId(memberOptional.get().getUserId())
.password(memberOptional.get().getPassword())
.build();
return login(secret, memberRequestDTO);
} else {
throw new UnauthorizedException();
}
}
}
그리고 OAuth 전용으로 로그인을 처리하는 클래스
코드수정이 많이 있었는데, 우선 기존에는 일일이 조건문마다 토큰 만들고 정보 저장하고 했다면 이제는 loginService 클래스를 상속받아 그대로 로직을 처리한다. 또한 상속받은 login 메서드에게 값을 모으고 전달하기 위해 loginhelp 메서드를 추가해 checkUserInformation 메서드는 이제 로그인된 정보만 받아서 이메일 (또는 식별자)만 뽑아 loginhelp에게 전달해주기만 하면 된다.

실행결과는 성공
DB에 저장된 값이 없어서 에러가 터지는게 맞다.
DB회원과 SNS계정 연동하기
이건 우선은 정적 변수가 false 일 때 진행된다. JWT토큰으로 회원정보를 받아서 DB에서 찾은 후에 SNS계정 정보를 저장하는 일련의 과정이다.
public void setLoginFor() {
whatForSignupDTO.setForLogin(true);
}
public ResponseEntity<?> loginMethodSorting(int oauthprovider,
String secret,
HttpServletRequest request) {
setLoginFor();
if (!whatForSignupDTO.isForLogin().get()) { //true --> 일반로그인 / false --> DB 회원정보 저장
setSessionJWT(secret, request); //회원정보 저장일 때만 세션에 유저정보 저장하기
}
우선은 loginService를 수정하고
import com.hongchelin.Repository.MemberRepositoryInterface;
import com.hongchelin.dto.whatForSignupDTO;
import com.hongchelin.service.JWT.JWTFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Service;
@Service
public class registerOAuthService extends loginService {
private final JWTFilter jwtFilter;
private final MemberRepositoryInterface memberRepository;
public registerOAuthService(JWTFilter jwtFilter,
MemberRepositoryInterface memberRepository) {
this.jwtFilter = jwtFilter;
this.memberRepository = memberRepository;
}
@Override
public void setLoginFor() {
whatForSignupDTO.setForLogin(false);
}
@Override
public void setSessionJWT(String secret,
HttpServletRequest request) {
String token = jwtFilter.getTokenFromHeader(secret, request).getAccessToken();
HttpSession session = request.getSession();
session.setAttribute("token", token);
}
}
어차피 '소셜로그인'이라는 기능자체는 똑같기 때문에 로그인 기능은 loginService에서 상속받아서 처리하고, 여기서는 회원DB에 접근하는지, 일반 소셜로그인인지 기능을 구분하는 정적변수를 만지고 JWT토큰을 받아서 세션에 저장하는 메서드만 오버라이드 해줬다.
부모클래스인 loginService 클래스에서 그 두 메서드들은 각각 정적변수를 true로 설정하고, 추상메서드로 설정해두었다.
사실 이게 맞는 방향인지도 모르겠고 상속관련해서 클래스 간 관계가 헷갈린다. 이래서 설계부터 잘해야합니다......
public ResponseEntity<?> registerOAuth(String secret,
OAuth2User oauth2user,
HttpServletRequest request,
OAuth2AuthenticationToken OAuth2request) {
String registrationId = OAuth2request.getAuthorizedClientRegistrationId();
if (registrationId.equals("google")) {
String email = oauth2user.getAttribute("email");
HttpSession session = request.getSession();
if (session != null) {
String token = session.getAttribute("token").toString();
String userId = jwtFilter.checkValidityAndReturnUserRoles(secret, token).getMemberInfo().getIdentifier();
Member member = memberRepository.findByUserId(userId).get(0);
member.setSocialEmail(email);
memberRepository.save(member);
return ResponseEntity.ok().build();
} else {
throw new UnauthorizedException();
}
} else if (registrationId.equals("naver")) {
Map<String, Object> identifiers = (Map<String, Object>) (oauth2user.getAttributes().get("response"));
String identifier = identifiers.get("id").toString();
HttpSession session = request.getSession();
if (session != null) {
String token = session.getAttribute("token").toString();
String userId = jwtFilter.checkValidityAndReturnUserRoles(secret, token).getMemberInfo().getIdentifier();
Member member = memberRepository.findByUserId(userId).get(0);
member.setSocialEmail(identifier);
memberRepository.save(member);
return ResponseEntity.ok().build();
} else {
throw new UnauthorizedException();
}
} else {
Map<String, Object> identifiers = (Map<String, Object>) (oauth2user.getAttributes().get("response"));
String identifier = identifiers.get("id").toString();
HttpSession session = request.getSession();
if (session != null) {
String token = session.getAttribute("token").toString();
String userId = jwtFilter.checkValidityAndReturnUserRoles(secret, token).getMemberInfo().getIdentifier();
Member member = memberRepository.findByUserId(userId).get(0);
member.setSocialEmail(identifier);
memberRepository.save(member);
return ResponseEntity.ok().build();
} else {
throw new UnauthorizedException();
}
}
}
이부분은 로그인 후 이어지는 화면에서 처리할 것
저장해두었던 세션에서 JWT토큰을 받아서 회원을 찾아서 SNS 계정을 저장한다.
public class registerOAuthMainService {
private final MemberRepositoryInterface memberRepository;
private final JWTFilter jwtFilter;
public registerOAuthMainService(MemberRepositoryInterface memberRepository,
JWTFilter jwtFilter) {
this.memberRepository = memberRepository;
this.jwtFilter = jwtFilter;
}
public ResponseEntity<?> registerOAuth(String secret,
OAuth2User oauth2user,
HttpServletRequest request,
OAuth2AuthenticationToken OAuth2request) {
String registrationId = OAuth2request.getAuthorizedClientRegistrationId();
String identifier;
if (registrationId.equals("google")) {
identifier = oauth2user.getAttribute("email");
} else if (registrationId.equals("naver")) {
Map<String, Object> identifiers = (Map<String, Object>) (oauth2user.getAttributes().get("response"));
identifier = identifiers.get("id").toString();
} else {
Map<String, Object> identifiers = (Map<String, Object>) (oauth2user.getAttributes().get("response"));
identifier = identifiers.get("id").toString();
}
return reachSession(secret, request, identifier);
}
private ResponseEntity<ResponseDTO> reachSession (String secret,
HttpServletRequest request,
String identifier) {
HttpSession session = request.getSession();
if (session != null) {
String token = session.getAttribute("token").toString();
String userId = jwtFilter.checkValidityAndReturnUserRoles(secret, token)
.getMemberInfo()
.getIdentifier();
if (!jwtFilter.checkValidityAndReturnUserRoles(secret, token).getValidity()){
throw new UnauthorizedException();
}
Member member = memberRepository.findByUserId(userId).get(0);
member.setSocialEmail(identifier);
memberRepository.save(member);
ResponseDTO responseDTO = ResponseDTO.builder()
.status(200)
.message("성공")
.build();
return ResponseEntity.ok(responseDTO);
} else {
throw new UnauthorizedException();
}
}
}
'세션 접근기능'이라는 것이 겹치기 때문에 이렇게 메서드로 따로 빼주었다.
우선 이정도로하고 컨트롤러와 연결지어서 테스트를 해봐야한다.
테스트
대충 저정도로 코드를 짜고 컨트롤러와 URL을 연결해서 테스트를 돌리기로했는데.. OAuth 기능은 POSTMAN에서 처리할 수 없어서 어떻게 할까 고민 중이다.

이렇게 로그인을하고나면 SOCIAL_EMAIL 컬럼이 추가되어있는 것을 볼 수 있다.
다만 갑자기 세션이 안불러와지고 NullException이 터져버려서 뭐지했는데,
@Override
public void setSessionJWT(String secret,
HttpServletRequest request,
HttpServletResponse response) {
String token = jwtFilter.getTokenFromHeader(secret, request).getAccessToken();
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
System.out.println("쿠키저장완료!");
}
Cookie[] list = request.getCookies();
String token = null;
for (Cookie cookie : list) {
if ("token".equals(cookie.getName())) {
token = cookie.getValue();
} else {
throw new UnauthorizedException();
}
}
if (token == null) {
throw new UnauthorizedException();
}
String userId = jwtFilter.checkValidityAndReturnUserRoles(secret, token)
.getMemberInfo()
.getIdentifier();
세션을 만드는 엔드포인트와 세션을 불러오는 엔드포인트가 달라서 서로 호환이 안된다. 그래서 쿠키로 교체함

디버깅의 흔적...
펄스! -> 위에서 다뤘던 정적변수가 false로 저장됨
그래! -> 정적변수를 false로 감지함

이렇게 잘 작동된다. 현재는 JWT 토큰이 헤더에 담겨있지 않아서 오류를 반환하는데 그게 맞다.
근데 정적변수를 다루는 것에서 약간의 문제가 생겼다. 정적변수를 바꾸는 것으로 설정해도 계속 false로 고정 중이야.. 어떻게 해야하나.. 암튼 내일 다시 해볼 예정
그냥 쿠키로 바꿀까 생각 중이다.
'팀 프로젝트 > [2025][GDG]홍대 맛집 아카이빙 프로젝트' 카테고리의 다른 글
| [GDG] 홍대 맛집 아카이빙 프로젝트 #20 - mySQL과 각종 코드 보강하기 (6) | 2025.08.18 |
|---|---|
| [GDG] 홍대 맛집 아카이빙 프로젝트 #19 - OAuth 되살리기 및 정규화 보강하기 (3) | 2025.08.16 |
| [GDG]홍대 맛집 아카이빙 프로젝트 백엔드 개발 #6.2. - Redis (1) | 2025.08.11 |
| [GDG]홍대 맛집 아카이빙 프로젝트 #17 - 테스트 디버깅 (3) | 2025.08.10 |
| [GDG]홍대 맛집 아카이빙 프로젝트 #16 - 테스트 돌리기 (2) | 2025.08.09 |