Post

[Spring_Security] 토큰 기반 인증 방식의 JWT 인증 및 인가 과정 구현과 스프링 시큐리티의 Filter Chain

인증 요청 가로채기, OncePerRequestFilter

🐀 OncePerRequestFilter는 스프링 프레임워크에서 제공하는 추상클래스이다

🌱OncePerRequestFilter특정 필터가 각 요청당 한 번만 실행되도록 보장하는 기능을 제공한다. 이는 중복실행을 방지한다.

🌱OncePerRequestFilter는 추상클래스이므로, 이를 상속받은
🦩InitialAuthenticationFilter는 스프링 시큐리티를 사용하여 사용자 인증을 처리할 때 사용되는 필터로 커스텀 할 수 있다.

🦩InitialAuthenticationFilter초기 인증 요청을 가로채고 처리하는 역할을 한다.
인증 처리 후, 성공하면 사용자 정보를 보안컨텍스트에 저장한다.

☕ InitialAuthenticationFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class InitialAuthenticationFilter extends OncePerRequestFilter {
    private final AuthenticationManager authenticationManager;

    public IntialAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override // 원하는 필터링 로직 정의
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return !request.getServletPath().equals("/api/sign-in");
    }

    @Override
    protected String getAlreadyFilteredAttributeName() {
        return getClass().getName() + ".FILTERED";
    }
}

🌱OncePerRequestFilter를 확장한 🦩InitialAuthenticationFilter각 요청당 한번만 실행되도록 보장할 것이고,
그렇기에 (인증이 필요한) 요청당 한 번만 쓰이는 사용자 인증을 처리하기 좋은 필터가 된다.

shouldNotFilter 메서드는 특정 요청에 대해서만 필터를 타게하거나 건너뛰고자 할 때 오버라이드 할 수 있다.

getAlreadyFilteredAttributeName 메서드는 요청당 필터링이 이미 수행되었는지를 확인하기 위해 사용되는 속성 이름을 반환한다.

특정 URL 패턴 매칭 인증 필터, AbstractAuthenticationProcessingFilter

🐁 AbstractAuthenticationProcessingFilter는 스프링 시큐리티에서 제공하는 추상클래스이다

🛡️AbstractAuthenticationProcessingFilter인증 요청을 처리하고, 성공 혹은 실패 시 각기 다른 메서드를 호출한다.

  • attemptAuthentication
    해당 메서드를 구현하여 사용자 인증 로직을 정의한다.
    1
    2
    3
    
    @Override
    public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException, IOException, ServletException;
    
  • successfulAuthentication
    인증이 성공했을 때 호출되는 메서드이다.
    보안컨텍스트에 인증 정보를 저장하고 다음 필터로 요청을 전달한다.
    1
    2
    3
    4
    5
    
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
     throws IOException, ServletException {
      SecurityContextHolder.getContext().setAuthentication(authResult);
      chain.doFilter(request, response);
     }
    
  • unsuccessfulAuthentication
    인증이 실패했을 때 호출되는 메서드이다.
    기본적으로 실패 이유를 로깅하고, 적절한 HTTP 응답을 반환한다.
    1
    2
    3
    4
    5
    6
    
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
     throws IOException, ServletException {
      SecurityContextHolder.clearContext();
    
      response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
     }
    
  • requiresAuthentication
    특정 요청이 인증을 필요로 하는지 여부를 결정하는 메서드이다.
    기본적으로 설정된 url 패턴과 요청 url을 비교하여 인증 필요 여부를 결정한다.
    1
    2
    3
    
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
     return requiresAuthenticationRequestMatcher.matches(request);
    }
    

☕ JwtAuthenticationFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  private static final AntPathRequestMatcher DEFAULT_FILTER_PROCCESSING_URL = new AntPathRequestMatcher("/api/sign-in", HttpMethod.POST.name());

  private final JwtTokenProvider jwtTokenProvider;

  public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
    super(DEFAULT_FILTER_PROCESSING_URL, authenticationManager);
    this.jwtTokenProvider = jwtTokenProvider;
  }

  @Override
  public Authentication attemptAuthentication(
      HttpServletRequest request,
      HttpServletResponse response) throws AuthenticationException {

    // ...(jwt 토큰을 이용한 사용자 인증 로직 생략)

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    return this.getAuthenticationManager().authenticate(authRequest);
  }

  @Override
  protected void successfulAuthentication(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain chain,
      Authentication authResult) throws IOException {

    // ... (보안컨텍스트에 인증정보 저장 로직 생략)
  }

  @Override
  protected void unsuccessfulAuthentication(
      HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException failed) throws IOException {

    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
  }
}

