번개장터V2-(2) JWT 토큰


1.SecurityContextPersistentFilter
2.LogoutFilter
3.UsernamePasswordAuthenticationFilter
4.DefaultLoginPageGenerationFilter
5.BasicAuthenticationFilter
6.RememberMeAuthenticationFilter
7.AnonymousAuthenticationFilter
8.SessionManagementFilter
9.ExceptionTranslationFilter
10.FilterSecurityInterceptor

3번 UsernamePasswordAuthenticationFilter 전에 jwt 관련 필터를 집어넣어 토큰를 파싱하여 user를 SecurityContextHolder에 집어넣거나 예외처리를 해줄것이다.

토큰 생성

로그인에 성공하면 토큰을 제공해준다.

  • JwtTokenProvider
    토큰을 생성하는 메소드이다. 토큰에는 user의 id,email를 claim에 넣어서 나중에 만들 API에 사용할것이다.
// JWT 토큰 생성
    public String createToken(Long userIdx,String email) {
        Date now = new Date();
        return Jwts.builder()
                .claim("userIDx", userIdx)
                .claim("email", email)// 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + tokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과 signature 에 들어갈 secret값 세팅
                .compact();
    }

토큰에서 정보 추출

아래 permitAll() 해준 Api를 제외 하고는 토큰이 필요하다.
회원가입이나 로그인 같은 경우 토큰이 있을수 없기 때문에 열어줘야하고 마이페이지 같은 경우는 토큰이 있어야만 접근이 가능할것이다. 그런 설정은 아래 WebSecurityConfigurerAdapter 에서 설정을 바꿔주면 된다.

  • WebSecurityConfig (extends WebSecurityConfigurerAdapter)
@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.httpBasic().disable()
                .csrf().ignoringAntMatchers("/h2-console/**").disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers("/user/**").permitAll()
                .antMatchers("/category/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class);
    }

Spring Security는 위에 그림처럼 필터 체인을 거쳐서 로그인이 진행된다.Jwt를 이용한 로그인을 처리하기 위해서 UsernamePasswordAuthenticationFilter 전에 jwt 처리 관련 필터를 삽입해준다.

  • JwtAuthenticationFilter(extends GenericFilter)
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 헤더에서 JWT 를 받아옵니다.
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest);

        // 유효한 토큰인지 확인합니다.
        if (token != null && jwtTokenProvider.validateToken(servletRequest,token)) {
            try {
                // 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
                Authentication authentication = jwtTokenProvider.getAuthentication(token);
                // SecurityContext 에 Authentication 객체를 저장합니다.
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (UsernameNotFoundException e) {
                servletRequest.setAttribute("exception","UsernameNotFoundException");
            }
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

이제 Header에 토큰을 넣고 APi를 호출했을때 이 토큰에서 정보를 추출하거나 이 토큰을 validation 하는 메소드가 필요하다.

위와 같이 Header에 X-AUTH-TOKEN을 넣고 APi를 호출했을때
Header에서 토큰을 추출하는 메소드이다. 만약 토큰이 없거나 비었다면 예외처리를 해준다.

  • JwtTokenProvider

위와 같이 Header에 X-AUTH-TOKEN을 넣고 APi를 호출했을때
Header에서 토큰을 추출하는 메소드이다. 만약 토큰이 없거나 비었다면 예외처리를 해준다.

    public String resolveToken(HttpServletRequest request) {
        String token = request.getHeader("X-AUTH-TOKEN");
        if (token == null || token.isEmpty()) {
            request.setAttribute("exception","NotFoundToken");
        }
        return token;
    }

토큰을 validation 해줘야한다. 만약 아래와 같이 토큰이 유효하지 않다면 필터에서 예외처리를 해주어야 한다.

(1) 토큰 없음

(2) 시그니처 불일치

(3) 토큰 만료

(4) 토큰 인증 후 권한 거부

public boolean validateToken(ServletRequest request, String jwtToken) {

        try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return true;
        } catch (MalformedJwtException e) {
            request.setAttribute("exception","MalformedJwtException");
        } catch (ExpiredJwtException e) {
            request.setAttribute("exception","ExpiredJwtException");
        } catch (UnsupportedJwtException e) {
            request.setAttribute("exception","UnsupportedJwtException");
        } catch (IllegalArgumentException e) {
            request.setAttribute("exception","IllegalArgumentException");
        } catch (SignatureException e) {
            request.setAttribute("exception", "SignatureException");
        }
        return false;
    }

토큰을 파싱하여 저장했던 email로 UserDetailsService에서 찾아 user를 찾아 SecurityContextHolder 에 넣어준다.

public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }
    
public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("email", String.class);
    }
  • CustomUserDetailService (implements UserDetailsService)
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("NOT FOUND USER"));
    }
}

좋은 웹페이지 즐겨찾기