포스트

240304 기록, jwt적용(3)

LoginFilter

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
67
68
69
70
import com.example.userservice.dto.MyUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
// UsernamePasswordAuthenticationFilter는 원래 HttpSecurity#formLogin에서 진행을 했는데
// jwt를 위해 formLogin을 disable 시켰기 때문에 개발자가 구현해줘야 한다

  private final AuthenticationManager authenticationManager;

  // JWTUtil 클래스를 통해서 로그인이 성공했을 때 토큰 발급
  // SecurityConfig에 가서 LoginFilter 생성자에 JWTUtil 파라미터도 추가해주어야 한다
  private final JWTUtil jwtUtil;

  @Override
  // 미승인 Authentication 생성하는 메소드
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{

      // 요청을 가로채서 username과 password를 추출
      String username = obtainUsername(request);
      String password = obtainPassword(request);

      System.out.println("username : " + username);

      // 스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 한다
      UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
                                                                                          // username, password, role
      // token에 담은 검증을 AuthenticationManager로 전달
      return authenticationManager.authenticate(authToken);
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
      MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
      String username = myUserDetails.getUsername();

      Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
      Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
      GrantedAuthority auth = iterator.next();

      String role = auth.getAuthority();

      String token = jwtUtil.createJwt(username, role, 60*60*10L);

      response.addHeader("Authorization", "Bearer " + token);
      // 위의 HTTP인증방식은 RFC 7235 정의에 의해서 공식적으로 형식을 지정해줌
      // Authorization: 타입 인증토큰
      // Authorization: Bearer 인증토큰string
      // "Bearer "의 경우 공백을 끝에 붙여줘야 함
  }

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
      response.setStatus(401);
  }
}
1
2
3
4
LoginFilter loginFilter = new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil);
        loginFilter.setFilterProcessesUrl("/api/users/loginProc");
        http
                .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
  • 로그인 성공 시 토큰을 생성하기 위해 로직이 구현되어 있는 JWTUtil을 의존성 주입해 준다
  • 시큐리티에서 로그인필터 등록 시 객체를 생성하며 등록을 했는데, JWTUtil을 필드에 선언하였으므로, 그곳에 매개변수로 JWTUtil을 추가해준다.
  • 로그인 성공 시 Authentication을 통해 username, role 을 가져와 토큰을 생성한다.
  • 생성된 토큰을 response header에 넣어준다. 넣을 때 형식에 주의한다.
  • response.addHeader(“Authorization”, “Bearer “ + token);
  • Bearer의 뒤에는 공백이 있다.
  • 로그인 실패 시 response의 상태를 401 에러로 세팅한다.
테스트
  • 포스트맨을 이용하여 로그인 진행
  • 로그인 성공 시 응답 헤더 탭으로 가보면
  • Authorization이 있고
  • Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIyQGFiYy5jb20iLCJyb2xlIjoiUk9MRV9VU0VSIiwiaWF0IjoxNzA5NTM1Nzg4LCJpc3MiOiJqd0tpbSIsImV4cCI6MTcwOTUzNTgyNH0.bhcxG4A0YCplb6-42p4oCUNvEc6LXFPRZ26LSRlr8t0
  • 라는 토큰이 발행됨
  • 이제 다른 서비스 요청 시 이 토큰의 정보를 담아 요청하면 됨
  • 아직은 토큰 검증 로직이 없어서 진행이 되지 않음
트러블슈팅
  • 포스트맨으로 로그인을 진행하였는데 에러발생
  • ECDSA signing keys must be PrivateKey instances
  • 원인을 찾아보니 토큰 생성 시 암호화 알고리즘을 .signWith(key, SignatureAlgorithm.ES256) 로 했었다.
  • .signWith(key, SignatureAlgorithm.HS256)로 수정.

JWT 검증 필터 구현

  • 스프링 시큐리티 filter chain에 요청에 담긴 JWT를 검증하기 위한 커스텀 필터
  • 해당 필터를 통해 요청 헤더 Authorization에 담긴 JWT를 검증하고 강제로 SecurityContextHolder에 세션을 임시로 생성
  • 해당 세션은 STATELESS 상태로 관리되므로 해당 요청이 끝나면 소멸된다
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import com.example.userservice.dto.MyUserDetails;
import com.example.userservice.model.User;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
@Component
public class JWTFilter extends OncePerRequestFilter {
        // OncePerRequestFilter 는 요청에 대해 한번만 동작한다

  private final JWTUtil jwtUtil;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    // request의 해더에 담아진 토큰 정보를 해석하여 유저 정보를 가져와야 한다
    // 이를 JWTUtil이 진행한다

    // 토큰 검증 구현
    // request에서 Authorization 헤더를 찾는다
    String authorization = request.getHeader("Authorization");

    // request 헤더에서 가져온 authorization이 null인지 혹은 Bearer로 시작하는지 확인
    if(authorization == null || !authorization.startsWith("Bearer ")){
        System.out.println("token null");
        
        // filterChain으로 엮여있는 다음 필터에 doFilter를 통해 request와 response를 넘겨준다
        filterChain.doFilter(request, response);

        // 조건에 해당되면 메소드 종료 (필수)
        return;
    }

    // 토큰을 분리하여 소멸 시간 검증
    String token = authorization.split(" ")[1];
    // Bearer 와 뒤의 토큰 정보를 분리하고, 토큰 정보를 받아옴

    // JWTUtil에 구현한 유효기간 검사
    if(jwtUtil.isExpired(token)){

        System.out.println("token expired");

        // filterChain으로 엮여있는 다음 필터에 doFilter를 통해 request와 response를 넘겨준다
        filterChain.doFilter(request, response);

        // 조건에 해당되면 메소드 종료 (필수)
        return;
    }

    // 여기까지 왔으면, 토큰이 있고, 양식도 제대로 되어있고 유효기간도 남아있다는 것
    // 토큰을 기반으로 일시적인 세션을 만들어서 저장한다.
    // 토큰에서 username과 role을 획득한다

    String username = jwtUtil.getUsername(token);
    String role = jwtUtil.getRole(token);

    // User Entity에 값을 설정하여 생성
    User user = new User();
    user.setEmail(username);
    user.setPassword("tempPassword");
    user.setRole(role);
    // 패스워드의 경우 토큰에 담겨있지 않았는데, 비밀번호도 초기화를 해줘야 한다
    // 비밀번호를 DB에서 매번 요청이 올 때마다 조회를 하면 비효율적이므로 임시로 만들어 초기화

    // MyUserDetails 객체를 만든다.
    MyUserDetails myUserDetails = new MyUserDetails(user);

    // 스프링 시큐리티에 인증 토큰 생성
    Authentication authentication = new UsernamePasswordAuthenticationToken(myUserDetails, null, myUserDetails.getAuthorities());

    // 세션에 사용자 등록
    SecurityContextHolder.getContext().setAuthentication(authentication);

    filterChain.doFilter(request, response);

    // 검증 필터를 만들었다면 SecurityConfig에 가서 필터를 등록해주자 
  }
}
  • SecurityConfig에서 LoginFilter 앞에 필터 등록
1
2
3
4
5
6
7
8
// 토큰 검증 필터 등록
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

        LoginFilter loginFilter = new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil);
        loginFilter.setFilterProcessesUrl("/api/users/loginProc");
        http
                .addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
트러블슈팅
  • JWTUtil에서 .parseClaimsJwt(token) 로 인해서 에러 발생
  • Signed Claims JWSs are not supported.
  • .parseClaimsJws(token) 로 변경하여 해결
깃 주소

GitHub