[SW 직무역량 부트캠프] 8일차

WJ·2023년 7월 5일

2023-07-05 SW 직무역량 부트캠프 8일차 TIL

JWT (JSON Web Token)

  • 유저를 인증하고 식별하기 위한 토큰 기반 인증

  • 세션과 달리 서버가 아닌 클라이언트에 저장됨

  • 인증, 인가에 필요한 모든 정보를 자체적으로 지님

JWT의 구조

  • .을 구분자로 헤더, 내용, 서명 3가지의 암호화된 문자열로 이루어짐

  • 헤더(Header)

    • 토큰의 타입과 해시 암호화 알고리즘에 대한 정보를 가짐
  • 내용(Payload)

    • 토큰에 담을 정보가 들어있음
    • 클레임(Claims) 단위로 저장, 클레임은 키/값 쌍을 뜻함
  • 서명(Signature)

    • 토큰의 유효성 검증 시 사용
    • 헤더와 정보의 암호화 값, 비밀 키 값을 지정한 암호화 알고리즘으로 서명하여 생성

Access Token, Refresh Token

  • 단순 액세스 토큰만으로는 토큰 탈취 시 보안이 취약해지기 때문에, 이를 보완하기 위해 두개의 토큰으로 나눠서 사용

  • Access Token

    • 사용자 권한 인증 및 권한 접근에 사용하는 토큰
    • 2~3시간 정도의 짧은 유효시간
    • DB에 저장되지 않음
  • Refresh Token

    • 액세스 토큰 재발급을 위해 사용하는 토큰
    • 15일 정도의 긴 유효시간을 가짐
    • DB에 저장됨

JwtProperties

@Getter
@Setter
@Component
@ConfigurationProperties("jwt") // 프로퍼티에 경로를 바인딩하여 해당 경로의 값 참조
public class JwtProperties {
    private String issuer;
    private String secret;
}

TokenAuthenticationFilter

@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 요청 헤더의 authorization 키 값 조회
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);

        // 가져온 값에서 접두사 제거
        String token = getAccessToken(authorizationHeader);

        // 가져온 토큰값이 유효한지 확인, 유효하다면 인증 정보를 만든다
        if(tokenProvider.validToken(token))
        {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getAccessToken(String authorizationHeader) {
        if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX))
        {
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }

        return null;
    }
}

TokenProvider // RefreshToken 관련 리포지토리 및 서비스 생성하여 사용

@RequiredArgsConstructor
@Service // 토큰 생성 및 검증 서비스
public class TokenProvider {

    private final JwtProperties jwtProperties;
    private final RefreshTokenService refreshTokenService;

    public Tokeninfo generateToken(User user, Duration accessTokenExpiredAt, Duration refreshTokenExpiredAt) {
        Date now = new Date();
        String accessToken = makeAccessToken(new Date(now.getTime()+accessTokenExpiredAt.toMillis()), user);
        String refreshToken = makeRefreshToken(new Date(now.getTime()+refreshTokenExpiredAt.toMillis()), user);
        refreshTokenService.saveToken(user.getEmail(), refreshToken);

        return Tokeninfo.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    // 다형성 ?
    public String makeAccessToken(Date expiry, User user){
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 : JWT
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(now)
                .setExpiration(expiry)
                .setSubject(user.getEmail())
                .claim("id", user.getId())
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret())
                .compact();
    }

    public String reissueAccessToken(User user, String refreshToken, Duration accessTokenExpiredAt){
        if(validRefreshToken(user, refreshToken))
        {
            Date now = new Date();;
            return makeAccessToken(new Date(now.getTime()+accessTokenExpiredAt.toMillis()), user);
        }
        return null;
    }

    public String makeRefreshToken(Date expiry, User user){
        Date now = new Date();
        String refreshToken = Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 : JWT
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(now)
                .setExpiration(expiry)
                .setSubject(user.getEmail())
                .claim("id", user.getId())
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret())
                .compact();

        // 저장소에 저장

        return refreshToken;
    }

    public boolean validToken(String token) {
        try{
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecret())
                    .parseClaimsJws(token);

            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public boolean validRefreshToken(User user, String refreshToken) {
        if(validToken(refreshToken)) {   
            String savedToken = refreshTokenService.findByEmail(user.getEmail()));
            if(savedToken == refreshToken) {
                return true;
            }
        }
        return false;
    }

    // 토큰 기반으로 인증 정보를 가져오는 작업
    public Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        // Authentication -> principal, credentials, authorities
        // collections 보기
        Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))
        return new UsernamePasswordAuthenticationToken(
                new org.springframework.security.core.userdetails.User(
                        claims.getSubject(), "",authorities
                ), // principal
                token, // credentials
                authorities
        );
    }

    public Long getUserId(String token) {
        Claims claims = getClaims(token);

        return claims.get("id", Long.class);
    }
    private Claims getClaims(String token) {
        return Jwts.parser()
                .setSigningKey(jwtProperties.getSecret())
                .parseClaimsJws(token)
                .getBody();
    }

}
profile
주니어 개발자

0개의 댓글