카테고리 없음

[Spring Security] JWT로그인 구현 - 2. JWT 필터, TokenProvider

빠작 2023. 8. 13. 12:32

JWT 로그인을 구현하면서 이해한 내용을 정리해보았다.

인프런에서 제공하는 아래 무료 강의를 보고 작성한 코드이다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0#

 

[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의

스프링부트 시큐리티에 대한 개념이 잡힙니다., 스프링부트 시큐리티,제가 공부했던 걸 쉽게 설명드릴게요 🙂 스프링부트 시큐리티 github https://github.com/codingspecialist/-Springboot-Security-OAuth2.0-V3 htt

www.inflearn.com

 

나는 프론트엔드 부분을 추가했기 때문에 JWT 필터 부분은 강의와 같지만 컨트롤러가 좀 다르다.

그리고 회원 엔티티도 강의와 다르게 만들었기 때문에 UserDetailsService 쪽도 차이가 있을 것이다.

이 글은 그냥 JWT 구현 방식의 구조에 대해 이해하기 위해서 참고만 하면 좋을 것 같다. 그래도 아무것도 모르고 코드를 따라만 치던 내가 이걸 이해한 끝에 작성한 글이니 읽어보면 도움은 될 것이라고 믿는다. 

 

1편

https://bbazackcoding.tistory.com/10

 

[Spring Security] JWT로그인 구현 - 1. 필터 구조, CustomUserDetailsService

JWT 로그인을 구현하면서 이해한 내용을 정리해보았다. 인프런에서 제공하는 아래 무료 강의를 보고 작성한 코드이다. https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB

bbazackcoding.tistory.com


코드 설명 전 전체적인 구조를 파악하고 가는 것이 좋을 것 같아 정리해보았다.

 

1. 필터 구조

가장 핵심이 되는 시큐리티 필터 구조이다.

Spring Security 필터 중 Authentication과 관련된 필터는 일부이고, 그 중 가장 먼저 등장하는 것이 UsernamePasswordAuthentication filter이다. 그 앞에 JWT 필터를 끼워넣었다. 

Security filterchain 구조. 이 중 건드린 것은 JWT filter와 UsernamePasswordAuthentication filter 뿐이기에 두 개만 크게 표시해두었다.

 


2. JWT 필터

이 경우는 1편과 달리 구현할 것이 많다. Filter는 물론, AuthenticationProvider와 AccessDeniedHandler, Entrypoint까지 직접 구현해야 한다.

무려 5개

1편에서는 AuthenticationProvider을 직접 생성하지 않았는데 왜 JWT 필터에서는 직접 구현해야 할까?

그 이유는 1편에서 사용한 UsernamePassword~~ 필터, 매니저, 프로바이더는 모두 스프링에서 제공되는 Authentication 관련 요소들이기 때문이다. 그래서 필요한 부분만 오버라이드 해서 고쳐주면 됐다.

 

하지만 JWT 필터는 제공되는 필터가 아니기에 filter와 provider을 직접 생성해야 한다.

JWT 필터의 흐름

 


2. 구현 : JWT 필터 만들기

아래는 JWT 필터 코드 전문이다.

HttpRequest의 헤더에 실린 jwt 토큰이 곧 Authentication 객체이다.

AuthenticationManager와 AuthenticationProvider는 없지만 대신 TokenProvider가 인증 처리를 해준다.

TokenProvider에서 true를 리턴하면, 즉 인증을 해준다면 JWT 필터에서 이제서야 Authentication 객체를 만들고 이를 SecurityContextHolder에 저장한다. 

public class JwtFilter extends GenericFilterBean {
    private static final Logger logger= LoggerFactory.getLogger(JwtFilter.class);
    public static final String AUTHORIZATION_HEADER="Authorization";
    private TokenProvider tokenProvider;
    public JwtFilter(TokenProvider tokenProvider){
        this.tokenProvider=tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest httpServletRequest=(HttpServletRequest) servletRequest;
        String jwt=ResolveToken(httpServletRequest);
        String requestURI=httpServletRequest.getRequestURI();
        logger.debug("JwtFilter-doFilter");
        System.out.println("JwtFilter-doFilter");
        //유효한 토큰이면 권한을 줌
        if(StringUtils.hasText(jwt)&& tokenProvider.validateToken(jwt)){
            //jwt 토큰에 기록되어 있던 사용자의 권한 정보를 가져옴
            Authentication authentication= tokenProvider.getAuthentication(jwt);
            //security context에 이 사용자의 정보를 저장함
            SecurityContextHolder.getContext().setAuthentication(authentication);
            logger.debug("JwtFilter-doFilter : SecurityContext에 '{}' 인증 정보를 저장했음. url: {}",authentication.getName(),requestURI);
            System.out.println("JwtFilter-doFilter : SecurityContext에 인증 정보를 저장했음. url:"+authentication.getName()+requestURI);
            System.out.println("이 사용자의 정보:"+authentication.getName()+authentication.getAuthorities());
        }
        else{
            logger.debug("JwtFilter-doFilter : 유효한 Jwt 토큰이 없음. url: {}",requestURI);
            System.out.println("JwtFilter-doFilter : no JWT token. url: {}"+requestURI);
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    private String ResolveToken(HttpServletRequest httpServletRequest) {
        logger.debug("JwtFilter-ResolveToken");
        System.out.println("JWTFilter-resolvetoken");
        String bearerToken= httpServletRequest.getHeader(AUTHORIZATION_HEADER);
        System.out.println(bearerToken);
        //jwt 토큰은 bearer로 시작함
        if(StringUtils.hasText(bearerToken)&&bearerToken.startsWith("Bearer ")){
            logger.debug("JwtFilter-ResolveToken:succeed");
            System.out.println("JWTFilter-ResolveToken:succeed");
            //bearer 뒤부터 유의미한 정보이기 때문에
            return bearerToken.substring(7);
        }
        System.out.println("JWTFilter-Resolvetoken:fail");
        return null;
    }
}

 


3. 구현 : TokenProvider 만들기

@Component
public class TokenProvider implements InitializingBean {

    //로그를 찍을 수 있도록 해줌.
    private final Logger logger= LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY="auth";
    private final String secret;
    private final long tokenValidityInMilliseconds;
    private Key key;

    public TokenProvider(@Value("${jwt.secret}")String secret, @Value("${jwt.token-validity-in-seconds}")long tokenValidityInMilliseconds) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
    }


    //다른 속성들이 다 설정되고 객체가 생성된 후 실행됨
    @Override
    public void afterPropertiesSet(){
        logger.debug("TokenProvider-afterPropertiesSet");
        System.out.println("TokenProvider-afterPropertiesSet");
        byte[] KeyBytes= Base64.getDecoder().decode(secret);
        this.key=Keys.hmacShaKeyFor(KeyBytes);
    }

    //
    public String createToken(Authentication authentication){
      logger.debug("TokenProvider-createToken");
      System.out.println("TokenProvider-createToken");
      String authorities=authentication.getAuthorities().stream()
              .map(GrantedAuthority::getAuthority)//GrantedAuthority 클래스 내의 getAuthority를 호출하여 이를 스트링 타입으로 변환
              .collect(Collectors.joining(",")); //얻은 스트링들을 ,로 연결한 후 반환
      long now=(new Date()).getTime();
      Date validity=new Date(now+this.tokenValidityInMilliseconds);

      //jwt의 페이로드에 담기는 내용
      //claim: 사용자 권한 정보와 데이터를 일컫는 말
      return Jwts.builder()
              .setSubject(authentication.getName()) //토큰 제목
              .claim(AUTHORITIES_KEY,authorities) //토큰에 담길 내용
              .signWith(key, SignatureAlgorithm.HS512)
              .setExpiration(validity)
              .compact();
    }

    //유효한 jwt 토큰을 가지고 있는 자에게 권한을 줌.
    public Authentication getAuthentication(String token){
        logger.debug("TokenProvider-getAuthentication");
        System.out.println("TokenProvider-getAuthentication");
        Claims claims=Jwts
                .parserBuilder() //받은 token을 파싱할 수 있는 객체(JwtParserBuilder)를 리턴
                .setSigningKey(key) //ParserBuilder의 key 설정
                .build() //ParserBuilder을 통해 Parser 리턴.
                .parseClaimsJws(token) //토큰을 파싱하여
                .getBody(); //body를 리턴함

        Collection<? extends GrantedAuthority> authorities= Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        Member principal=new Member(claims.getSubject(),"", authorities);
        System.out.println(claims.getSubject());
        System.out.println("auth : "+authorities);
        //일반적인 세션 기반의 id/pw 인증방식이었다면 token에 pw가 담겼을 것...?
        //jwt 로그인에서는 발급받은 jwt 토큰인 token을 기반으로 인증하기 때문에 credential에 token이 담김
        //얘는 인증받은 토큰을 리턴함.
        return new UsernamePasswordAuthenticationToken(principal,token,authorities);
    }

    public boolean validateToken(String token){
        logger.debug("TokenProvider-validateToken");
        System.out.println("TokenProvider-validateToken");
        try{
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch(io.jsonwebtoken.security.SecurityException|MalformedJwtException e){
            logger.info("잘못된 JWT 서명입니다.");
            System.out.println("1 JWT");
        } catch(ExpiredJwtException e){
            logger.info("2 JWT");
            System.out.println("만료된 JWT 토큰입니다");
        }catch(UnsupportedJwtException e){
            logger.info("지원되지 않는 JWT 토큰입니다");
            System.out.println("3 JWT");
        }catch(IllegalArgumentException e){
            logger.info("JWT 토큰이 잘못되었습니다");
            System.out.println("4 JWT");
        }
        return false;
    }
}

 


4. 구현 : SecurityFilterChain에 필터 추가하기 - JWTSecurityConfig

 

이렇게 JWTSecurityConfig를 작성해주고

public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private TokenProvider tokenProvider;
    public JwtSecurityConfig(TokenProvider tokenProvider){
        this.tokenProvider=tokenProvider;
    }

    @Override
    public void configure(HttpSecurity httpSecurity){
        JwtFilter customFilter=new JwtFilter(tokenProvider);
        httpSecurity.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

이런 방식으로 전체 SecurityConfig에서 JwtSecurityConfig를 적용해주면 된다.