BackEnd Developer, Love Crossfit, Welcome to Jay's blog

프로그래머스 웹백엔드 스터디 7기 / 2주차 - Spring Security 활용한 인증과 인가

|

프로그래머스 웹백엔드 스터디 7기 활동을 하면서 공부한 내용과 기능 구현 과정 고민을 정리한 내용입니다.


Index

  • 스터디 2주차
    • 느낀점
    • 고민
    • 공부한 내용
    • TODO 기능 구현
  • References

스터디 2주차

Spring Security 활용한 인증과 인가

느낀점

이번 스터디의 주제는 세션 인증 방식과 JWT 인증 방식부터 시작해 스프링 시큐리티가 무엇인지, 어떻게 인증과 인가가 수행되는지 스프링 시큐리티의 역할과 동작원리에 대해 학습하였다. 그리고 JWT 토큰 인증으로 커스텀화하여 유저를 식별하고, Voter를 이용해 포스트 공개 범위를 친구로 제한해보는 기능을 구현해보았다. 전반적인 시큐리티의 흐름을 코드로 경험해볼 수 있는 시간이었다.

무엇보다 포스트 공개 범위에 대한 추가 요구사항이 일어났을 때 (ex.친구공개가 아니라 친구의 친구까지도 공개할 수 있도록 추가) 기존 코드는 변경없이 확장에 용이하도록 코드를 작성해야한다는 것을 배웠다. 그리고 이러한 OCP 개방 폐쇄의 원칙을 전략패턴으로 직접 리팩토링해봄으로써 실제 코드로 구현해 볼 수 있었다.(<= 제일 좋았던 점)

하지만 그만큼 처음 다뤄보는 기술이어서 그런지 어렵기도 했다. 무엇보다 공부하고 이해할 개념들이 많아서 정신이 없는 주차였다. 그래도 계속 반복해서 보다 보니 지금은 처음보다 많이 나아짐이 느껴진다.스터디가 끝나고 로그인 구현하는 연습을 더 많이 해봐야 겠다.

고민

  • 이메일 중복 체크 API에서 RequestBody에 이메일 1개 밖에 안 들어오는데, 다른 API들처럼 별도의 DTO 클래스를 굳이 만들어야 의문이 들었다.

    => 코드 리뷰를 통해 꼭 DTO 대신 Map을 이용할 수도 있다는 것을 알았다. Map을 이용할 경우 추가적인 class가 필요하지 않는다는 장점이 있지만, 다른 사람이 코드를 볼 경우 해당 파라미터로 어떤 값이 들어오는지 알기 어렵기 때문에 키값에 대한 정보를 주석이나 Swagger 같은 문서화 도구를 이용해 명시해줘야 한다는 단점이 있다. DTO는 어떤 값이 들어올지 쉽게 알 수 있다는 장점이 있지만, class를 새로 만들어야 한다는 단점이 있다.

  • 나의 게시물을 나와 친구만 볼 수 있도록 구현하는 부분

    => 이를 위해서는 본인 또는 친구관계 일때만 특정 패턴 API**(/api/user/{userId}/)를 호출할 수 있어야 했는데 이 부분이 어려웠다.

    • 요청 URI가 처리해야하는 URI인지 어떻게 알지?
    • 처리해야하는 URI라면 어떻게 UserId를 가져 올 것인가?

    => URI 판단을 위해 RequestMatcher 인터페이스 구현체 중 정규식 처리를 이용하는 방법과 URI에서 userID를 추출하기 위해 Function 인터페이스와 람다함수를 이용하는 방법에 대해 알게 되었다.

    private final RequestMatcher requiresAuthorizationRequestMatcher;
    
    private final Function<String, Id<User, Long>> idExtractor;
    
    private UserService userService;
    
    public ConnectionBasedVoter(RequestMatcher requiresAuthorizationRequestMatcher, Function<String, Id<User, Long>> idExtractor) {
        checkNotNull(requiresAuthorizationRequestMatcher, "requiresAuthorizationRequestMatcher must be provided.");
        checkNotNull(idExtractor, "idExtractor must be provided.");
    
        this.requiresAuthorizationRequestMatcher = requiresAuthorizationRequestMatcher;
        this.idExtractor = idExtractor;
    }
    
  • Voter가 지원하는 URI가 아닐 경우 접근 승인 종류를 무엇으로 내려줘야하는지

    => 특정 패턴 API (/api/user/{userId}/)에 접근하는 요청이 아니라 다른 리소스에 대한 요청이면 접근 권한을 판단할 수 없는 상황이라 생각해 ‘승인’과 ‘거부’가 아닌 ‘보류’로 판단했었는데, 앞에서 WebExpressionVoter가 승인을 해주기 때문에 의미적으로 승인 처리가 더 맞는 것 같아 승인으로 처리해주었다.

    
    if (!requiresAuthorizationRequestMatcher.matches(request)) {
    return ACCESS_GRANTED;
    }
    
    

