SEB_BE 65일차 - JWT 인증2 (Authentication)

subimm_·2022년 11월 29일
0

코드스테이츠

목록 보기
65/83

💡 오늘의 학습목표

  • Spring Security에서의 JWT인증
  • JWT 자격 증명을 위한 로그인 인증 구현
  • JWT를 이용한 자격 증명 및 검증 구현

📔 JWT 적용을 위한 사전 작업

📖 의존 라이브러리 추가

  • build.gradle

📖 SecurityConfiguration 추가

  • Spring Security를 이용한 보안 강화를 위해 최소한의 보안 구성(V1)

  • H2 웹 콘솔의 화면 자체가 내부적으로 태그를 사용하기 때문에 개발환경에서는 H2 웹 콘솔을 정상적으로 사용할 수 있도록 (1)과 같이 추가
    .frameOptions().sameOrigin() 호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링 허용

  • (2) CSRF 공격에 대한 Spring Security 설정 비활성화

    • 로컬환경에서 진행하기 때문에 설정이 필요하지 않다.
  • (3) CORS 설정 추가 .cors(withDefaults())일 경우 corsConfigurationSource 라는 이름으로 등록된 Bean을 이용함

    • CORS를 처리하는 쉬운 방법은 CorsConfigurationSource Bean을 제공함으로써 CorsFilter를 적용할 수 있다.
  • CORS (Cross-Origin Resource Sharing)
    애플리케이션 간에 출처(Origin)가 다를 경우 스크립트 기반의 HTTP 통신(XMLHttpRequest, Fetch API)을 통한 리소스 접근이 제한되는데, CORS는 출처가 다른 스크립트 기반 HTTP 통신을 하더라도 선택적으로 리소스에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책
  • CSR 방식에서 주로 사용하는 JSON 포맷으로 Username과 Password를 전송하는 방식을 사용할 것이므로 (4) 폼 로그인 비활성화

  • HTTP Basic 인증은 request를 전송할 때 마다 Username과Password 정보를 HTTP Header에 실어서 인증하는 방식. 사용하지 않으므로 (5) 비활성화

    • 폼 로그인과 HTTP Basic 인증을 비활성화하면 해당 인증과 관련된 Filter가 비활성화됨.
  • (6)은 JWT를 적용하기 전이므로 우선은 모든 HTTP request 요청에 대해서 접근 허용 설정

  • (7) PasswordEncoder Bean 객체를 생성

  • (8) CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책 설정

    • (8-1) 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정
    • (8-2) 파라미터로 지정한 HTTP Method에 대한 HTTP 통신 허용
    • (8-3) CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성
    • (8-4) 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용

📖 회원 가입 로직 수정

  • 샘플 애플리케이션에 회원 등록 시, 회원의 인증과 관련된 정보가 필요함.

1. MemberDto.Post 클래스에 패스워드 필드 추가

  • 회원 등록 시, 회원의 패스워드 정보를 전달 받기 위해 password 필드 추가
    • 실제 서비스에서는 회원 가입 시, 사용자가 입력한 패스워드가 맞는지 재확인 하기 위해 패스워드 입력 확인 필드가 추가로 존재하는 경우가 대부분. 입력한 두 패스워드가 일치하는지를 검증하는 로직이 필요함
    • 또한 패스워드의 생성규칙에 대한 유효성 검증도 실시한다.

2. Member 엔티티 클래스에 패스워드 필드 추가

  • (1) Member 엔티티 클래스에 패스워드 필드 추가 (암호화되어 저장되어서 컬럼 길이 100 지정)
  • (2) @ElementCollection 애너테이션을 이용해 사용자 등록 시, 사용자의 권한 등록 위한 권한 테이블 생성
    3. 사용자 등록 시, 패스워드와 사용자 권한 저장
  • (1), (2)에서 PasswordEncoderCustomAuthorityUtils 클래스를 DI 받도록 필드 추가
  • (3) 패스워드를 단방향 암호화
  • (4) 등록하는 사용자의 권한 정보를 생성한다.

  • CustomAuthorityUtils 클래스

