SpringBoot 소셜 로그인 구현하기 (with. JWT Filter 구현 및 적용)

신준호·2023년 11월 2일
1
post-thumbnail

이번 포스팅에서는 SpringSecurity에 filter을 커스텀한 JWT 필터 적용한 과정을 정리해보자!

JwtAuthenticationFilter

JWT 토큰으로 인증하고 SecurityContextHolder에 추가하는 필터를 설정하는 클래스이다.

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        log.info("인증필터 진입 doFilterInternal");

        // 헤더에서 JWT 받기
        String token = jwtTokenProvider.extractAccessToken(request).orElse(null);
        // 유효한 토큰인지 확인
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효하면 토큰으로부터 유저 정보를 받기
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            // SecurityContext 에 Authentication 객체를 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

}

GenreicFilterBean vs OncePerRequestFilter

GenreicFilterBean

기존 필터에서 가져올 수 없는 스프링의 설정 정보를 가져올 수 있게 확장된 추상 클래스다.

처음에는 GenreicFilterBean으로 상속받아 사용했었다. RequestDispatcher에 의해 다른 서블릿으로 디스패치되면서 필터가 두 번 실행되는 현상이 발생했다. 이 같은 문제를 해결하기 위해 OncePerRequestFilter가 있다

OncePerRequestFilter

GenreicFilterBean을 상속받고 있지만 매 요청마다 한 번만 실행된다.

요청 당 한번의 실행을 보장하기 때문에 인증이나 인가를 한번만 거치고 다음 로직을 진행할 수 있다.

doFilterInternal()

OncePerRequestFilter에서 구현하는 메서드이다

doFilter()는 다음 filterChain을 실행하는 것이며, 마지막 filter-chain인 경우 Dispatcher Servlet이 실행된다.

JwtTokenProvider

JWT 토큰과 관련된 메서드 모음 클래스

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
    private final UserDetailsService userDetailsService;

    @Value("${jwt.secretKey}")
    private String secretKey;

    @Value("${jwt.access.expiration}")
    private Long accessTokenValidTime;

    @Value("${jwt.refresh.expiration}")
    private Long refreshTokenValidTime;

    private final MemberRepository memberRepository;
    private final RedisTemplate<String,String> redisTemplate;
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public String createAccessToken(String userPk) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    public String createRefreshToken(String id) {

        Date now = new Date();
        return Jwts.builder()
                .setId(id) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }

    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
    }

    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    public String getUserId(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getId();
    }

    // Request의 Header에서 accesstoken 값을 가져옴. "Authorization" : "ACCESSTOKEN값'
    public Optional<String> extractAccessToken(HttpServletRequest request) {
        return Optional.ofNullable(request.getHeader("Authorization"));

    }

    // Request의 Header에서 refreshtoken 값을 가져옴. "Authorization-Refresh" : "REFRESHTOKEN값'
    public Optional<String> extractRefreshToken(HttpServletRequest request) {
        return Optional.ofNullable(request.getHeader("Authorization-Refresh"));

    }

    public void storeRefreshToken(String id, String refreshToken) {
        Member member = memberRepository.findById(id).orElse(null);
        if (member != null) {
            redisTemplate.opsForValue().set(
                    id,
                    refreshToken,
                    refreshTokenValidTime,
                    TimeUnit.MILLISECONDS

            );
        }
    }

    public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (SignatureException e) {
            log.warn("JWT 서명이 유효하지 않습니다.");
            throw new SignatureException("잘못된 JWT 시그니쳐");
        } catch (MalformedJwtException e) {
            log.warn("유효하지 않은 JWT 토큰입니다.");
            throw new MalformedJwtException("유효하지 않은 JWT 토큰");
        } catch (ExpiredJwtException e) {
            log.warn("만료된 JWT 토큰입니다.");
            throw new ExpiredJwtException(null,null,"토큰 기간 만료");
        } catch (UnsupportedJwtException e) {
            log.warn("지원되지 않는 JWT 토큰입니다.");
            throw new UnsupportedJwtException("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.warn("JWT claims string is empty.");
        } catch (NullPointerException e){
            log.warn("JWT RefreshToken is empty");
        } catch (Exception e) {
            log.warn("잘못된 토큰입니다.");
        }
        return false;

    }

