본문 바로가기
모각코

[모각코 8회차] Spring Security with JWT

by moonstal 2022. 8. 25.

목표: 시큐리티 파트2

Stateful과 Stateless

Stateless기반의 JWT에 대해 알아보기 전에 Stateful과 Stateless방식의 차이점을 알아보자

  • Stateful

    • Session 사용
    • Session Cluster필요(수평확장 어렵)
    • 단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현 쉬움
  • Stateless

    • Session 사용 x, 근데 서버는 사용자를 식별할 수 있어야 함
    • Session Cluster필요x(수평확장 쉬움)
    • 단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현 어려움

JWT (Json Web Token)

  • Stateless 상태를 유지하며, 서버에서 사용자를 식별할 수 있는 수단을 제공

  • 서버에서 사용자 인증되면 JWT 반환

  • 클라이언트는 JWT를 로컬 영역에 저장하고, 이후 서버에 요청을 보낼 때 JWT를 HTTP 헤더에 포함

  • 구조

    • Header: JWT를 검증하는데 필요한 정보(토큰 타입, 사용된 알고리즘)
    • Payload: JWT를 통해 전달하고자 하는 데이터(Claim-Set:Key-Value 데이터 쌍)
    • Signature: 비밀키를 이용해 헤더에 정의된 알고리즘으로 서명
  • Session과의 차이

    • 자체적으로 필요한 모든 정보를 지님
    • Session을 사용할 경우 Active User 수 만큼 Session을 저장->JWT사용이 유리
    • 유효기간이 남아 있는 정상적인 토큰에 대해 강제 만료 처리 어려움

REST API with JWT

  • Spring Security 설정: csrf, headers, formLogin, http-basic, rememberMe, logout filter 비활성화

  • xml설정

      <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.18.1</version>
      </dependency>
  • yml 설정

    jwt:
    header: token #HTTP Header 이름
    issuer: prgrms #토큰 발급자
    client-secret: EENY5W0eegTf1naQB2eDeyCLl5kRS2b8xa5c4qLdS0hmVjtbvo8tOyhPMcAmtPuQ #HS512 알고리즘으로 서명
    expiry-seconds: 60 #토큰 만료 시간(1분)
  • JwtConfigure class

    @Component
    @ConfigurationProperties(prefix = "jwt")
    public class JwtConfigure {
    
    private String header;
    private String issuer;
    private String clientSecret;
    private int expirySeconds;
    }
  • Jwt class

    • JWT 발행: sign(Claims claims)
    • JWT 검증: verify(String token)
  • Bean 등록

      @Bean
      public Jwt jwt(JwtConfigure jwtConfigure) {
      return new Jwt(
          jwtConfigure.getIssuer(),
          jwtConfigure.getClientSecret(),
          jwtConfigure.getExpirySeconds()
      );
      }
  • username으로 사용자정보를 가져와서 JWT 토큰을 만들고 반환

  • HTTP 헤더를 통해 JWT 토큰을 전달, 토큰의 Claims을 Map으로 변환하여 반환

JwtAuthenticationFilter

  • 필터 구현

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    
    //인증된 사용자 인지 확인
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
      //HTTP 헤더에서 JWT 토큰 가져옴
      String token = getToken(request);
      if (token != null) {
        try {
          //JWT 토큰 검증하고 디코딩
          Jwt.Claims claims = verify(token);
          String username = claims.username;
          List<GrantedAuthority> authorities = getAuthorities(claims);
    
          if (isNotEmpty(username) && authorities.size() > 0) {
            UsernamePasswordAuthenticationToken authentication =
              new UsernamePasswordAuthenticationToken(username, null, authorities); //loginId
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            //UsernamePasswordAuthenticationToken 객체 참조 전달
            SecurityContextHolder.getContext().setAuthentication(authentication);
          }
        } catch (Exception e) {
          log.warn("Jwt processing failed: {}", e.getMessage());
        }
      }
    } else {
      log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'",
        SecurityContextHolder.getContext().getAuthentication());
    }
    
    chain.doFilter(request, response);
    }
  • 필터 추가(SecurityContextPersistenceFilter 필터 바로 뒤)

      public JwtAuthenticationFilter jwtAuthenticationFilter() {
      Jwt jwt = getApplicationContext().getBean(Jwt.class);
      return new JwtAuthenticationFilter(jwtConfigure.getHeader(), jwt);
      }
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
      http
          .addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class);
      }

인증 API 추가 & 코드 리팩토링

  • UsernamePasswordAuthenticationToken -> JwtAuthenticationToken
  • org.springframework.security.core.userdetails.User -> JwtAuthentication
  • JwtAuthenticationToken 타입을 처리할 수 있는 JwtAuthenticationProvider
    • UserService 클래스를 이용해 로그인을 처리하고, JWT 토큰 생성
  • 인증 요청은 JwtAuthenticationToken 객체를 만들어 AuthenticationManager를 통해 처리
  • 내정보 조회 API 구현 — @AuthenticationPrincipal
      //AuthenticationPrincipalArgumentResolver 통해 처리
      @GetMapping(path = "/user/me") //인증필요
      public UserDto me(@AuthenticationPrincipal JwtAuthentication authentication) {
          return userService.findByLoginId(authentication.username)
          .map(user ->
              new UserDto(authentication.token, authentication.username, user.getGroup().getName())
          )
          .orElseThrow(() -> new IllegalArgumentException("Could not found user for " + authentication.username));
      }

    SecurityContextRepository(JwtAuthenticationFilter 대체)

  • JwtAuthenticationFilter의 핵심 역할: HTTP 요청헤더에서 JWT 토큰을 확인, 검증하여 SecurityContext 생성
  • SecurityContextPersistenceFilter 이미 존재
  • SecurityContextRepository에서 SecurityContext을 읽어옴

등록

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http
        .securityContext()
            .securityContextRepository(securityContextRepository())
    }

@프로그래머스 미니 데브코스 & CNU SW Academy 강의 내용 정리