생성자를 호출할 때 쓰이는🛡️AntPathRequestMatcher는 스프링 시큐리티에서 url 패턴을 매칭하는데 사용되는 클래스다.
Spring의 Ant Style Path Pattern을 사용하여 요청 url이 특정 패턴과 일치하는지 확인하는 데 사용된다.

🍍 Ant 스타일 경로 패턴
파일 시스템 경로와 유사한 방식으로 url 패턴을 정의할 수 있게 한다.
또한, 특정 HTTP 메서드를 지정하여 매칭할 수 있다.
기본적으로 대소문자를 구분하지만, 구분하지 않도록 설정할 수도 있다.

해당 클래스를 통해 /api/sign-in을 제외한 모든 요청은 건너뛰고, 매칭된 url만이 다음 FilterChain을 타게 된다.

🛡️AbstractAuthenticationProcessingFilter를 확장한 🦩JwtAuthenticationFilter는 정의한 인증로직을 시도해보고
인증이 성공할 시, 인증 성공 처리 로직을 처리한 뒤, 보안 컨텍스트에 인증 정보를 담을 수 있다.

🫎 SecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final CorsFilter corsFilter;
  private final JwtAuthenticationProvider jwtAuthenticationProvider;
  private final JwtTokenProvider jwtTokenProvider;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    // ...(생략)
    http.addFilter(corsFilter).apply(new MyCustomDsl());
    // ...(생략)

    return http.build();
  }

  public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {

    @Override
    public void configure(HttpSecurity http) {

      AuthenticationManager authenticationManager =
        http.getSharedObject(AuthenticationManager.class);

      InitialAuthenticationFilter initialAuthenticationFilter = new InitialAuthenticationFilter();

      JwtAuthenticationFilter jwtAuthenticationFilter =
        new JwtAuthenticationFilter(authenticationManager, jwtTokenProvider);

      http.addFilterBefore(initialAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        .addFilterAfter(jwtAuthenticationFilter, InitialAuthenticationFilter.class)
        .authenticationProvider(jwtAuthenticationProvider);

      http.addFilter(new JwtAuthorizationFilter(authenticationManager));
    }
  }
}

한편 🫎SecurityConfig 설정파일에서, 🦩InitialAuthenticationFilter의 다음 필터는 🦩JwtAuthenticationFilter로 지정하여 🦩InitialAuthenticationFilter를 통과한 url 요청만이 🦩JwtAuthenticationFilter를 타도록 구현했다.

이처럼, 기존에 스프링 시큐리티에 정의된
추상클래스를 구현하여 내 애플리케이션 상황에 알맞은 사용자 인증 기능을 작성할 수 있다.

세션 기반 인증 방식과 토큰 기반 인증 방식

🐀 세션 기반 인증은 사용자 정보를 세션객체에 저장하고, 토큰 기반 인증은 사용자 정보를 기반으로 토큰을 생성한다
📘 세션과 쿠키란
우선 세션이란 서버에 클라이언트 별로 정보를 저장할 수 있는 저장소이다.
각 저장공간을 식별할 수 있는 세션ID라는 것이 존재하는데,
서버는 클라이언트가 최초 접속 시, 해당 사용자를 앞으로도 구별하기 위해
이 사용자만의 저장소에 대한 식별자, 세션ID를 쿠키라는 작은 데이터 파일에 넣어, 웹브라우저로 보낸다.

따라서, 쿠키를 쓰지 않으면, 세션 저장소를 식별할 ID값도 없기 때문에 세션도 쓰지 못한다.

🍦세션 기반 인증
사용자가 로그인하면 서버는 사용자 정보를 세션객체에 저장하고, 세션ID를 생성
브라우저에 쿠키로 전달한다.
이 이후부터 사용자 요청시마다 쿠키에 포함된 세션ID를 서버에 전송하고,
서버는 해당 세션(저장소)를 조회하여 사용자를 인증한다.