📔 JWT 자격 증명을 위한 로그인 인증 구현

  • Username(이메일 주소) 과 Password로 로그인인증에 성공한 사용자에게 JWT를 생성 및 발급하는 것
  1. 클라이언트가 서버 측에 로그인 인증 요청(Username/Password를 서버에 전달)
  2. 로그인 인증을 담당하는 Security Filter(JwtAuthenticationFilter)가 클라이언트의 로그인 인증 정보 수신
  3. Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager에게 전달해서 인증 처리를 위임.
  4. AuthenticationManager가 Custom UserDetailsService(MemberDetailsService) 에게 사용자의 UserDetails 조회를 위임
  5. Custom UserDetailsService(MemberDetailsService)가 사용자의 크리덴셜 DB에서 조회 후 AuthenticationManager에게 사용자의 UserDetails를 전달
  6. AuthenticationManager가 로그인 인증 정보와 UserDetails의 정보를 비교해 인증 처리
  7. JWT 생성 후, 클라이언트의 응답으로 전달
  • JwtAuthenticationFilter 구현(2-3번), MemberDetailsService(5번) 구현

1. Custom UserDetailsService 구현
데이터베이스에서 사용자의 크리덴셜을 조회한 후, 크리덴셜을 AuthenticationManager에게 전달하는 Custom UserDetailsService를 구현하는 것.

  • MemberDetailsService

2. 로그인 인증 정보 역직렬화(Deserialization)를 위한 LoginDTO 클래스 생성

  • LoginDTO 클래스는 클라이언트가 전송한 Username/Password를 Security Filter에서 사용할 수 있도록 역직렬화 하기 위한 클래스

3. JWT를 생성하는 JwtTokenizer 구현

  • 로그인 인증에 성공한 클라이언트에게 JWT를 생성 및 발급하고 클라이언트의 요청이 들어올때마다 전달된 JWT를 검증하는 역할을 한다.

  • (1) JwtTokenizer 클래스를 Spring Container(ApplicationContext)에 Bean으로 등록하기 위해 @Component 애너테이션 추가
  • (2),(3),(4) 는 JWT 생성 시 필요한 정보, application.yml 파일에서 로드
    • (2) JWT 생성 및 검증 시 사용되는 Secret Key 정보
    • (3) Access Token에 대한 만료 시간 정보
    • (4) Refresh Token에 대한 만료 시간 정보
  • (5) getTokenExpiration() 메서드는 JWT의 만료 일시를 지정하기 위한 메서드로 JWT 생성 시 사용

✔ application.yml

  • JWT의 서명에 사용되는 Secret Key 정보는 민감한 정보이므로 시스템 환경 변수의 변수로 등록
  • ${JWT_SECRET_KEY} 는 OS의 시스템 환경 변수의 값을 읽어오는 일종의 표현식

    ⭐ 시스템 환경 변수에 등록한 변수를 사용할 때는 application.yml 파일의 프로퍼티명과 동일한 문자열을 사용하지 않도록 주의

    시스템 환경 변수와 application.yml에 정의한 프로퍼티명의 문자열이 동일할 경우 yml 파일에 정의된 프로퍼티를 클래스의 필드에서 참조할 때 (예: ${jwt.key.secret}) 시스템 환경 변수의 값으로 채워지므로 개발자가 의도하지 않은 값으로 채워질 수 있다.

    가급적 시스템 환경 변수의 값도 application.yml에서 먼저 로드한 뒤에 application.yml에서 일관성 있게 프로퍼티 값을 읽어오는 것이 좋다.

🔍 맥 사용 시 환경변수 설정
1. 숨김파일 보기 설정 후 user 루트에 .zprofile 파일
2. export JWT_SECRET_KEY="xxxxxxx" 추가
3. 터미널과 인텔리제이 종료 후 다시 실행

🔍 프로젝트 별로 env 파일을 만들어 환경 변수 관리하기
1. resource/env.yml 또는 resource/env.properties 파일 만들기 ( JWT키 추가)
2. 사용하고자 하는 클래스에 @PropertySource 애너테이션 추가 (main메서드가 존재하는 클래스에 추가)
@PropertySource("classpath:/env.yml")
3. @Value 애너테이션 / Enviroment 객체를 통해 환경변수 가져오기
4. 깃허브에 커밋 시에는 .gitignore 에 env 파일을 등록하여 공유하지 않기