createAccessToken

엑세스 토큰을 만드는 메서드

public String createAccessToken(String userPk) {
        Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣음
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + accessTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }
  • JWT payload 에 저장되는 정보단위, 보통 여기서 user를 식별하는 값을 넣는다
  • JWT에 정보, 토큰 발행 시간 정보, 만료 시간, 사용할 암호화 알고리즘과 signature 에 들어갈 secret값을 세팅한다

createRefreshToken

리프레시 토큰을 만드는 메서드

public String createRefreshToken(String id) {

        Date now = new Date();
        return Jwts.builder()
                .setId(id) // 정보 저장
                .setIssuedAt(now) // 토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + refreshTokenValidTime)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey)  // 사용할 암호화 알고리즘과
                // signature 에 들어갈 secret값 세팅
                .compact();
    }
  • JWT에 정보, 토큰 발행 시간 정보, 만료 시간, 사용할 암호화 알고리즘과 signature 에 들어갈 secret값을 세팅한다

getUserPk

JWT 토큰에서 userpk를 가져오는 메서드

public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }
  • 엑세스 토큰으로 Jwt 토큰에 세팅한 값이 일치한다면 userpk를 가져온다.

getAuthentication

사용자 인증하는 메서드

public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
    }
  • 사용자의 userpk를 추출한 후 UserDetails 객체를 반환한다.
  • UserDetails : SpringSecurity에서 제공하는 사용자의 세부 정부를 나타낸다.
  • UserDetails 객체, 토큰, UserDetails에서 추출한 권한 정보를 기반으로 UsernamePasswordAuthenticationToken 객체를 반환한다.
  • UsernamePasswordAuthenticationToken: Authentication 인터페이스의 구현체로, 주로 사용자 이름과 패스워드 기반으로한 인증에서 사용된다. 여기서는 토큰을 인증의 'credential'로 사용했다.

extractAccessToken

엑세스 토큰을 헤더에서 꺼내오는 메서드

public Optional<String> extractAccessToken(HttpServletRequest request) {
        return Optional.ofNullable(request.getHeader("Authorization"));

    }
  • Request의 Header에서 엑세스 토큰을 가져온다.

validateToken

토큰이 유용한지 검사하는 메서드

public boolean validateToken(String jwtToken) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (SignatureException e) {
            log.warn("JWT 서명이 유효하지 않습니다.");
            throw new SignatureException("잘못된 JWT 시그니쳐");
        } catch (MalformedJwtException e) {
            log.warn("유효하지 않은 JWT 토큰입니다.");
            throw new MalformedJwtException("유효하지 않은 JWT 토큰");
        } catch (ExpiredJwtException e) {
            log.warn("만료된 JWT 토큰입니다.");
            throw new ExpiredJwtException(null,null,"토큰 기간 만료");
        } catch (UnsupportedJwtException e) {
            log.warn("지원되지 않는 JWT 토큰입니다.");
            throw new UnsupportedJwtException("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.warn("JWT claims string is empty.");
        } catch (NullPointerException e){
            log.warn("JWT RefreshToken is empty");
        } catch (Exception e) {
            log.warn("잘못된 토큰입니다.");
        }
        return false;

    }
  • try ~ catch 문으로 JWT 토큰의 예외처리를 통해 유효한지 검사한다.

이제 doFilterInternal() 과정에 대해 정리해보자!

과정

  • 헤더에서 JWT 받는다.
    • jwtTokenProvider.extractAccessToken()메서드를 이용한다.
  • 유효한 토큰인지 검사한다.
    • jwtTokenProvider.validateToken()이고 null이 아닌지 검사한다.
  • 유효한 토큰이면 유저 정보를 받고 SecurityContext 에 Authentication 객체를 저장한다.
    • jwtTokenProvider.getAuthentication(token)으로 유저 정보를 받는다.
    • SecurityContextHolder에 받은 유저 정보인 Authentication 객체를 저장한다
profile
개발 공부 일지

0개의 댓글