세션 기반 인증은 보안 측면에서 우수하다.
즉, 클라이언트 측에서 정보(세션)가 탈취되더라도 어차피 사용자 정보는 서버에 있기 때문에
탈취된 서버의 세션ID만 변경하면 된다.

🍁 하지만 일반적으로 클라이언트가 웹 브라우저가 아닌 경우, 쿠키를 잘 쓰지 않는다.
세션 기반 인증은 쿠키에 의존적이므로, 쿠키 사용을 막는 환경에서 문제가 발생할 수 있다.

또한 결국, 세션이란건 서버안의 저장소이기 때문에 서버가 상태를 유지해야 한다.
🍁 이는 무상태성(stateless)를 지향하는 Restful 아키텍처와 맞지 않는다.

🍁 더불어, 서버가 늘어나면(scale-out) 서버끼리 세션 정보를 공유해야하는 문제가 발생한다.
로드밸런서에 의해 처음 매핑된 서버가 이후에도 매핑된다는 보장이 없다.

🍧토큰 기반 인증 - JWT (Json Web Token)
사용자가 로그인하면 서버는 사용자 정보를 기반으로 JWT 토큰을 생성하여 클라이언트에 전달한다.
이후 사용자는 요청할 때마다 헤더에 토큰을 포함시켜 서버에 전송하고,
서버는 토큰을 검증하여 사용자를 인증한다.

JWT는 처음 로그인할 때, DB통신으로 얻은 회원객체를 Json형태로 변환한다.
여기에 비밀번호를 걸어 암호화하고, 그 결과인 암호문을 클라이언트에 전달한다.
클라이언트가 요청마다 토큰을 헤더에 넣어 보내면
서버는 암호화할 때 사용한 비밀번호를 이용해서 복호화하여 회원정보가 담긴 Json객체를 얻을 수 있다.
즉, JWT 토큰을 통한 사용자 인증방식은, 최초 로그인을 제외하고
🍁 서버가 네트워크 DB통신을 하지 않고도 인증을 처리할 수 있어 속도가 빠르다.

여기서 복호화하기 위해 썼던 비밀번호가 내가 암호화했던 객체란 것을 보장한다.

🍁 또한, 쿠키 외에도 HTTP 헤더로컬 스토리지 등으로 토큰을 저장할 수 있어 유연성🏆이 뛰어나며,

🍁 토큰은 클라이언트 측에서 관리되므로, (서버가 늘어나는 것과 상관없이) 모바일 앱이나 분산 시스템에서 유용하다.

다만, 단점은 토큰이 탈취되면, 만료되기 전까지 계속 인증을 위해 사용할 수 있다는 것이다.
이같은 단점을 보완하기 위해토큰 만료 시간을 짧게 가져가고, 또 이로 인한 문제는 만료시간이 긴 리프레시 토큰으로 보완해주기도 한다.

참고로, JWT 토큰의 경우 사용자 정보가 포함되어 있기 때문에 크기가 클 수 있어, 오버헤드를 증가시킬 수 있다.

빈(Bean)이 초기화될 때 특정 동작 수행, InitializingBean

🐁 Jwt 토큰 생성을 위해 특정 알고리즘을 사용해 비밀키를 생성하는데, 초기화할 때 비밀키를 생성할 수 있다

보통 자신이 설정한 (비밀키를 위한) 문자열은 Base64 인코딩한 후,
yml 파일에 작성한다.

📘 Base64 란
Base64 알고리즘바이너리 데이터텍스트 형식으로 인코딩하는데 사용되는 알고리즘이다.

주로 이메일 전송, url 인코딩 같은 상황에서 바이너리 데이터를 안전하게 전송하기 위해 사용된다.
⚠️ Base64 알고리즘은 6비트 단위의 바이너리 데이터를 64가지 ASCII 문자로 변환한다.

반대로 Base64 디코딩텍스트 데이터바이너리 형식으로 바꿔주는 과정이다.

자바용 JWT 라이브러리인 JJWT를 통해 HMAC-SHA Algorithm 구현한 메서드를 이용해 비밀키를 생성할 수 있다.

