본문 바로가기

스터디 기록/SpringBoot 심화 스터디

Jwt 토큰 생성 세팅과 필터 등록하기

먼저 Jwt 란 뭔지 간단하게 알아보자

Jwt(Json Web Token)

Jwt은 주로 사용자 인증 및 정보 전달에 사용하는 토큰 기반 인증 방식이다. 토큰 자체에 사용자 정보를 포함하고 있기 때문에 서버 측 세션 저장소가 필요하지 않다는 장점이 있다. 

 

- 헤더 : 어떤 알고리즘을 사용해서 서명했는지에 대한 내용

- 페이로드 : 인증 또는 인가에 필요한 사용자 정보를 담고 있는 부분

- 서명 : 토큰의 신뢰성을 보장하기 위한 부분. 헤더와 페이로드의 데이터 무결성을 확인하기 위해 서버는 헤더와 페이로드를 조합하고, 서버의 시크릿키로 서명하여 이 서명을 생성한다. 

 

인코딩되어있는 Jwt 토큰을 디코딩해보면 위처럼 헤더, 페이로드, 서명값을 얻을 수 있다.

 

Jwt 기본 정보 세팅을 해보자

jwt 폴더 안에 먼저 Jwt의 기본 정보들을 넣어주자. 

public interface JwtVO {
    public static final String SECRET = "메타코딩"; // HS256 (대칭키) 서버의 키
    public static final int EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7; // 만료시간은 일주일
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER = "Authorization";
}

 

- SECRET : 토큰을 암호화할 때 필요한 중요한 시크릿키이다. 원래 절대 노출되면 안된다.

- EXPIRATION_TIME : Jwt 토큰의 만료시간을 정해준다. 보통 일주일로 지정한다.

- TOKEN_PREFIX : Jwt 토큰이 Authorization 헤더에 포함될 때 앞에 붙는 Bearer 접두사이다. 전처리 시 떼어줘야하기 때문에 PREFIX로 정해주었다.

- HEADER : 클라이언트가 Jwt를 서버로 보낼 때 사용하는 HTTP 헤더 필드 이름이다. 일반적으로 Authorization 헤더에 Jwt를 포함해 보내도록 한다.

 

Jwt 필터 세팅을 해보자

이제 인증 필터와 인가 필터를 구현해야한다. 전체적인 흐름을 그림으로 정리해보았다. 

 

JwtAuthenticationFilter

=> 인증을 수행하는 필터이다. 

- 기본값으로 /login 에서 동작한다. (원하는 대로 로그인 api를 변경할 수 있다.)

 

<진행 순서>

  1. 클라이언트로부터 username, password를 받는다.
  2. 파싱 후 LoginReqDto로 반환하고 강제로 로그인해 인증용 토큰을 생성한다.
  3. UserDetailsService의 loadUserByUsername을 호출한다.
  4. 호출 후 로그인이 성공하면 LoginUser 객체 생성 후 시큐리티 세션(SecurityContext) 에 담는다. 로그인이 실패하면 실패 에러를 날린다.
    (우리가 JWT를 쓴다 하더라도, 컨트롤러 진입을 하면 시큐리티의 권한체크, 인증체크 도움을 받을 수 있으므로 임시 세션을 만든다.)
  5. JWT 토큰을 생성하고 response 헤더에 JWT 토큰을 담아 브라우저에게 응답한다.

브라우저는 JWT 토큰 만료 시간 전까지 계속 헤더에 JWT 토큰을 담고 있고, 권한이 필요한 페이지에서 JwtAuthorizationFilter 를 호출해 사용한다.

 

JwtAuthorizationFilter

=> 인가를 수행하는 필터이다. 

- 권한이 필요한 모든 주소에서 동작한다.

 

<진행 순서>

  1. 헤더에 든 JWT 토큰을 가져와 검증한다.
  2. 검증이 성공하면 Authentication(LoginUser) 객체를 생성 하고 강제 로그인을 진행해 스프링 시큐리티의 SecutiryContext에 담는다.
  3. 이때 이 객체는 인증이 완료되었는지랑 권한 체크용으로만 사용한다.
  4. 검증이 완료되면 체인을 타고 계속 진행한다.

위 필터들을 SecutiryConfig에 등록해줘야 한다.

 

Jwt 필터에 필요한 토큰 생성 로직과 검증 로직을 구현 해보자

- 토큰 생성하기

인증에 성공하면 LoginUser가 임시 세션에 담기는데, 이 LoginUser를 사용해서 토큰을 생성할 것이다. 

// 토큰 생성
    public static String create(LoginUser loginUser) {
        String jwtToken = JWT.create()
                .withSubject("bank")
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtVO.EXPIRATION_TIME))
                .withClaim("id", loginUser.getUser().getId())
                .withClaim("role", loginUser.getUser().getRole()+"") // 문자열 캐스팅
                .sign(Algorithm.HMAC512(JwtVO.SECRET));
        return JwtVO.TOKEN_PREFIX+jwtToken;
    }

 

설정 내용은 JwtVO 안에 담긴 기본 정보 세팅 파일들을 사용해주었다.

 

- 토큰 검증하기

인가를 진행할 때 토큰을 검증해야한다. 토큰을 받고 리턴되는 LoginUser 객체를 강제로 시큐리티 임시 세션에 직접 주입할 것이다. (강제 로그인)

// 토큰 검증
    public static LoginUser verify(String token) {
        DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(JwtVO.SECRET)).build().verify(token);
        Long id = decodedJWT.getClaim("id").asLong();
        String role = decodedJWT.getClaim("role").asString();
        User user = User.builder().id(id).role(UserEnum.valueOf(role)).build();
        LoginUser loginUser = new LoginUser(user);
        return loginUser;
    }

 


 

이제 Jwt 필터를 SecurityConfig에 적용해주자

만들어둔 Jwt 필터를 그냥 사용할 순 없다. SecurityConfig에 적용해줘야한다.

 

// JWT 필터 등록이 필요함.
    // AbstractHttpConfigurer<본인 클래스명, HttpSecurity>
    public class CustomSecurityFilterManager extends AbstractHttpConfigurer<CustomSecurityFilterManager, HttpSecurity> {
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); // AuthenticationManager 에 접근 가능.
            builder.addFilter(new JwtAuthenticationFilter(authenticationManager));
            builder.addFilter(new JwtAuthorizationFilter(authenticationManager));
            super.configure(builder);
        }

        public HttpSecurity build(){
            return getBuilder();
        }
    }

 

참고로 SecurityFilterChain 클래스에서 인증과 인가 권한 예외를 가로채서 원하는 에러 형태로 예쁘게 만들어줄 수 있다.

백엔드에서 대부분의 에러를 잘 처리해줘야 하기 때문에 잘 기억해두자.

// Exception 가로채기
        // 인증 실패
        http.exceptionHandling(e -> e.authenticationEntryPoint((request, response, authException) -> {
            CustomResponseUtil.fail(response, "로그인을 진행해 주세요.", HttpStatus.UNAUTHORIZED);
        }));

        // 권한 실패
        http.exceptionHandling(e -> e.accessDeniedHandler((request, response, accessDeniedException) -> {
            CustomResponseUtil.fail(response, "권한이 없습니다.", HttpStatus.FORBIDDEN);
        }));

+) 나머지 코드는 bank-junit 레포 글로벌 에러처리 참고하기