4. 로그인 인증 요청을 처리하는 Custom Security Filter 구현

  • 클라이언트의 로그인 인증 정보를 직접적으로 수신하여 인증 처리의 엔트리포인트 역할을 하는 필터

  • (1) UsernamePasswordAuthenticationFilter 를 상속한다. 폼 로그인 방식에서 사용하는 디폴트 Security Filter로써, 폼 로그인이 아니더라도 Username/Password 기반의 인증을 처리하기 위해 확장해서 구현할 수도 있다.
  • (2) AuthenticationManager와 JwtTokenizer를 DI 받는다.
    • DI 받은 AuthenticationManager는 로그인 인증 정보를 전달 받아 UserDetailsService와 인터랙션 한 뒤 인증 여부 판단
    • DI 받은 JwtTokenizer는 클라가 인증에 성공할 경우, JWT를 생성 및 발급함
  • (3) attmptAuthentication()은 메서드 내부에서 인증을 시도하는 로직 구현
    • (3-1) 클라이언트에서 전송한 Username과 Password를 DTO 클래스로 역직렬화 하기 위해 ObjectMapper 인스턴스 생성
    • (3-2) ServletInputstreamLoginDto클래스의 객체로 역질렬화 한다.
    • (3-3) Username과 Password 정보를 포함한 UsernamePasswordAuthenticationToken을 생성
    • (3-4) UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달하여 인증 처리 위임
  • (4) successfulAuthentication() 메서드는 클라이언트의 인증 정보를 이용해 인증에 성공할 경우 호출됨.
    • (4-1) authResult.getPrincipal() 로 Member 엔티티 클래스의 객체를 얻는다.
      AuthenticationManager 내부에서 인증에 성공하면 인증된 Authentication 객체가 생성되면서 principal 필드에 Member 객체가 할당된다.
    • (4-2) Access Token 생성
    • (4-3) Refresh Token 생성
  • (5), (6) Access Token과 Refresh Token을 생성하는 구체적인 로직

5. Custom Filter 추가를 위한 SecurityConfiguration 설정 추가

  • JwtAuthenticationFilter 구현이 되었으면 Spring Security Filter Chain에 추가해서 로그인 인증을 처리하도록 한다.
  • SecurityConfiguration(V2)
  • Spring Security에서는 직접 Custom Configurer를 구성해 Spring Security의 Configration을 커스터마이징 할 수 있다.
    • (1) apply()메서드에 Custom Configurer를 추가 가능
  • (2) Custom Configurer인 CustomFilterConfigurer 클래스. 구현한 JwtAuthenticationFilter를 등록하는 역할을 한다.
    • (2-1) AbstractHttpConfigurer 를 상속해서 Custom Configurer 구현 가능
      AbstractHttpConfigurer를 상속하는 타입과 HttpSecurityBuilder를 상속하는 타입을 제너릭 타입으로 지정할 수 있다.
    • (2-2) configure() 메서드를 오버라이드하여 Configuration을 커스터 마이징
    • (2-3) AUthenticationManager의 객체를 얻을 수 있다.
      getSharedObject() 를 통해서 Spring Security의 설정을 구성하는 SecurityConfigurer간에 공유되는 객체를 얻을 수 있음.
    • (2-4) JwtAuthenticationFilter를 생성하면서 사용되는 AuthenticationManager와 JwtTokenizer를 DI 해준다.
    • (2-5) 메서드를 통해 디폴트 request URL인 /login/v11/auth/login으로 변경
    • (2-6) 메서드를 통해 JwtAuthenticationFilter를 Spring Security Filter Chain에 추가

📖 로그인 인증 테스트

  • postman 실행 -> 회원가입 -> 로그인 인증 요청

📖 로그인 인증 성공 및 실패에 따른 추가 처리

  • Spring Security에서는 Username/Password 기반의 로그인 인증에 성공 했을시, 로그기록이나 사용자 정보를 response로 전송하는 등 추가 처리를 할 수 있는 핸들러(AuthenticationSuccessHandler) 를 지원하며, 인증 실패 시 추가 처리 핸들러(AuthenticationFailureHandler)를 지원한다.

