요즘 마케팅, 상품기획, 콘텐츠 기획에서 많이 보이는 키워드인
'나만의 유형검사' 를 주제로한
디프백, '디자인' '프론트엔드' '백엔드'
연계 딥다이브를 6주간 진행했었습니다.
23.05.26 ~ 23.07.06
시작전 넘블에서 마련한 오프라인 네트워킹을 통해
팀원들을 볼 수 있었고 그 덕분인지
이후 온라인(디스코드)에서 크게 어색함을 찾아볼 수 없었습니다.
여러 사람들과 함께 공통 목표를 통해 나가면서
재미도 있었고 트러블도 있었지만 무엇보다
무언가를 함께 만들어 간다는 것과 디스코드가 쉬질 않아 그동안 한 배를 탄 동료가 있다는 것이 좋았습니다.
회사일 때문에 많은 시간을 투자할 수 없었고 많이 미흡한 면도 있었기에
아쉬움이 많았지만 이번 딥다이브로 프론트 개발자와의 협업을 통해 백엔드로서
겪을 수 있는 문제들을 직면하고 해결해 나가면서 성장할 수 있었습니다.
넘블 감사합니다.
소셜로그인
소셜로그인 부분을 담당하게 되었고 구현에 들어가기전
로그인 전과정 흐름을 파악한 후에 구현하기로 생각하였습니다.
1차 설계
회의 결과를 바탕으로
카카오에서 받은 회원정보로 db 검색을 하여 비회원일 경우
로그인까지 처리하는 로직으로 변경합니다.
카카오에서 발급 받은 토큰으로 서버 인증/인가에 사용할까 생각했지만
그렇게 된다면 카카오에 대한 의존성이 높아지고 토큰 검증과 발급시 카카오에 요청하고 응답을 받아야되기 때문에
느려질 것으로 판단하여 서버자체 토큰을 발급하여 JWT토큰인증인가 방식으로 진행하기로 결정했습니다.
2차 수정
카카오에서 발급받은 토큰(Access Token)은 소셜 프로필 정보를 얻는 용도로만 활용하고 (그외 로그아웃을 위해 저장할 필요가 있음..)
서버 인증/인가 용도로 JWT를 발급하여 사용하기로 판단
3차 수정 최종
서버 토큰 검증 플로우 설계,
소셜 로그인 과정에서 로그인한 적이 있는 경우 db에 저장된 access token 응답,
로그인한 적 없는 경우 새로 토큰을 발급하여 저장 후 응답
※ API 문서화 툴인 스웨거를 사용하고 있기 때문에
인증 부분 추가
Components components = new Components()
.addSecuritySchemes("bearer-key", new SecurityScheme()
.type(Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(In.HEADER)
.name("Authorization"));
트러블 슈팅
카카오 로그인 토큰발급 401 Unauthorized: [no body]
카카오 로그인 토큰발급 401 Unauthorized: [no body] 문의드립니다
헤더에 필수적으로 추가해야할 데이터가 있기 때문에 해당 코드를 추가해야합니다.
@Override
public ResponseEntity<String> requestToken(Stringcode) {
// MultiValueMap 해당 자료구조는 추가된 데이터 순서를 보장해주는 Map 자료구조
// 토큰을 요청하는 URI 을 구성할 때 query들의 순서에 맞게 구성되어야 함.
MultiValueMap<String,String>params= new LinkedMultiValueMap<>();
// Content-type: application/x-www-form-urlencoded; 으로 호출
HttpHeadershttpHeaders= new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // 헤더 설정
httpHeaders.add("Accept", "application/json");
params.add("grant_type", "authorization_code");
params.add("client_id" , clientId);
params.add("redirect_uri", kakaoRedirectUrl);
params.add("code",code); // 필수로 받을 경우
HttpEntity<MultiValueMap<String,String>> request = new HttpEntity<>(params,httpHeaders);
ResponseEntity<String>strTokensResponseEntity = restTemplate.postForEntity(tokenUrl,request, String.class); // 요청
log.info(strTokensResponseEntity.getBody());
if (strTokensResponseEntity.getStatusCode() != HttpStatus.OK){
throw new JsonParseException("strTokensResponseEntity is not found");
}
return strTokensResponseEntity;
}
400, KOE320
카카오 로그인 토큰 요청시 에러 발생
인증URI (카카오로그인 창) 에서 로그인 후 발급 받은 code를 가지고 카카오에 토큰을 요청해야함.
이때 해당 code는 한번 밖에 사용할 수 없기 때문에 2번 이상 사용하게 되면 KOE320 에러가 발생
그리고
악명 높았던
카카오 로그인 프론트-백엔드 연동
카카오 로그인 - 프론트와 연동시 제대로 값을 받을 수 없는 문제 발생
인증 URI(카카오 로그인 창) 이후 카카오에서 보낸 redirect를 벡엔드에서 받아서 처리를 했기 때문에 값을 제대로 받을 수가 없었습니다.
프론트엔드에서는 자신들이 보낸 소셜로그인 요청에 대한 응답이 아니라 소셜로그인 창 이후는 카카오에서 리다렉트 경로로 요청을 보낸 것이기 때문에 값을 받을 수 없었습니다.
그래서 로직을 변경해서, 기존에 백엔드 에서 리다이렉트를 받아 진행하는 것이 아니라 프론트엔드에서 소셜로그인을 한후 리다이렉트를 통해 받은 code를 통해 서버 토큰을 받도록 하였습니다.
그래서 redirect_uri 를 프론트엔드 도메인 주소로 설정하여 카카오에 리다이렉트를 프론트엔드쪽으로 요청하게 수정하였고
프론트엔드는 카카오에서 얻은 code를 백엔드에 전달하여 벡엔드에서 카카오에 토큰발급을 요청하여 카카오 Access Token을 얻으면서 로직을 이어나갔습니다.
참고: https://velog.io/@milmilkim/카카오-소셜로그인-프론트엔드-리액트-파트
그 밖에 Preflight 문제
CORS와 Preflight에 관한 이슈 해결 (tistory.com)
💡 Access to XMLHttpRequest at 'https://animals-mbti-numble.o-r.kr/api/archives?size=3' from origin 'http://localhost:3000/' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
registry.addMapping("/**")
.allowedOrigins("<https://animal-mbti-test.vercel.app>")
.allowedOrigins("")
.allowedMethods(ALLOWED_METHOD_NAMES.split(","))
.maxAge(3600); // 3600초 동안 preflight 결과를 캐시에 저장
CORS 설정 추가
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (CorsUtils.isPreFlightRequest(request)) {
return true;
}
인터셉터에서 PreFlight options 는 통과하도록 설정
다형성으로 리팩토링
기존코드
public OAuthAttributes oAuthLogin(final String code) {
ResponseEntity<String>strTokensResponseEntity = kakaoOauth.requestToken(code);
String accessToken = kakaoOauth.mapToKakaoToken(strTokensResponseEntity).getAccessToken();
ResponseEntity<String>userInfoResponseEntity = kakaoOauth.requestUserInfo(accessToken);
OAuthAttributes kakaoAttributes = kakaoOauth.getUserInfo(userInfoResponseEntity);
return kakaoAttributes;
}
현재 소셜 로그인은 카카오와 구글이 있지만
로그인 메소드에서 KakaoOauth(카카오 인증 작업 클래스) 개별 클래스를 통해 카카오만의 인증 로직을 구현
구글 로그인 또한 이와 같은 작업 순서를 가지고 있기 때문에 상위 객체를 두어서 같은 메소드에 각각의 기능을 수행할 수 있도록 다형성을 통해 구현하기로 하였습니다.
우선 소셜 기능을 수행할 상위 인터페이스 생성 SocialOauth
public interface SocialOauth {
String getOauthRedirectURL();
ResponseEntity<String> requestToken(Stringcode);
OAuthToken mapToKakaoToken(ResponseEntity<String>strToken);
ResponseEntity<String> requestUserInfo(String accessToken);
OAuthAttributes getUserInfo(ResponseEntity<String>userInfoResponseEntity);
}
그런 다음 상위 클래스를 구현하는 (카카오, 구글) 클래스 생성
public class KakaoOauth implements SocialOauth { ... }
public class GoogleOauth implements SocialOauth { ... }
public OAuthAttributes oAuthLogin(final Stringcode, SocialOauth socialOauth){
ResponseEntity<String>tokenResponse = socialOauth.requestToken(code);
String accessToken = socialOauth.mapToKakaoToken(tokenResponse).getAccessToken();
ResponseEntity<String>userInfoResponse=socialOauth.requestUserInfo(accessToken);
OAuthAttributes oAuthAttributes=socialOauth.getUserInfo(userInfoResponse);
returno AuthAttributes;
}
SocialOauth 인터페이스를 통해 로그인 알고리즘 메소드를 작성
활용
private final GoogleOauth googleOauth;
private final KakaoOauth kakaoOauth;
if (SocialConstant.SocialLoginType.valueOf(social.toUpperCase()).equals(SocialConstant.SocialLoginType.GOOGLE)){
oAuthAttributes= socialService.oAuthLogin(code, googleOauth);
} else {
oAuthAttributes= socialService.oAuthLogin(code, kakaoOauth);
}
각 타입에 맞는 로그인 수행
이는 전략 패턴과 유사한 구조를 가지고 있습니다.
SocialOauth 인터페이스를 구현하는 KakaoOauth와 GoogleOauth 클래스가 각각의 전략을 나타내며, oAuthLogin 메소드에서 이 전략을 선택하여 사용 이렇게 함으로써 코드는 실행 시간에 적절한 전략을 선택할 수 있습니다.
이번 프로젝트에서 느낀 것
- 프론트엔드와 협업 프로젝트시 CI/CD 가 먼저 구축되어있어야 개발 서버가 배포되고 연동을 했을 때의 문제들을 프로덕션 배포전에 미리 해결하여 원활하게 진행할 수 있습니다.
- 공통 예외 코드 작성을 사전에 협의하면 예외처리시 조금 더 편해질 것 같았습니다.
- 마일스톤을 세우며 개발을 진행했으면 보다 여유롭게 했을 것 같다는 생각이 들었습니다.
'Dev > [넘블] 2023 백엔드 챌린지' 카테고리의 다른 글
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 4 (핀포인트, nGrinder) (0) | 2023.04.17 |
---|---|
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 3 (동시성 처리) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 2 (CI/CD) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 트러블 슈팅과 회고 1 (DB, 아키텍처, 테스트) (0) | 2023.04.17 |
[Numble] Spring으로 타임딜 서버 구축 - 클린코드를 포함한 리팩토링 (TOP3 선정!) (0) | 2023.04.09 |
댓글