[Spring] JWT 적용하기

yeonjoo913·2023년 6월 21일
0

Spring

목록 보기
4/19

지난 게시글에서 spring security에 대해 공부하였는데, 이번에는 JWT를 적용해보겠다.

JWT란?

json web token의 약자이다. 유저를 인증하고 식별하기 위한 token기반 인증이다.
토큰은 서버에 저장되지 않고 클라이언트에 저장되기 때문에 서버 부담이 적어진다.
jwt의 가장 중요한 특징은 토큰 자체에 사용자의 권한정보,서비스에 필요한 정보들을 가지고 있다는 점이다.

일반적으로 jwt를 사용했을 경우 아래와 같은 순서로 진행된다.
1. 클라이언트에서 인증요청
2. 서버에서 토큰을 생성하여 클라이언트로 반환
3. 클라이언트는 추가 정보 요청 api를 호출할때 header에 토큰을 함께 전달
4. 서버는 토큰을 검증하여 데이터를 전달


JWT 사용해보기

client의 요청에 대한 접근권한이 없는 경우, 기본적으로 Username and Password Authentication Mechanism을 사용해 로그인폼으로 보내지게되는데 그 역할을 하는 필터는UsernamePasswordAuthenticationFilter이다.
하지만, 현재는 해당 매커니즘을 사용하지 않을 것이기 때문에 UsernamePasswordAuthenticationFilter 이전에 사용자 정의 필터인 JWTFilter에서 인증 및 권한 처리가 필요하다.

public class JwtFilter extends GenericFilterBean {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";
    private final TokenProvider tokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String token = resolveToken((HttpServletRequest) request);
        if (token != null && tokenProvider.validateToken(token, request)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
  • JWTFilter는 jwt가 유효한 토큰인지 인증하기 위한 Filter이다.
  • spring security filter와 통합하지 않고 사용자가 정의한 필터에서 인증 및 권한 작업을 진행할것이기 때문에 AuthenticationManager를 사용하지 않고 TokenProvider를 통해서 인증 후 SecurityContextHolder를 바로 사용하였다.

이제 JWTFilter를 UsernamePasswordAuthenticationFilter 이전에 등록하는 설정이 필요하다.

public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final TokenProvider tokenProvider;
    @Override
    public void configure(HttpSecurity http) {
        JwtFilter jwtFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

필터를 등록했으면 jwt token을 생성하고, 인증 및 권한 부여 등의 기능을 제공할 Provider를 만들어야한다.

public AuthTokenVO generateTokenDto(Authentication authentication, int userAuth, String type, TokenReq token) {
        //AccessToken 생성
        Date accessTokenExpiresIn =
                userAuth == 1 ? getTokenTime(ACCESS_TOKEN_NAME, true) : getTokenTime(ACCESS_TOKEN_NAME, false);
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, userAuth)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();

        //RefreshToken 생성
        String refreshToken = Jwts.builder()
                .setExpiration(
                        userAuth == 1 ? getTokenTime(REFRESH_TOKEN_NAME, true) : getTokenTime(REFRESH_TOKEN_NAME, false))
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();

        return AuthTokenVO.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .accessTokenExpiresIn(accessTokenExpiresIn.getTime())
                .refreshToken(refreshToken)
                .build();
    }
  • accessToken 과 refreshToken을 생성한다.
  • accessToken의 시간의 짧고, refresh는 길게 가져가는 것이 일반적이다.

인증 기능을 수행할 Provider를 만들었으면 JWTTokenProvider가 제공한 사용자 정보로 UserDetais를 제공할 UserDetailService를 만들어야한다.

public class CustomUserDetailsService implements UserDetailsService {
    private final SecurityService securityService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        AuthUserVO userInfo = securityService.getApiUser(username);
        if (userInfo != null) {
            return createUserDetails(userInfo);
        }
        throw (new UsernameNotFoundException(username + "없는 사용자입니다."));
    }

    private UserDetails createUserDetails(AuthUserVO member) {
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("user");
        return new User(
                String.valueOf(member.getUserName()),
                member.getPassword(),
                Collections.singleton(grantedAuthority)
        );
    }
}

추가로 필요한 부분이나 커스텀해야하는 부분들이 많지만 기본적인 사용은 위와 같은 방식으로 적용해보면 될 것 같다.


JWT Token을 사용하면서 생긴 궁금증이 몇가지 있어 정리해보았다.

  1. accessToken은 길게 가져가면 안되나?
    = 안된다. 길게 가져갈 경우 토큰 탈취의 위험이 있다.
  2. accessToken을 서버에 저장해서 사용해도 되나?
    = 저장할 수 있으나 좋지 않은 방법이다. JWT의 무상태성에 위배된다
  • 무상태성이란? 서버가 클라이언트의 상태를 보존하지 않음을 의미한다. 즉, 서버는 클라이언트를 기억하지 않으며 오직 클라이언트의 요청에 대한 응답만을 준다는 것이다.
  1. 여러개의 accesstoken들로 하나의 refreshtoken을 공유해도 되나?
    = accesstoken/refreshtoken은 다른 세션/장치 간에 공유되어서는 안된다.
    이 둘은 각자 자신의 쌍을 가져야한다. 작업을 사용할 때 refreshtoken 해당 요청과 함께 사용된 accesstoken만 만료되어야하며 관련없는 토큰은 만료되지 않아야한다.
profile
주니어 백엔드 개발자. 까먹는다 기록하자!

0개의 댓글