1. AuthenticationSeccessHandler 구현

  • Custom으로 정의하려면 AuthenticationSuccessHandler 인터페이스를 구현해야 한다.(1)
  • onAuthenticationSuccess() 추상 메서드를 구현하여 추가 처리
    (2) 로그 처리 외에도 Authentication 객체에 사용자 정보를 얻은 후, HttpServletResponse로 출력 스트림을 생성하여 response를 전송할 수 있다.

2. AuthenticationFailureHandler 구현

  • (1) AuthenticationFailureHandler 인터페이스 구현
  • onAuthenticationFailure() 추상 메서드 구현하여 추가 처리
  • (2) sendErrorResponse() 메서드 호출하여 출력 스트림에 Error 정보를 담고 있다.
    • (2-1) Error 정보가 담긴 객체를 JSON 문자열로 변환하는데 사용되는 Gson 라이브러리의 인스턴스를 생성
    • (2-2) ErrorResponse 객체 생성 HttpStatus.UNAUTHORIZED 상태 코드 전달
    • (2-3) response의 Content Type이 application/json 이라는 걸 클라이언트에게 알려주도록 HTTP Heeader에 추가
    • (2-4) response의 status가 401임을 클라이언트에게 알려줄 수 있도록 HTTP Header에 추가
    • (2-5) Gson 이용해 ErrorResponse 객체를 JSON 포맷 문자열로 변환 후, 출력 스트림 생성

3. AuthenticationSuccessHandler와 AuthenticationFailureHandler 추가

  • SecurityConfiguration(V3)
  • (3),(4) 와 같이 JwtAuthenticationFilter에 등록

4. AuthenticationSuccessHandler 호출

  • JwtAuthenticationFilter(AuthenticationSuccessHandler 호출 코드 추가)
  • onAuthenticationSuccess() 메서드 호출
  • 실패 핸들러는 알아서 호출됨.

📔 JWT를 이용한 자격 증명 및 검증 구현

📖 JWT 검증 기능 구현

1. JWT 검증 필터 구현

  • JWT를 검증하는 전용 Security Filter를 구현

  • (1)과 같이 OncePerRequestFilter를 확장해서 request 당 한번만 실행되는 Security Filter를 구현할 수 있다.

  • (2) JwtTokenizer와 CustomAuthorityUtils를 DI 받는다.

    • JwtTokenizer는 JWT를 검증하고 Claims(토큰에 포함된 정보)를 얻는데 사용
    • CustomAuthorityUtils는 JWT 검증에 성공하면 Authntication 객체에 채울 사용자의 권한을 생성하는데 사용됨.
  • (3) verifyJws() 메서드는 JWT를 검증하는데 사용되는 private 메서드

    • (3-1) request의 header에서 JWT를 얻고 있다.
      여기서의 JWT는 클라이언트가 response header로 전달 받은 JWT를 request header에 추가해서 서버 측에 전송한 것. Bearer 부분 제거
    • (3-2) JWT 서명을 검증하기 위한 Secret Key를 얻는다.
    • (3-3) JWT에서 Claims를 파싱
      즉, verify()같은 검증 메서드가 따로 존재하는것이 아니라 Claims가 정상적으로 파싱이 되면 서명 검증 역시 성공했다.
  • (4) setAuthenticationToContext() 메서드는 Authenrication 객체를 SecurityContext에 저장하기 위한 private 메서드

    • (4-1) JWT에서 파싱한 Claims에서 username을 얻는다.
    • (4-2) JWT의 Claims에서 얻은 권한 정보를 기반으로 List<GrantedAuthority>를 생성
    • (4-3) usernameList<GrantedAuthority>를 포함한 Authentication객체 생성
    • (4-4) SecurityContext에 Authentication 객체 저장
      • SecurityContext에 Authentication을 저장하게 되면 Spring Security의 세션 정책에 따라서 세션을 생성할 수도있고, 그렇지 않을 수도 있다.
        JWT 환경에서는 세션 정책(Session Policy) 설정을 통해 세션 자체를 생성하지 않도록 설정한다.
  • 문제없이 JWT 서명 검증에 성공하고, Security Context에 Authentication을 저장한 다음에는 (5) 와 같이 다음 Security Filter를 호출한다.

  • (6) OncePerRequestFilter의 shouldNotFilter() 를 오버라이드 한 것, 특정 조건에 부합하면(true) 해당 Filter의 동작을 수행하지 않고 다음 Filter로 건너뛰도록 해준다.

    • (6-1) Authorization header의 값을 얻은 후
    • (6-2) 헤더 값이 null이거나 "Bearer"로 시작하지 않는다면 해당 Filter의 동작을 수행하지 않도록 정의한다.
      • JWT가 Authorization header에 포함되지 않았다면 JWT 자격증명이 필요하지 않은 리소스에 대한 요청이라고 판단하고 다음 Filter로 처리를 넘긴다.

