✏️ JWT
- JWT 토큰을 발급받기 위해 사용했던 추상 객체로,
Spring Security 라이브러리를 의존하면 사용할 수 있었다. 
- 2022 년 2월 21일에 업데이트된 Spring Security 5.7.0-M2 버전 이후부터 서비스가 종료되었다.
- 즉, 이 객체를 사용하지 않고 최신 방법으로 JWT 토큰을 발급받을 예정이다.
 
- 만약 
W**ebSecurityConfigureAdapter 객체를 사용하고 싶다면 버전을 낮춰야한다.**
- java 11
 
- spring boot 2.7.3
 
 
 
✏️ 환경설정
📍 Dependency
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
📍 Application yml
- JWT 의 암호화 복호화를 위한 Secrit key 를 추가한다.
- HS256 알고리즘을 사용해야 하기 때문에 256 bit 보다 커야한다.
 
- 한단어 당 8bit 이므로 32 글자 이상이 필요하다.
 
 
jwt:
  secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa
✏️ TokenInfo
- 클라이언트에 토큰을 보내기 위한 DTO
grantType
- HTTP header 에 prefix 로 붙여주는 타입으로 
Bearer 를 사용한다. 
 
 
package com.baeker.baeker.base.security.jwt;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@AllArgsConstructor
public class JwtTokenInfo {
    private String grantType;
    private String accessToken;
    private String refreshToken;
}
✏️ JwtTokenProbider
- Access Token 과 Refresh Token 을 생성하는 Class
 
86480000
- Date 생성자에 입력하는 수치로 토큰의 유효기간을 뜻한다.
 
- 86480000 는 24시간을 뜻한다.
 
- 보통 토큰은 30분 정도로 생성하는데 원활한 test 를 위해 24시간으로 세팅했다.
 
 
package com.baeker.baeker.base.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
@Slf4j
@Component
public class JwtTokenProvider {
    private final Key key;
    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
        byte[] keyByes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyByes);
    }
    
    
    public JwtTokenInfo generateToken(Authentication authentication) {
        
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
        long now = (new Date()).getTime();
        
        Date accessTokenExpiresIn = new Date(now + 86400000);
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("auth", authorities)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + 86400000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        return JwtTokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }
    
    public Authentication getAuthentication(String accessToken) {
        
        Claims claims = parseClaims(accessToken);
        if (claims.get("auth") == null)
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get("auth").toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims String is empty", e);
        }
        return false;
    }
    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(accessToken)
                    .getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}
✏️ JwtAuthenticationFilter
- 클라이언트 요청시 JWT 를 인증하는 커스텀 필터
 
UsernamePasswordAuthenticationFilter 이전에 실행된다.
JwtAuthenticationFilter 를 통과 하면 UsernamePasswordAuthenticationFilter 이후 필터는 통과한것으로 본다는 의미이다. 
- 즉, Username + PW 를 통과한 인증을 JWT 를 통해 수행한다는 의미
 
 
package com.baeker.baeker.base.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final JwtTokenProvider jwtTokenProvider;
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        
        String token = resolveToken((HttpServletRequest) request);
        
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer"))
            return bearerToken.substring(7);
        return null;
    }
}