1
2
3
4
5
dependencies {
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}
📕 HMAC SHA 알고리즘이란
Hash-based Message Authentication Code Secure Hash Algorithm
메세지의 무결성을 검증하기 위해 사용되는 암호화 해시 함수이다.

HMAC는 메세지 인증코드를 생성하는 방법으로, 해시함수와 비밀키를 결합해서
메세지의 무결성을 확인하고 인증한다.
SHA해시함수로, SHA-1, SHA-256, SHA-512 등의 변종이 있다.
SHA 알고리즘은 고정 길이의 해시값을 생성한다.

SHA-256은 키를 생성할 문자열의 크기가 최소 32바이트, SHA-512는 최소 64바이트라는 의미를 가진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
public class JwtTokenProvider implements InitializingBean {

  private final String secret;
  private final Long tokenValidityInMilliseconds;
  private Key key;

  public JwtTokenProvider(Environment env) {
    this.secret = env.getProperty("jwt.secret");
    this.tokenValidityInMilliseconds = Long.parseLong(Objects.requireNonNull(env.getProperty("jwt.token-validity-in-seconds"))) * 1000;
  }

  @Override
  public void afterPropertiesSet() {
    byte[] keyBytes = Decoders.BASE64.decode(secret);
    // yml에 Base64인코딩되어 있는 해시값을 디코딩한다. (텍스트 -> 바이너리 데이터)
    this.key = Keys.hmacShakeyFor(keyBytes);
    // HMAC SHA 암호화 알고리즘, 해시값 생성
  }

  public String createToken(Authentication authentication) {
    // 인증된 사용자
    MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();

    String authorities = authentication.getAuthorities().stream()
      .map(GrantedAuthority::getAuthority)
      .collect(Collectors.joining(","));

      long now = (new Date()).getTime();
      Date validity = new Date(now + this.tokenValidityInMilliseconds);

      return Jwts.builder()
        .setSubject(authentication.getName()) // 토큰 주제 설정
        .claim("memId", memberDetails.getMember().getMemberId().toString()) // 사용자 정의 클레임 추가
        .claim("memberRole", authorities)
        .signWith(key, SignatureAlgorithm.HS512) // 토큰을 서명할 알고리즘과 비밀 키 설정
        .setExpiration(validity) // 토큰 만료시간 설정
        compact(); // 직렬화된 문자열 반환
  }
}

Keys.hmacShakeyFor()가 JJWT 라이브러리가 제공하는 HMAC-SHA Algorithm을 구현한 메서드이다.

즉, 인코딩되어 yml에 작성된, 키를 위한 해시값을 디코딩하고 암호화하여 (JWT 서명을 생성하고 검증하는데 필요한) 비밀키를 생성한다.

이 작업을 하기 좋은 시점이 빈이 생성되고 초기화된 직 후이고,
InitializingBean 인터페이스의 afterPropertiesSet 메서드를 구현해 이 시점을 맞춰줄 수 있다.

요즘 스프링 애플리케이션에서는 @PostConstruct를 사용하는 것이 더 일반적이다.
스프링 프레임워크에 덜 종속적이며, 가독성🏆이 좋기 때문이다.

기본 토큰 구조 정의, AbstractAuthenticationToken

🐁 AbstractAuthenticationToken은 스프링 시큐리티에서 제공하는 토큰 구조 정의 추상 클래스이다

🪙AbstractAuthenticationToken은 인증과 관련된 정보를 캡슐화한다.
인증주체 principal, 권한정보 authorities, 자격증명 credentials, 인증상태관리isAuthenticated setAuthenticated

Jwt Token 서명을 통해, 서명이 정상이면 Authentication 객체를 만들어준다.
스프링 시큐리티가 만들어 주는 이 Authentication 객체는 사실 Principal을 확장한 형태이다.
Authentication extends Principal

🪙 JwtAuthenticationToken.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Getter
public class JwtAuthenticationToken extends AbstractAuthenticationToken {

  private final Object principal;
  private final Object credentials;

  public JwtAuthenticationToken(Object principal, Object credentials) {
    super(null); //authorities: null
    this.principal = principal;
    this.credentials = credentials;
    setAuthenticated(false);
  }

  public JwtAuthenticationToken(
      Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(true);
  }

