JWT Filter / 심화주차 과제 구현 (항해일지 27일차)

김형준·2022년 6월 5일
0

TIL&WIL

목록 보기
27/45

1. 개발 및 학습일지


1) JWT 로그인 구현 과정


JWTAuthenticationFilter

  • 시큐리티에 UsernamePasswordAuthenticationFilter가 있는데,

    • (POST) /login 요청해서 username, password 전송하면
    • UsernamePasswordAuthenticationFilter가 동작을 한다.
    • 따라서 JWTAuthenticationFilter는 UsernamePasswordAuthenticationFilter를 상속받는다.
  • 필요한 기능 구현 과정

    • 로그인 시도를 위해서 실행되는 함수를 오버라이드 한다. (attemptAuthentication())
    • Client가 보낸 request에서 username, password를 아래와 같은 방식으로 읽어온다. ( 자세한 설명은 아래에 작성)
            //JSON 형식으로 받았을 때 읽어오는 방법. -> @RequestBody와 같은 기능!
            ObjectMapper om = new ObjectMapper();
            Users user = om.readValue(request.getInputStream(), Users.class);
            System.out.println(user);
  • 폼 로그인 이었으면 UsernamePasswordAuthenticationToken은 자동으로 생성된다. 하지만 JSON 형식으로 받아왔기 때문에 토큰을 만들어줘야 한다.
    • new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
  • PrincipalDetailsService의 loadUserByUsername() 함수가 실행되어
    • PrincipleDetails에 찾아온 user를 넣어 Authentication으로 리턴해줌. -> Authentication 생성
    • Authentication으로 리턴하는 과정에서 user 객체를 DB에서 조회해오면, authenticationManager가 알아서 비밀번호까지 검증해준다.
    • 즉, 아래 코드가 정상적으로 작동하면 DB에 있는 username과 password가 일치한다는 뜻이다.
Authentication authentication = authenticationManager.authenticate(authenticationToken);
  • 위 코드가 정상적으로 authentication 값을 담아왔고, getPrincipal()로 가져온 것이 출력이 된다면 로그인 성공한 것!! -> 테스트 용도임
            PrincipalDetials principalDetials = (PrincipalDetials) authentication.getPrincipal();
            System.out.println("로그인 완료됨: " + principalDetials.getUser().getUsername());
  • authentication 리턴 시 SESSION에 저장됨.
    • 리턴의 이유는 권한 관리를 security가 대신 해주기 때문에 편하려고 하는 것!
    • 굳이 JWT를 사용하며 세션을 만들 이유가 없다. 단지 권한 처리때문에 session에 넣어줌
return authentication;
  • attemptAuthentication 실행 후 인증이 정상적으로 되었으면 successfulAuthentication 함수가 실행된다.
  • JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해주면 된다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        System.out.println("successfulAuthentication 실행됨: 로그인 및 인증이 완료되었다는 뜻");

        PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();

        // RSA가 아닌 Hash 암호 방식
        String jwtToken = JWT.create()
                .withSubject("토큰")
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
                .withClaim("id", principalDetails.getUser().getId())
                .withClaim("username", principalDetails.getUser().getUsername())
                .sign(Algorithm.HMAC512(JwtProperties.SECRET));

        response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + jwtToken);
    }

➕자바 영역에서 HTTP request 정보 읽기

  • request.getInputStream() : 바이트 단위로 읽어들임

  • request.getReader()

  • request.getParameter() / request.getParameterValues() / request.getParameterNames()

  • 🔗 참고링크_JavaDocs_ServletRequest

    • BufferReader:
    • ** Buffer: 데이터 전송 시 사용되고, 데이터를 어디에 잠깐 보관하는 메모리 영역
    • [JavaDocs] Reads text from a character-input stream, buffering characters so as to provide for the efficient reading of characters, arrays, and lines.
    • 버퍼를 둠으로써, 파일,네트워크와 같은 물리적인 장치에서 데이터를 사용자가 요청할때마다 매번 읽어오는것보단, 일정량사이즈로 한번에 읽어온후, 버퍼에 보관

Spring Security VS JWT

  • Spring Security (세션 방식)

    • username, password 로그인 정상
    • 서버에서 SESSIONID 생성
    • 클라이언트 쿠키에 SESSIONID를 응답
    • 요청할 때마다 쿠키값 SESSIONID를 항상 들고 서버쪽으로 요청하고,
    • 서버는 SESSIONID가 SESSION 저장소에 저장되어있는지 검토하고 존재한다면 인증이 필요한 페이지로 접근시켜줌
    • session.getAttribute("세션값")으로 확인하는 것.
  • JWT (Bearer 방식)

    • username, password 로그인 정상
    • 서버에서 JWT를 생성
    • 클라이언트 response header에 (Authorization: Bearer~ ) 키 밸류 형식으로 JWT를 보내줌
    • 요청할 때마다 JWT를 가지고 요청하고,
    • 서버는 JWT가 유효한지 판단한다. (저장된 값이 아니기 때문에, 필터를 통해 판단한다.)

