pwoogi
자신의 왜곡된 경험을 진실이라고 생각하지 말자

프로그래밍/Spring

첫 미니프로젝트 종료

pwoogi 2022. 8. 18. 23:21

 

 

첫 프론트와의 협업이 끝나고 많은 것을 깨닫게 되었다.

 

부족한 자료구조, 메서드 사용방식부터 AWS EC2를 활용한 Http -> Https 동적 웹 ALB 기술 기초지식까지 아직 모르고 있는게 너무나 많다.

L7 Load Balancer로 프로토콜 헤더로 서버 분산을 가능케 해주는 AWS의 Load Balancer에 대해서도 알아보았고 Spring security에 대한 복습 + JWT의 관련 사용법을 다시 한 번 복습하게 되었다.

 

Spring security의 관심도

 

 

아래와 같은 기초 지식들 복습해보는 시간을 가져야겠다.

 

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에 토큰 넣어서 전달하고 받는다면 그렇게 크게 문제가 되지는 않는 것 같다. 

https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

 

 

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