  //인증 처리 전 호출
  public static JwtAuthenticationToken unauthenticated(Object principal, Object credentials) {
    return new JwtAuthenticationToken(principal, credentials);
  }

  // 인증 처리 후 호출
  public static JwtAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
    return new JwtAuthenticationToken(principal, credentials, authorities);
  }
}

Jwt 토큰 생성

🐙 Jwt 토큰 생성
  • Jwt 토큰
    🪙 header.payload.signature

    • Header 해싱 알고리즘, 토큰 타입

      1
      2
      3
      4
      
      {
        "alg": "HS256",
        "typ": "JWT"
      }
      
    • Payload 토큰 주제, 토큰 만료시간, …
      claim이란 Jwt가 정보를 표현하는 용어다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      {
        "sub": "subject",
        "name": "peter",
        "iat": 1516239022,
        "exp": 1516242622,
        "iss": "issuer",
        "aud": "audience",
        "roles": ["user", "admin"],
        "permission": {
          "read": true,
          "write": false,
          "delete": false
        }
      }
      
    • Signature
      (Encoded)헤더 + (Encoded)페이로드 + 비밀키
      1
      2
      3
      4
      
      HMACSHA256(
        base64UrlEncode(header) + "." + base64UrlEncode(payload),
        secret
      )
      

Authorization 헤더를 통한 인증과, BasicAuthenticationFilter

🏈 BasicAuthenticationFilter는 스프링 시큐리티에서 HTTP Basic 인증을 처리하는 필터다

헤더에 인코딩된 자격증명이 노출될 수 있고, 이를 복호화할 수 있기 때문에
HTTP Basic 인증은 권고하지 않는다.

그럼에도 불구하고 스프링 시큐리티에서 제공하는 🛡️BasicAuthenticationFiler를 상속받아 쓸 수 있다.
이 같은 인증방식이 가능한 이유는 단순 자격증명(이름, 암호)를 Authorization 헤더에 넘기는 것이 아닌
인코딩된 헤더와 페이로드 그리고 비밀키를 조합해서 해싱 알고리즘을 돌리고, 이를 통해
생성한 해시값(Signature)을 보내기 때문에, 보안측면에서 더 안전하다.

결국, 🛡️BasicAuthenticationFilter를 쓰는 이유는 이 필터가 Authorization 헤더를 읽고 유효한 인증정보를 확인하여 인증을 수행하는 필터이기 때문이다.

🦩 JwtAuthorizationFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

  private final String HEADER_STR = "Authorization";
  private final String TOKEN_PREFIX = "Bearer ";
  private final String secret;

  private final MemberRepository memberRepository;

  public JwtAuthorizationFilter(
    AuthenticationManager authenticationManager,
    MemberRepository memberRepository,
    Environment env) {
      super(authenticationManager);
      this.memberRepository = memberRepository;
      this.secret = env.getProperty("jwt.secret");
  }

  @Override
  protected void doFilterInternal(
    HttpServletRequest request,
    HttpServletResponse response,
    FilterChain chain) {

      String header = request.getHeader(HEADER_STR);
      if(header == null || !header.startsWith(TOKEN_PREFIX)) {
        chain.doFilter(request, response);
        return;
      }

      String jwtToken = header.replace(TOKEN_PREFIX, "");

      byte[] keyBytes = Decoders.BASE64.decode(secret);
      SecretKey key = Keys.hmacShaKeyFor(keyBytes);

      try {
        // Jwt가 사용자 정보 표현하는 객체를 가져온다
        // 이때 비밀키가 필요하다
        Claims claims = Jwts.parserBuilder()
          .setSigningKey(key)
          .build()
          .parsingClaimsJws(jwt)
          .getBody();

        // Claim으로부터 정보를 가져온다
        Long memId = Long.parseLong(claims.get("memId", String.class));
        if(memId != null) {
          // 빼내온 정보(사용자식별ID)로 부터 DB통신을 통해 세부 정보를 가져온다
          Member foundMem = memberRepository.findById(memId).orElseThrow();
          // 시큐리티 사용자를 나타내는 임의의 객체를 생성한다
          MemberDetails memDetails = new MemberDetails(foundMember);

          // 임의의 사용자 인증 토큰을 생성한다
          // 이미 인증된 경우이므로 credentials에는 null을 넣어준다
          Authentication authentication = new UsernamePasswordAuthenticationToken(memDetails, null, memDetails.getAuthorities());

          // 강제로 시큐리티의 세션에 접근하여 인증객체를 넣어준다
          // 인증 완료
          SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
          // ...에러 처리(생략)
        }

        chain.doFilter(request, response);
      }
  }
}