JWTAuthorizationFilter

  • 시큐리티가 filter를 가지고 있는데, 그 필터중에 BasicAuthenticationFilter 라는 것이 있다.

  • 권한이나 인증이 필요한 특정 주소를 요청했을 경우 위 필터를 무조건 타게 되어있다.

    • 만약에 권한이나 인증이 필요한 주소가 아니라면 이 필터를 타지 않는다.
    • 라고 강의에서 공부했는데, 인증이나 권한이 필요하지 않은 곳에서도 해당 필터를 타게 된다. 스터디 팀원들과 공부해볼 부분.
  • 따라서 JWTAuthorizationFilter에서 BasicAuthenticationFilter를 상속받는다.

  • 필요한 기능 구현 과정

    • 먼저 BasicAuthenticationFilter는 AuthenticationManager를 생성자의 파라미터로 받는다.
    • 따라서 이를 상속받는 클래스도 AuthenticationManager를 넣어주고, super(AuthenticationManager)로 부모 클래스의 생성자를 사용한다.
    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }
  • doFilterInternal()를 오버라이드 한다. 해당 필터는 인증이나 권한이 필요한 주소 요청이 있을 때 작동한다.
  • request의 Header에 JWT가 있는지 확인하고 없다면 다른 필터로 넘긴다.
  • JWT를 검증하여 정상적인 사용자 인지 확인한다.
String username =
                JWT.require(Algorithm.HMAC512(JwtProperties.SECRET)).build().verify(jwtToken).getClaim("username").asString();
  • 만약 정상적인 사용자라면 ( 서명이 제대로 됐다면 ) userRepository에서 해당 User 객체를 불러온다.
  • User객체를 UserDetails를 상속한 클래스의 생성자 파라미터로 넘겨 User 정보를 담은 UserDetails 를 생성한다.
  • 다음으로 Authentication 객체를 만들어주는데, 우항에는 UsernamePasswordAuthenticationToken()을 두며, 파라미터로 UserDetails 정보들을 넘긴다.
Authentication authentication =
                    new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
  • 생성된 Authentication 객체를 세션의 SecurityContext (세션저장소)에 저장한다.
SecurityContextHolder.getContext().setAuthentication(authentication);

2) 스프링 심화주차 과제 구현 (완료)

  • 🔗 완성 코드

  • 리뷰

    • 이번 과제의 목표는 백엔드의 주요 업무인 API 개발에 초점을 둔 것 같았다.
    • 요구 사항대로 API를 구현하며 나름 서비스 로직이 굵어졌다.
    • 이번 과제를 구현하며 느낀 점은, 요구사항이 주어졌을 때 스스로 3계층 설계부터 구현까지 해낼 수 있게 되었다는 것이다.
    • 스프링 입문 주차 때를 생각하면 나름 유의미한 성장이 있던 것 같다.😊
    • 하지만 아직 공부할게 엄청 많다.. 그래도 긍정적으로 엄청 많이 공부하면 그만큼 성장도 할테니! 화이팅!
  • 개발 과정

    • DB Table 설계
      • 가장 먼저 Entity 간 연관관계를 설계하고, 이를 JPA가 제공하는 연관관계 어노테이션으로 구현하고자 했다.
      • Table 설계도를 작성하며, 자연스럽게 어떻게 개발해야 하는 지에 대한 고민도 이뤄졌던 것 같다.


  • Order (주문) 테이블에서 음식과 수량을 같이 저장하는 방법 -> HasbMap을 사용했다. (@ElementCollection)

  • 양방향 연관관계 설정 -> 이번 과제에선 대부분 일방향으로만 사용했다.

  • 연관관계를 사용할 때 주의해야 할 부분은 아무래도 순서인 것 같다

    • 코드 작성 전 설계했던 구조대로 구현해 가며 심심치 않게 nullPointerException을 마주했는데,
    • 해당 오류는 연관관계만 정의하고 초기화가 이뤄지지 않아 발생했었다.
    • 따라서 오늘의 교훈은 <먼저 DB Table 설계를 확실하게, 그 다음으로 설계도에 입각한 논리 구현에 충실하자>
      • TMI) 전 직장에서 로우코드 프로그래밍을 할 때에도 이렇게 했었는데 이번 과제에 도움이 됐던 것 같다. (위에 테이블 설계도도 로우코드 IDE를 통해 만든 것 😅)
  • API 명세 및 요구 사항은 위에 기입한 링크의 readMe.Md 파일에 올려두었다.


2. 코멘트

  • 사실 오늘은 27일 차가 아니라 28일 차다.
  • 어제 업로드 했었어야 했는데, 다 작성하고 업로드를 못했다. (기절함🤦‍♂️)
  • 아래 과제 구현 부는 28일차 일요일에 진행된 부분이다.
profile
BackEnd Developer

0개의 댓글