2. SecurityConfiguration 설정 업데이트

  • JwtVerificationFilter를 사용하기 위해서 두가지 설정 추가

    • 세션 정책 설정 추가
    • JwtVerificationFilter 추가
  • 자격 검증에 성공하면 인증된 Authentication 객체를 SecurityContext에 저장하는데 stateless한 애플리케이션 유지를 위해 세션 유지 시간을 아주 짧게 가져가기 위한 (거의 무상태) 설정을 추가할 필요가 있다.

  • SecurityConfiguration(V4)

  • (1) 세션을 생성하지 않도록 설정한다.

  • (2) JwtVerificationFilter의 인스턴스를 생성하면서 사용되는 객체들을 생성자로 DI 해준다.

  • (3) JwtVerificationFilter가 JwtAuthenticationFilter가 수행된 바로 다음에 동작하도록 뒤에 추가한다.


📖 서버 측 리소스에 역할(Role) 기반 권한 적용

  • MemberController를 통해 접근할 수 있는 리소스에 대한 접근 권한 부여

  • SecurityConfiguration(V5)

  • (1) 회원등록 URL 과 HTTP Method(POST) 에 해당된다면 누구나 접근 허용

  • (2) 회원정보 수정의 경우 (2) 일반사용자 (USER) 권한 사용자만 접근 가능

    • MemberController의 patchMember() 핸들러 메서드에 대한 접근 권한 부여 설정
  • (3) 모든 회원 정보 목록은 관리자(ADMIN) 권한을 가진 사용자만 접근 가능

  • (4) 특정 회원 정보 조회는 일반사용자와 관리자 모두 접근 가능

  • (5) 특정 회원 삭제 요청은 해당 사용자가 탈퇴 처리를 할 수 있어야 하므로 일반사용자만 접근 가능


📖 예외 처리

1. JwtVerificationFilter에 예외 처리 로직 추가

  • JWT에 대한 서명 검증에 실패할 경우 throw 되는 SignatureException에 대해 처리
  • JWT가 만료될 경우, 발생하는 ExpiredJwtException 에 대한 처리
  • (1) try~catch 문으로 특정 예외 타입의 Exception이 catch되면 해당 Exception을 request.setAttribute("exception", Exception객체) 와 같이 HttpServletRequest의 애트리뷰트로 추가된다.
    • 추가된 애트리뷰트는 AuthenticationEntryPoint 에서 사용 가능
    • 이런식으로 하는 예외처리는 예외가 발생하면 SecurityContext에 클라이언트의 인증정보가 저장되지 않는다.
      ⭐ SecurityContext에 클라이언트의 인증 정보가 채워지지 않은 상태에서 Security Filter로직을 수행하게 되면 AuthenticationException이 발생한다.

2. AuthenticationEntryPoint 구현

  • Exception 발생으로 인해 SecurityContext에 Authentication이 저장되지 않을 경우 등 AuthenticationException이 발생할 때 호출되는 핸들러 같은 역할

  • 인증과정에서 AuthenticationException이 발생할 경우 호출, 처리하고자 하는 로직을 commence() 메서드에 구현하면 된다.

  • 인증 과정에서 AuthenticationException이 발생하면 ErrorResponse를 생성해서 클라이언트에게 전송

  • ErrorResponder

  • ErrorResponse를 출력 스트림으로 생성하는 역할

3. AccessDeniedHandler 구현

  • 요청한 리소스에 대해 적절한 권한이 없을 경우 호출되는 핸들러로써, 처리하고자 하는 로직을 handle()메서드에 구현

4. SecurityConfiguration에 AuthenticationEntryPoint 및 AccessDeniedHandler 추가

  • SecurityConfiguration(V6)
profile
코린이의 공부 일지

0개의 댓글

관련 채용 정보