🛡️SecurityContextHolder현재 스레드와 연관된 보안컨텍스트 객체를 저장하고 관리한다.
스프링 시큐리티는 인증과정에서 🛡️SecurityContextHolder인증정보를 자동으로 설정하지만,
위의 코드와 같이 특정 시나리오에서는 직접 설정할 수 있다.

Authentication auth = SecurityContextHolder.getContext().getAuthentication()
이처럼 🛡️SecurityContextHolder로부터 인증객체를 가져올 수 있고,
인증 객체를 통해 다양한 인증 정보를 빼올 수도 있다.
auth.getName() auth.getAuthorities() auth.isAuthenticated

스프링 시큐리티 FilterChain과 Jwt 인증/인가 플로우

⚔️ 스프링 시큐리티 FilterChain과 Jwt 인증/인가 플로우

🫎 SecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  // ...(생략)

  public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {

    AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);

    // ...(생략)

    http.addFilterBefore(initialAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
      .addFilterAfter(jwtAuthenticationFilter, InitialAuthenticationFilter.class)
      .authenticationProvider(jwtAuthenticationProvider);

      http.addFilter(new JwtAuthorizationFilter(authenticationManager));
  }
}

시큐리티 설정에서 기존 filter chain에 다음과 같은 흐름을 가지는 맞춤형 필터를 구성할 수 있다.

Alt text

인증관리자는 스프링 시큐리티의 기존 인증관리자를 사용했고,
인증공급자는 기존 인증공급자를 확장한 맞춤형 빈 JwtAuthenticationProvider를 사용했다.

☕ JwtAuthenticationProvider.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class JwtAuthentiationProvider implements AuthenticationProvider {

  private final MemberDetailsService memberDetailsService;
  private final PasswordEncoder passwordEncoder;

  public JwtAuthenticationProvider(MemberDetailsService memberDetailsService) {
    this.memberDetailsService = memberDetailsService;
    this.passwordEncoder = new BCryptPasswordEncoder();
  }

  // 지원하지 않는 인증 객체를 받으면 null 반환
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    String username = authentication.getName();
    String password = authentication.getCredentials().toString();

    MemberDetails memberDetails = memberDetailsService.loadUserByUsername(username);

    //탈퇴한 회원
    if(!memberDetails.isEnabled()) {
      throw new AuthenticationException(CustomErrMessage.INACTIVE_MEMBER) {
        @Override
        public String getMessage() { return super.getMessage(); }
      }
    }

    // 암호 인코더로 암호(비밀번호) 검증
    if(!passwordEncoder.matches(password, memberDetails.getPassword())) {
      throw new BadCredentialsException(CustomErrMessage.NOT_FOUND_CREDENTIALS);
    }

    return JwtAuthenticationToken.authenticated(memberDetails, null, memberDetails.getAuthorities());
  }

  // 지원하는 인증 유형 설정
  @Override
  public boolean supports(Class<?> authenticationType) {
    return authenticationType.equals(JwtAuthenticationToken.class);
  }
}

사용자를 찾기 위해 authenticate 메서드가 호출되는데 이 때, 파라미터로 해당 인증공급자가 지원하지 않는 객체를 받으면 null을 반환한다.

여기서 사용자 인증과정이 일어난다.
db통신을 통해 사용자의 정보를 찾고, PasswordEncoder를 통해 비밀번호가 맞는지 확인한다.

만일, 인증과정에서 예외가 발생한다면AuthenticationException이라는 스프링 시큐리티가 제공하는 예외가 발생하고,
이 예외는 스프링 시큐리티 핸들러에 의해 처리될 수 있다.

사용자 인증과정에서 제공된 자격증명이 유효하지 않을 때 BadCredentialException이 발생하는데 이는 AuthenticationException의 확장된 형태이다.

This post is licensed under CC BY 4.0 by the author.