pre-project를 함께하게 된 팀원들과의 첫만남
아직 무엇을 만들지에 대해서도 모르기 때문에 간력하게 회의를 진행할 때 사용할 툴(discord, google meet)을 정하는 등 가벼운 인사를 나누고 백엔드 기능을 담당하는 팀(이하 BE팀)은 따로 남아 아래 회의록과 같은 내용에 대해서 이야기를 나눠봤다.
1. 스키마
2. api문서화(+테스팅)
- TDD → RestDocs → 코드양이 너무 많고 오래걸림 ← 2단계
- Swagger3.0 → 편하긴 하지만 실서비스 코드에 영향이 있음
- Postman docs → ?.?.?.? ← 1단계
3. 컨벤션 + 커밋메세지 + 커밋약속(실행되는것만 커밋 등)
4. 다른사람 패키지 최대한 건들지 않기
- if 건들게 되면 꼭 연락 먼저하기
5. 역할분담
1. 시큐리티(세션+쿠키, Oauth2) + 기능 - 원종
2. 배포+ 기능 - 현민
3. db + 기능 - 석현 (postgreSQL)
6. 함께
1. 테스팅 + API문서화 각자 맡는 파트
2. member 같이
3. 스키마 같이
7. 학습할 내용
- Postman docs 문서화 뽑기
8. ****통합개발환경(Integrated Development Environment, IDE)****
- 인텔리제이
9. ****JDK(Java Development Kit)****
- JDK11
10. ****프레임워크(Framework)****
- Spring-boot 2.7.1 버전
11. ****빌드 방식****
- Gradle
12. DB
- postgreSQL
간단하게 사용자 요구사항 정의서를 만들어 봤다.
OAuth2 인증 방식을 추가할 때 회원 관리를 위해 아래와 같은 2가지 case 중 어떤 방식을 선택할지 정해야 했다.
장점:
단점:
장점:
단점:
결론적으로, 회원 관리를 위해 데이터 테이블을 추가하는 방식은 명확한 구분과 데이터 구조 관리의 이점이 있지만, 구현 및 유지보수의 복잡성이 높아질 수 있습니다. 반면, 기존 인증방식에 필요한 데이터 중 부족한 부분을 더미 데이터로 채우는 방식은 구현이 간단하나, 데이터 정합성 문제가 발생할 수 있다.
위와 같은 장단점을 고려해 BE팀에서 의논을 통해 데이터 테이블을 하나 추가하는 방식
으로 진행하기로 결정했다.
위와 같은 자잘한 협의과정을 통해 약속을 정하고 테이블 설계에 들어갔다.
학습과정에서 테이블관점의 스키마를 배워 다른 BE팀원들이 객체 관점의 스키마 구조에 익숙하지 않았지만, 각 entity의 연관관계를 명확히 하고 내용을 공유하기 쉽게하기 위해서 객체 관점으로 스키마를 그리자고 설득하고 약속했다.
// Use DBML to define your database structure
// Docs: https://www.dbml.org/docs
Table Answer {
answer_id integer
member Member
post Post
body varchar [note: 'Content of the Answer']
answer_comment List(Answer_Comment)
vote Vote
created_at LocalDateTime
modifiedAt LocalDateTime
}
Table Answer_Comment {
comment_id integer
answer Answer
member Member
body varchar [note: 'Content of the Comment']
created_at LocalDateTime
modifiedAt LocalDateTime
}
Table Member {
member_id integer [primary key]
member_name varchar
member_nick_name varchar
email varchar
profile_image varchar
password varchar
role varchar
post List(post)
answer List(answer)
answer_comment List(answer_comment)
save_post_id List(post_id)
save_answer_id List(answer_id)
notification_list list(Notification)
location varchar [note: 'default = null']
title varchar [note: 'default = null']
aboutMe varchar [note: 'default = null']
modifiedAt LocalDateTime
created_at LocalDateTime
}
Table Member_Oauth2 {
member_oauth2_id integer [primary key]
member_oauth2_name varchar
member_oauth2_nick_name varchar
oauth2_email varchar
profile_image varchar
role varchar
post List(post)
answer List(answer)
answer_comment List(answer_comment)
save_post_id List(post_id)
save_answer_id List(answer_id)
notification_list list(Notification)
location varchar [note: 'default = null']
title varchar [note: 'default = null']
aboutMe varchar [note: 'default = null']
modifiedAt LocalDateTime
created_at LocalDateTime
}
Table Post {
post_id integer [primary key]
title varchar
body varchar [note: 'Content of the post']
user_id integer
status varchar
answer List(Answer)
post_like Post_Like
post_hate Post_Hate
created_at LocalDateTime
modifiedAt LocalDateTime
member Member
view View
article_hashTag_list list(article_hashTag)
}
Table HashTag {
hashTag_id integer [primary key]
hashTag_name varchar
article_hashTag_list list(article_hashTag)
}
Table Article_HashTag {
article_hashTag_id integer [primary key]
hashTag HashTag
post Post
}
Table Notification {
notification_id integer [primary key]
member Member
notification_type notification_type
}
Table View {
view_id integer [primary key]
post Post
}
Table Post_Like {
like_id integer [primary key]
statusPlus boolean
post Post
}
Table Post_Hate {
hate_id integer [primary key]
status boolean
post Post
}
Table Vote {
vote_id integer [primary key]
isvoted boolean
answer Answer
modifiedAt LocalDateTime
}
Ref: "Member"."post" < "Post"."member"
Ref: "Member"."save_post_id" < "Post"."post_id"
Ref: "Answer_Comment"."answer" < "Answer"."answer_comment"
Ref: "Member"."save_answer_id" < "Answer"."answer_id"
Ref: "Member"."answer" < "Answer"."member"
Ref: "Member"."answer_comment" < "Answer_Comment"."member"
Ref: "Post"."answer" < "Answer"."post"
Ref: "Post"."view" < "View"."post"
Ref: "Post"."post_like" < "Post_Like"."post"
Ref: "Post"."post_hate" < "Post_Hate"."post"
Ref: "Post"."article_hashTag_list" < "Article_HashTag"."post"
Ref: "HashTag"."article_hashTag_list" < "Article_HashTag"."hashTag"
Ref: "Member"."notification_list" < "Notification"."member"
Ref: "Answer"."vote" - "Vote"."answer"
아래 첨부한 캡쳐본과 같이 04/13날 BE팀이 작성한 요구사항 정의서에 대해서 FE팀이 질문사항을 정리해와 회의를 진행하며 서로의 의견을 조율하며 중요도나 response request에 담아줘야할 내용들에 대해 정리했다.
post
라고 명명해 소통 과정에서 post메서드와 혼동하는 경우가 생겨 question
으로 변경했다.BE팀은 본격적으로 이슈를 등록하고 작업을 시작하려고 한다.
BE팀 총원 3명이 함께 기본구조가 될 Member
, JWT
, Exception
을 작성하며 작명 규칙을 통일하는 등 전반적인 구조에 대한 설계방향에 대해 약속했던 내용들을 확인할 수 있었다.
httpstatus 타입으로 적는 것 보다 int타입으로 적는게 더 일반적이라고 들어서 수정했다.
인증오류시 클라이언트에 응답 보내주고 log 남기는 로직을 추가했다.
member 기능구현을 하며 회원가입 완료 시 이메일을 보내보면 좋을 것 같다는 생각이 들었다. (후에 기능추가 하도록 이슈로 기록해뒀다.)
프로필 이미지 등록을 필수로 하지 말고 따로 분리해서 다루는게 더 유연할 것 같다는 생각이 들었다. 대략적으로 구현해두고 팀원과 상의해봐야겠다.
JWT를 이용해 권한부여를 적용하려다 후에 개발과정에서 테스트하는데 번거로울 것 같아 우선순위를 미뤘다.
member 객체가 갖고 있는 정보들 중 nickName, email, password는 필수값이고 자기소개 같은 선택사항은 따로 다룰 수 있도록 기능을 구현하기는 했지만 아래와 같이 하나의 메서드에 다 담아두고 각각 다른곳에서 다른 api요청을 받았을 때 처리하는 방식이 비효율적이진 않을까 의문이 들었다.
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public Member updateMember(Member member) {
Member findMember = findVerifiedMember(member.getMemberId());
Optional.ofNullable(member.getMemberNickName())
.ifPresent(name -> findMember.setMemberNickName(name));
Optional.ofNullable(member.getPassword())
.ifPresent(password -> findMember.setEmail(password));
Optional.ofNullable(member.getProfileImage())
.ifPresent(image -> findMember.setProfileImage(image));
Optional.ofNullable(member.getLocation())
.ifPresent(location -> findMember.setLocation(location));
Optional.ofNullable(member.getTitle())
.ifPresent(title -> findMember.setTitle(title));
Optional.ofNullable(member.getAboutMe())
.ifPresent(aboutMe -> findMember.setAboutMe(aboutMe));
return memberRepository.save(findMember);
}
애플리케이션을 계속 실행시켜야 한다.
BE팀원 중 한명이 배포해줘야 하기 때문에 (FE분들 ngrok이나 docker 사용하실 줄 모름) 배포 중에도 본인 작업을 해야하기 때문에 포트번호를 바꾸는 등 세팅 변경을 해줘야하고 터미널에서 돌리다보니 환경변수 설정도 따로 해줘야 한다. (기존에는 편하게 인텔리제이가 시스템 환경변수 가져와서 사용)
컴퓨터가 힘들어 한다 ㅠㅡㅠ
시간 제한이 있다.
MemberController에 JWT 토큰 검증 로직을 추가했다.
힘들게 찾아보면서 토큰의 Claims에서 값을 빼와 검증했는데 팀원이 같은 로직을 다른 엔티티컨트롤러에 적용한 것을 봤더니 너무 간단해서 자세히 알아봤다.
아래 첫번째 방식이 내가 사용한 방법이고 두번째 방식이 팀원이 사용한 방식이다.
@Component
public class JwtUtil {
@Value("${JWT_SECRET_KEY}")
private String secretKey;
public String extractEmailFromToken(String token) {
// "WishJWT " 부분 제거 (토큰만 남김)
if (token.startsWith("WishJWT ")) {
token = token.substring(8);
}
// secretKey를 Base64로 인코딩
String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
// 토큰에서 claims 가져오기
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(encodedSecretKey)
.parseClaimsJws(token);
// 이메일 값 반환
return claimsJws.getBody().get("memberEmail", String.class);
}
}
@PatchMapping("/{member-id}/profile-image")
public ResponseEntity updateProfileImage(
@PathVariable("member-id") @Positive long memberId,
@RequestBody MemberDto.ProfileImage requestBody,
@RequestHeader("Authorization") String token) {
String userEmail = jwtUtil.extractEmailFromToken(token);
Member member = memberService.findMember(memberId);
member.setProfileImage(requestBody.getProfileImage());
Member updatedMember = memberService.updateMember(member, userEmail);
MemberJoinResponseDto responseDto = memberMapper.memberToMemberResponse(updatedMember);
return new ResponseEntity(responseDto, HttpStatus.OK);
}
@PatchMapping("/{questionId}")
public Response<QuestionResponse> update(@PathVariable Long questionId,
@RequestBody QuestionUpdateRequest request,
Authentication authentication){
QuestionDto questionDto = questionService.update(request.getTitle(), request.getBody(), authentication.getName(), questionId);
return Response.success(QuestionResponse.from(questionDto));
}
두 방식 모두 유효하지만, 전반적으로 첫 번째 방식은 토큰 처리 로직을 한 곳에 모아 놓기 때문에 코드의 재사용성과 유지 보수성이 높다는 장점이 있다. 반면, 두 번째 방식은 추가적인 클래스 없이 Spring Security의 Authentication 객체를 사용하여 간단하게 구현할 수 있다는 장점이 있다.
결론적으로, 프로젝트의 규모와 요구 사항에 따라 적절한 방식을 선택하면 된다. 코드 재사용성과 유지 보수성이 중요한 경우 첫 번째 방식을 사용하고, 간단한 구현을 원하면 두 번째 방식을 사용하면 좋다.
JWT 설정의 변수명 직관적이지 않은 것 같아서 바꿧다가 사용한 곳 찾느라 고생 좀 했다.
팀원을 초대해서 요청문을 함께 관리할 수 있다.
Documentation 기능을 이용해 간력하게라도 문서화를 자동으로 할 수 있다.
(restdoc으로 하느라 (테스트 코드 다 짜느라) 시간이 너무 많이 걸렸는데... FE분들은 빨리 필요하고... 힘든 와중에 너무 좋은 기능이다.)
restdoc을 이용해 api 문서화를 완료했다. (풀코드 링크 참조)
작성 도중에 경로오류 때문에 고생했었다.
프론트측에서 cors 오류가 발생했다고 해결해 달라는 요청이 들어왔다.
@Configuration
@EnableWebSecurity // Spring Security를 사용하기 위한 필수 설정들을 자동으로 등록
@EnableGlobalMethodSecurity(prePostEnabled = true) // 메소드 보안 기능 활성화
public class SecurityConfiguration {
~~생략~~
@Bean
CorsConfigurationSource corsConfigurationSource() { // CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE")); // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성
source.registerCorsConfiguration("/**", configuration); // 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용
return source;
}
~~생략~~
}
이때 당시에는 RESTfull한 API를 디자인하기 위해서 "GET","POST", "PATCH", "DELETE"
만 사용해야 한다고 REST API의 정의에 대해 잘못알고 있었다.
혜나님과 함께 작업하며 생겼던 이슈들이 앞으로 다른 FE팀원분들에게도 비슷한일이 생길 수 있어서 알게된 정보 공유 겸 정해야할 부분에 대해서 의논하고자 해서 회의를 제안했습니다. 참여해주셔서 감사합니다.
오리진이 다를 때 cors 오류가 발생할 것 같아서 서버에서 전체 허용을 해뒀지만
브라우저가 오리진이 다르면 프리플레이트를 자동으로 보내서 OPTIONS 타입의 요청을 서버로 보냅니다.
OPTIONS에 대한 요청처리가 서버에 구현되어 있지 않아서 서버에서는 OPTION 타입을 차단해 두었고 이러한 오류가 cors로 나타났었습니다,
본래 cors란 동일출처가 아닐경우 위험한 접근일까봐 차단하는 것인데, 전혀 다른 이유로 오류가 발생했던 것입니다.
따라서 cors와 같이 서버와 연관된 오류가 발생했을 때 빠르게 해결하기 위해서 request헤더, 화면공유를 적극적으로 요청해주셨으면 좋겠습니다.
그리고 이에 대한 해결방안은 크게 2가지가 있을 것 같습니다. FE가 해결 or BE가 해결입니다.
cors 등 문제가 발생했을 때 저에게 연락하시면 되는데, 저는 척보면 척할 실력이 안되니 요청과 함께 정보도 많이 부탁드립니다. ㅠㅡㅠ
혜나님이 http proxy middleware 방식으로 해결하셨는데 "모든" 요청에 적합한 방식이 아니기 때문에 추가적인 해결이 필요해 보입니다. FE 아이디어 부탁드립니다.
BE의 해결방법은 각 엔티티 컨트롤러마다 OPTIONS처리 로직 구현 작동 안함 ㅠㅡㅠ
(but 배운적이 없어서 잘모릅니다.. 일단 구글링해서 작성해보긴 했는데 제대로 작동할지 모르겠습니다.)
저는 FE분들과 얘기도 안해보고 3-Tier 아키텍쳐를 당연시 했는데, 어제 혜나님 작업방식을 보니 현재 구조는 클라이언트 - 서버(FE) - 서버(백엔드서버) - DB 구조였습니다.
ngrok서버는 닫았다 열 때마다 url 주소가 바뀝니다. 주의해주세요.
백엔드 팀원이 request를 요청할 때 FE분들 기준의 엔드포인트는 필요가 없습니다. postMan으로 작성하는 url 기준으로 공유 바랍니다.
이 밖에도 프론트측과 백엔드가 함께 대화가 필요한 내용이 있을 것 같은데 어떤 기술을 적용하실 때(자동으로 적용될 수도 있습니다 ㅠ 이것도..함꼐) 확인하셔서 회의를 제안해주시면 좋을 것 같습니다.
프론트분과 함께 고생했던 cors 오류를 해결하기 위해 OPTIONS 로직을 추가하는 등 다양한 방법을 사용했지만 해결되지 못했다.
혹시나 싶어 ec2에 또 배포했더니 ec2에서는 OPTIONS 로직 없이도 문제없이 동작한다.
ngrok안쓰기로 정했다. 앞으로 ec2만 쓸 것이다.
ec2 RDS 연결 과정에서 과금 이슈가 발생할까 겁나서 꼼꼼히 확인하고 정리해뒀다.
postman 문서화 기능을 이용해 웹 페이지에서 간편하게 볼 수 있도록 다른 팀원이 작성한 문서화 내용과 합쳤다. (캡쳐본 링크 참조)
기존 코드가 Oauth2 추가에 대한 대비가 전혀 되어있지 않았다.
Oauth2 객체를 따로 관리하기 위해서는 아래와 같이 매개용 객체를 만들었어야 했는데... 이제와서 깨달아 버렸다.
이제와서 매개용 객체를 추가하면 관련된 기능들을 전부 다 손봐야하는데 사실상 2일밖에 시간이 남지 않았다...
어쩔 수 없이 기존 member 객체에 password를 더미객체로 채워넣는 방식으로 만들어야할 것 같다.
문제는 순환참조 오류가 발생한다는 것이다.
┌─────┐
| OAuth2MemberSuccessHandler defined in file [D:\AAWonJong\it\seb43_pre_033\digitalWizard-server\build\classes\java\main\com\seb33\digitalWizardserver\auth\handler\OAuth2MemberSuccessHandler.class]
↑ ↓
| memberService defined in file [D:\AAWonJong\it\seb43_pre_033\digitalWizard-server\build\classes\java\main\com\seb33\digitalWizardserver\member\service\MemberService.class]
↑ ↓
| securityConfiguration defined in file [D:\AAWonJong\it\seb43_pre_033\digitalWizard-server\build\classes\java\main\com\seb33\digitalWizardserver\config\SecurityConfiguration.class]
└─────┘
Member 객체와 Oauth2 객체가 필요한 기능들이 비슷하다 보니 Oauth2 설정에 Member기능을 가져와 쓰다보니 발생했다.
어쩔 수 없이 코드의 중복이 발생하지만 아래와 같이 정보 저장방법을 수정해 해결했다.
private Member saveMember(String email, String nickname, String profileImage) {
memberRepository.findByEmail(email).ifPresent(it ->
{throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS, String.format("%s is duplicated 버그발생! OAuth2 핸들러 검사하시오.", email));
});
Member member = new Member();
member.setEmail(email);
member.setMemberNickName(nickname);
member.setProfileImage(profileImage);
Member savedMember = memberRepository.save(member);
List<String> roles = authorityUtils.createRoles(email);
savedMember.setRoles(roles);
CustomMemberDto.from(savedMember);
return member;
}
로컬에서는 정상적으로 작동한다.
하지만 문제가 있다. 매개용 객체를 만들지 않아서 구글로 로그인하면 password 컬럼에 null값이 들어있다. 한번 로그인한 후로는 password를 입력하지 않고 이메일만 입력해도 로그인이 되어버리기 떄문에 더미 데이터를 채워주던지 google 로그인으로 저장된 회원인지 검증 가능한 로직을 추가해야할 것 같다.
이번에는 그래도 비교적 간단했다.
로그인 과정에서 오류가 발생해서 설정 이것저것 바꾸다가...
Credentials을 로그인 과정에서는 필요 없다 생각해서
configuration.setAllowCredentials(Boolean.valueOf(false));와 같이 설정해 뒀는데 프론트측 코드에 Credentials = true로 설정해서 차단되었던 것이였다.
아래와 같이 cors 설정을 수정해 해결했다.
@Bean
CorsConfigurationSource corsConfigurationSource() { // CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE", "OPTIONS")); // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(Boolean.valueOf(true));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성
source.registerCorsConfiguration("/**", configuration); // 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용
return source;
}
OAuth2 회원가입 회원들의 password를 더미데이터로 채워서 관리하려고 했는데, 그냥 비밀번호가 null인 경우 회원가입 못하도록 막는게 더 좋을 것 같다.
회원가입시 유효성 검사에서 null값 제외시키는 것은 클라이언트측에서 담당해주면 될 것 같다. (물론 더블체킹할 수 있게 만들면 더 좋지만 기간이 얼마 안남아서... 빠르게 할 수 있는 방향으로 선택한거다.)
대략적으로 짜본 스케쥴
화= 개발 마무리
수 = 배포완료
목 = 체크리스트 작성
nogrok에서 계속 cors나서 ip 주소 받아서 명시해 보는 등 조치를 취해봤지만 계속 안됐다.
같은 버전의 서버파일을 ec2에 업데이트 후 테스트하니 문제없다...
다른 백엔드 팀에서 작업 중 막히는 부분이 있다고 도와달라고 종종 요청이 왔었는데 오늘은 3팀에서 요청을 받았다. JWT, 배포, 쿼리문 다양하게 질문을 받았는데 작업 후반부에 오니 세팅의 차이가 너무 심해서 대부분 적절한 해결방법을 찾아내지 못해서 도움이 되어드리지 못했다.
특히 JWT의 경우 인증과정이 제대로 수행되지 않아 로그인 시 토큰 발급이 안되는 상황이였는데 기능분리가 제대로 되어있지 않고, 코드의 중복이 심해서 security에서 자동으로 호출하는 메서드들이 이곳저곳에서 오버라이딩 되어있는 등 숨은그림 찾기가 매우 어려웠다... @Configuration 클래스가 서로 중복되거나 충돌하는 등 일부 문제점을 찾고 수정을 돕기는 했지만 2시간정도 시간동안 시도하다 결국 완벽하게 해결하지는 못하고 포기하게 되어 너무 아쉬웠다.
오늘의 경험 덕분에 코드의 가독성, 확장성, 기능분리 등의 중요함을 다시한번 느낄 수 있었다.
리프래쉬 토큰을 이용해 엑세스 토큰을 재발급하는 로직을 추가해 뒀었는데 프론트에서 해당 기능을 적용하는데 어려움을 겪고 있다고 해서 함께 고민해 봤다.
cf. refresh 토큰도 토큰헤더를 붙이는게 좋다는걸 알고 있지만 편하게 구분하시라고 헤더를 빼는 방향으로 구분지었다.
기존에 로그인을 통해 각 토큰을 브라우저에 받는 것을 문제없이 해결하신걸 보면 각 정보의 변수명을 구분 못하신건 아닐텐데 리프래쉬 토큰을 Authorization = 리프래쉬 토큰
과 같이 서버로 전송하셔서 제대로 동작하지 않았던 것이였다.
@RestController
@RequestMapping("/refresh")
@AllArgsConstructor
public class RefreshController {
private final JwtTokenizer jwtTokenizer;
@PostMapping
public ResponseEntity<String> refreshAccessToken(HttpServletRequest request) { // 리프레쉬 토큰 받으면 엑세스 토큰 재발급
String refreshToken = request.getHeader("Refresh");
if (refreshToken != null) { // && refreshTokenHeader.startsWith("WishJWT ") 나중에 추가
// String refreshToken = refreshTokenHeader.substring(8);
try {
Jws<Claims> claims = jwtTokenizer.getClaims(refreshToken, jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()));
String email = claims.getBody().getSubject();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
String accessToken = jwtTokenizer.generateAccessToken(claims.getBody(), email, expiration, base64EncodedSecretKey);
return ResponseEntity.ok().header("Authorization", "WishJWT " + accessToken).body("Access token refreshed");
} catch (JwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
}
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing refresh token");
}
}
}
위 코드와 같이 request.getHeader("Refresh")!=null
로 되어 있기 때문에 변수명이 달라 서버가 리프래쉬 토큰값을 인식하지 못한 것이다.
이번에는 다행히 내가 도움이 될 수 있었지만 프론트 문법을 몰라서 로직 관련된 부분은 이해하기 힘들었다. 후에 간단하게라도 공부해서 이해도를 높이면 협업 과정에서 큰 도움이 될 것 같다.
위와 같이 리다이렉트 미스매치라고해서... 보내줘야하는 클라이언트의 주소가 적절하지 않거나 권한이 없나 했더니...
구글 입장에서는 나의 백엔드 서버가 클라이언트니까... 나의 서버에 권한이 없는거였다 ㅠㅡㅠ
이제 아무곳에서나 접속해도 로그인창 정상적으로 보인다.
클라이언트에서 구글인증에 대한 처리를 추가못해서 정확한 확인은 못했지만
아래와 같이 배포환경에서도 정상정으로 동작을 수행하는 것 같다.
(위 켭쳐본은 전체 기능 중 일부분입니다.)
만들다보니 자잘한 기능들이 생각나서 추가하게 되는 경우가 많았다.
이러한 자잘한 작업들이 모여 생각보다 많은 시간을 허비하게 되었고 달성률이 아쉽게 나왔다.
그래도 협업과정을 연습해보는 기회로 충분히 좋은 경험이 된 것 같다.