공부한 내용

  • 인증(Authentication) vs 인가(Authorization)
    • 인증: 신원을 확인 & 식별하는 것 ex.로그인
    • 인가: 인증된 사용자가 해당 리소스에 권한이 있는가, 특정 리소스 접근할 수 있는 역할(Role)을 부여하는 것
    • 대부분의 Spring 기반 프로젝트는 Spring Security를 이용해 사용자를 식별하고 특정 리소스에 접근할 수 있도록 역할을 부여한다
  • Web 서비스 아키텍처
    • 3-Tier 아키텍처
      • Data Layer - 데이터 저장/조회
      • Application Layer - 트랜잭션 처리, 비즈니스 로직
      • Presentation Layer - 사용자와의 접점, UI, ex 안드로이드앱, 브라우저 웹페이지
    • 장점
      • 각 계층을 모듈화해 서로 미치는 영향을 최소화
      • 수평 확장에 용이
    • 현재 많은 웹 서비스들이 3-Tier 아키텍처 구조로 되어 있다.
      => 하지만, 문제점은 없을까?
  • Session 기반 사용자 인증

    Session 기반의 서비스를 운영 중에 있다고 가정 해보자. 어느날 서비스가 잘되서 트래픽이 늘어났다. 마침 우리의 서비스는 3-Tier 아키텍처 구조로 되어 있기 때문에, 서버 노드 수를 늘림으로써(application layer를 옆으로 늘린다) 모든 트래픽을 견뎌낼 수 있었다.

    하지만, 어느날, 특정 노드에 장애가 생겨 해당 서버의 사용이 불가능해졌다. 이럴 경우, 그 서버에 있던 사용자 정보들은 어떻게 되는 걸까?

    • 세션은 서버의 메모리를 사용하기 때문에, 장애 발생시 유실될 것이다. 따라서 문제가 생긴 서버에 저장되어 있던 세션 정보는 더이상 사용하지 못하게 되어 로그인이 모두 풀려버린다. 즉, 다시 로그인을 해야 됨.
    • 특정 서버에 장애가 생겨도 다른 서버를 통해 서비스가 지속 가능하려면 모든 서버는 세션을 공유하고 있어야 한다. 이를 위한 것이 바로 세션 클러스터.
    • Spring에는 Redis 기반의 spring session 컴포넌트가 있음
    • 하지만, 세션 클러스터 또한 단점(클러스터 관리 포인트 증가, 클러스터 문제시 대규모 장애로 이뤄짐)이 있는데.. 이러한 단점 없이 훨씬 간단하게 사용자 인증을 하는 방법이 없을까? =====» JWT!!
  • JWT(Json Web Token)
    • 더이상 세션을 이용하지 않음
    • 세션은 Stateful(상태를 어딘가에 저장)했다면 JWT는 Stateless(상태를 저장하지 않음)
    • 어떻게 Stateless 할 수 있단 말인가?
    • JWT는 필요한 모든 정보를 자체적으로 지니고 다님 alt text
    • 위 사진에서 왼쪽의 인코딩된 상태가 서버와 클라이언트간에 주고 받는 형태. 요청/응답의 헤더에 JWT를 넣어서 주고 받음
    • 서버에서는 요청 헤더의 JWT를 디코딩해서 유저 정보를 가져옴.
      • JWT를 디코딩하면 다음 세부분으로 이루어져 있다.
        • header - JWT 메타데이터
        • payload - 유저 식별 정보가 들어있음
        • signature - 토큰의 payload 위변조 확인을 위한 것
    • 주의할 점
      • JWT Payload에 사용자 민감 정보(email, password 등)은 넣으면 안된다. 사용자 식별값(PK, 유저 ID)을 넣어야 한다.
    • 자세한 내용은 다음 포스트에서
  • Spring Security
    • 주요 개념 / 용어
      • 접근 주체(Principal): 보호된 리소스에 접근하는 사용자
      • 인증(Authentication):
        • ‘증명하다’라는 의미로, 유저가 누구인지 신원을 확인하는 과정
        • 유저 아이디와 비밀번호를 이용하여 로그인 하는 과정
        • 관련 컴포넌트: AuthenticationManager, AuthenticationProvider
      • 인가(Authorization):
        • 인증된 사용자가 리소스에 접근할 수 있는 권한이 있는지 확인 또는 권한 부여**(Access)
        • 관련 컴포넌트: AccessDecisionManager, AccessDecisionVoter
      • GrantedAuthority
        • 인증된 사용자가 가지고 있는 권한 (ROLE_*)
      • SecurityContext
        • 접근 주체(Aithentication)과 인증정보(GrantedAuthority)를 담고 있는 Context
        • ThreadLocal 영역에 저장되고, SecurityContextHolder(인메모리 세션 저장소) 통해 context 정보를 가져온다
    • 아키텍처와 Flow
      • 스프링 시큐리티는 크게 인증과 인가 부분으로 나뉘며 인증 처리는 Authentication Manager 컴포넌트, 인가 처리는 AccesionDecision Manager라는 컴포넌트가 담당하여 처리한다. 그리고 이러한 인증과 인가 작업은 일련의 필터 체인을 통해 이루어진다. 자세한 내용은 아래 포스트에 따로 정리해 놓았다.
      • 추가 학습 / 정리한 내용은 다음 포스트에서
      • [Spring Security 소개] 포스트 보러가기
      • [Spring Security 인증] 포스트 보러가기
      • [Spring Security 인가] 포스트 보러가기
    • 우리는 JWT를 이용한 인증과 인가를 커스텀해서 구현해 볼 것이다.

TODO 기능 구현

1. User, ConnectedUser 모델에 NotNull 속성의 프로퍼티 추가

  • 친구관계에 있는 유저(커넥티드유저)를 조회하는 SQL 및 RowMapper 수정필요

2. 사용자 인증시 발급되는 JWT의 데이터 파트에 앞에서 추가한 이름을 포함시키기

3. 가입시 이메일 중복을 확인할 수 있는 API 추가

  • 중복이라면 true를 리턴한다.

4. 헬스체크를 위한 API 추가

  • 해당 API는 퍼블릭으로 시큐리티에서 설정 필요
  • /api/_hcheck
  • 현재 Unix Timestamp를 반환

5. 본인 또는 친구 관계시만 특정패턴 API(/api/user/{userID}/**)를 호출할 수 있도록 커스텀 Voter 완성

  • 현재 접근 중인 URL이 해당 voter에서 감시해야하는 URL인지 유연하게 판단할 수 있어야함
  • url에서 userID추출을 유연하게 할 수 있는 방법에 대해 고민

References

JWT

  • https://jwt.io/introduction
  • https://meetup.toast.com/posts/239