첫 프론트와의 협업이 끝나고 많은 것을 깨닫게 되었다.
부족한 자료구조, 메서드 사용방식부터 AWS EC2를 활용한 Http -> Https 동적 웹 ALB 기술 기초지식까지 아직 모르고 있는게 너무나 많다.
L7 Load Balancer로 프로토콜 헤더로 서버 분산을 가능케 해주는 AWS의 Load Balancer에 대해서도 알아보았고 Spring security에 대한 복습 + JWT의 관련 사용법을 다시 한 번 복습하게 되었다.
아래와 같은 기초 지식들 복습해보는 시간을 가져야겠다.
AuditorAware
JPA와 AuditorAware를 사용하면 다음과 같이 간단한 매핑을 통해 특정 필드에 지금 로그인한 사람의 정보로 등록자를 자동으로 입력 해줄 수 있다.
@Configuration
@EnableJpaAuditing
public class AuditingConfig {
}
필드에 다음과 같이 @EntityListeners(AuditingEntityListener.class)를 추가해 준다.
이후, 로그인 한 사람의 정보가 들어가야 할 필드에 @CreatedBy를 매핑 하주면 된다. 참고로 @CreateDate를 매핑해주면 생성된 순간의 시간이 들어간다.
글로벌 예외처리
@RestControllerAdivce로 각 종 에러들을 담아두고 예외처리를 할 수 있다
@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {
// 500 에러
@ExceptionHandler(IllegalArgumentException.class) // 지정한 예외가 발생하면 해당 메소드 실행
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 각 예외마다 상태 코드 지정
public Response illegalArgumentExceptionAdvice(IllegalArgumentException e) {
log.info("e = {}", e.getMessage());
return Response.failure(500, e.getMessage().toString());
}
// 400 에러
// 요청 객체의 validation을 수행할 때, MethodArgumentNotValidException이 발생
// 각 검증 어노테이션 별로 지정해놨던 메시지를 응답
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response methodArgumentNotValidException(MethodArgumentNotValidException e) { // 2
return Response.failure(400, e.getBindingResult().getFieldError().getDefaultMessage());
}
// 401 응답
// 아이디 혹은 비밀번호 오류시
@ExceptionHandler(LoginFailureException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Response loginFailureException() {
return Response.failure(401, "로그인에 실패하였습니다.");
}
// 409 응답
// username 중복
@ExceptionHandler(MemberUsernameAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public Response memberEmailAlreadyExistsException(MemberUsernameAlreadyExistsException e) {
return Response.failure(409, e.getMessage() + "은 중복된 아이디 입니다.");
}
// 404 응답
// 요청한 자원을 찾을 수 없음
@ExceptionHandler(MemberNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response memberNotFoundException() {
return Response.failure(404, "요청한 회원을 찾을 수 없습니다.");
}
// 401 응답
// 유저 정보가 일치하지 않음
@ExceptionHandler(MemberNotEqualsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Response memberNotEqualsException() {
return Response.failure(401, "유저 정보가 일치하지 않습니다.");
}
// 404 응답
// 요청한 자원을 찾을 수 없음
@ExceptionHandler(RoleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response roleNotFoundException() {
return Response.failure(404, "요청한 권한 등급을 찾을 수 없습니다.");
}
사용했던 방법은 Response의 객체를 만들어두고 지네릭 타입으로 클래스의 결과를 code, message 형태로 뷰 단에게 전달할 수 있는데 이러면 프론트에서는 어떠한 코드가 에러 혹은 성공했는지 쉽게 확인이 가능하고 어떤 기능에서 구현이 안되고 있는지 가능한 쉽게 알아볼 수 있어서 협업시 용이하다
Response
@JsonInclude(JsonInclude.Include.NON_NULL) // null 값을 가지는 필드는, JSON 응답에 포함되지 않음
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class Response {
private boolean success;
private int code;
private Result result;
public static Response success() { // 4
return new Response(true, 0, null);
}
public static <T> Response success(T data) { // 5
return new Response(true, 0, new Success<>(data));
}
public static Response failure(int code, String msg) { // 6
return new Response(false, code, new Failure(msg));
}
public static Response success(int code, String msg) {
return new Response(true, code, new Success<>(msg));
}
}
토큰을 Header에 담는지 Body에 담는지 어떤면에서 보안 측면에서 유리한지 아직 개념이 확실하게 잡히지는 않았지만 body로 전달해서 사용될 때와 header로 전달할 때 프론트 협업시에는 커뮤니케이션이 잘 이루어져야 할 것 같다
JWT 바디에 보낼 때 짰던 코드
@Transactional
public TokenResponseDto logIn(LoginRequestDto req) {
Member member = memberRepository.findByUsername(req.getUsername()).orElseThrow(() -> {
return new LoginFailureException();
});
validatePassword(req, member);
// 1. Login ID/PW 를 기반으로 AuthenticationToken 생성
UsernamePasswordAuthenticationToken authenticationToken = req.toAuthentication();
// 2. 실제로 검증 (사용자 비밀번호 체크) 이 이루어지는 부분
// authenticate 메서드가 실행이 될 때 CustomUserDetailsService 에서 만들었던 loadUserByUsername 메서드가 실행됨
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 3. 인증 정보를 기반으로 JWT 토큰 생성
TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
// 4. RefreshToken 저장
RefreshToken refreshToken = RefreshToken.builder()
.key(authentication.getName())
.value(tokenDto.getRefreshToken())
.build();
refreshTokenRepository.save(refreshToken);
TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.id(member.getId())
.username(member.getUsername())
.nickname(member.getNickname())
.build();
// 5. 토큰 발급
return tokenResponseDto;
}
JWT 헤더에 보낼 때 짰던 코드
Transactional
public TokenResponseDto logIn(LoginRequestDto req, Httpheaders httpheaders) {
Member member = memberRepository.findByUsername(req.getUsername()).orElseThrow(() -> {
return new LoginFailureException();
});
//중간과정 생략, 담을 때만 header에 저장
httpheaders.setHeader("보낼이름", "보여줄값 " + tokenDto.getAccessToken());
httpheaders.setHeader("토큰 만료시간", String.valueOf(tokenDto.getAccessTokenExpiresIn()));
// 토큰을 담아줄 TokenResponseDto에 빌더를 사용한다
TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
.accessToken(tokenDto.getAccessToken())
.refreshToken(tokenDto.getRefreshToken())
.id(member.getId())
.username(member.getUsername())
.nickname(member.getNickname())
.build();
front에서 닉네임과 id를 필요로해서 전달하게 되었는데 보통은 탈취당할 위험이 있어서 바디에 담지 않고 setCookie로 쿠키에 담아서 전달하는데 클라이언트로부터 setCookie로 받은 토큰을 저장할 수 없다는 이야기가 종종있는데 이때는 Cors의 문제를 생각해볼 수 있다고 생각했다
해결방법은 WebConfig에 allowCredential 설정 및 allowHeaders 추가해서 cors 설정을 해주는 것인데
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowCredentials(true)
.allowedHeaders("*")
.allowedMethods("GET", "POST", "PUT", "PATCH");
}
위와 같이해도 실패했었다. 그래서 바디에 담아서 보냈는데 알고보니 ResponseCookie에 SameSite와 Secure 설정을 해주면되었다.
구글 크롬의 경우 새로운 쿠키 정책의 도입 이후samesite의 값이 Lax로 디폴트 값을 갖는다고 한다.
SameSite란, 서로 다른 도메인 간 쿠키 전송에 대한 보안을 설정하는 속성인데 속성 값의 종류로는 Strict, Lax, None이 있다. (None쪽으로 갈수록 보안의 벽이 낮아짐)
SameSite 값을 None으로 설정할 경우 모든 도메인에서 쿠키를 전송 및 사용이 가능해진다. (단, 이 방법으로 인해 CSRF 및 각종 보안 위협에 취약해질 수 있다고 한다.)
다음과 같은 방법으로 samesite를 커스텀 해보았다.
private String getCookie(Member member, TokenType type) {
String token = getToken(user, type);
return ResponseCookie
.from(type.getType(), token)
.maxAge(type.getTime())
.sameSite(SameSite.NONE.toString())
.secure(true)
.path("/")
.build()
.toString();
단, SameSite=None의 경우 꼭 추가적으로 Secure 옵션을 true로 해주어야 모든 도메인간 쿠키 전송 및 이용이 가능해지고 만약 Secure 설정을 하지 않았을 경우 브라우저는 경고메시지와 함께 쿠키 적용을 막는다고 한다.
이 방법을 늦게 안.. 초보 서버개발자 1인..
나는 반대로 ResponseBody에 넣어서 고생했는데 다른 블로그들은 Header에서 넣어서 오히려 바디에 넣어서 전달하는 방법을 찾았다니.. 정말 어떻게 시작하고 개념공부를 꾸준히해야하는 이유인 것 같다.
실제로 우리나라 카카오기업이 Body에 토큰 넣어서 전달하고 받는다면 그렇게 크게 문제가 되지는 않는 것 같다.
HTTP와 Token.. 공부하자
CRUD의 정확한 전달과 Entity의 설계 및 API..진짜 이게 제일 기본이면서 중요한 걸 새삼 느꼈으니.. 나는 또 학습하러 간다
개발자들 화이팅
'프로그래밍 > Spring' 카테고리의 다른 글
Spring Boot Validation (0) | 2022.08.08 |
---|---|
ORM, Hibernate, JPA (0) | 2022.08.07 |
AOP & Logging (slf4j) (0) | 2022.08.05 |
[SPRING BOOT]JWT, Thymeleaf, form (2/2) (0) | 2022.08.04 |
[SPRING BOOT]JWT, Thymleaf, form (1/2) (2) | 2022.08.04 |