티스토리 뷰

 

Spring Security의 기본 동작은 표준 웹 애플리케이션을 사용하기에 쉽다. 쿠기 기반 인증 및 세션을 사용한다. 또한 CSRF 토큰을 자동으로 처리한다.

반면에 외부 서비스 또는 SPA/모바일 응용 프로그램과 함께 사용되는 REST API만 빌드하는 경우에는 전체 세션이 필요하지 않을 수 있다. 여기에 디지털 서명 토큰인 JWT(JSON Web Token)가 있다. 필요한 모든 정보를 토큰에 저장할 수 있으므로 서버가 세션이 없을 수 있다. 서버가 사용자에게 권한을 부여할 수 있도록 모든 HTTP 요청에 JWT를 첨부해야 한다. 토큰을 보내는 방법에 대한 몇 가지 옵션이 있다. 예를 들어 URL 매개 변수로 또는 Bearer 스키마를 사용하는 HTTP 인증 헤더를 사용한다.

Authorization: Bearer <token string>

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

JWT는 마침표 (.)를 구분자로 세 부분으로 나누어져 있으며 JOSE 헤더, JWT Claim Set, Signature라고 한다.

Header - 일반적으로 토큰 유형 및 해싱 알고리즘을 포함한다.
Payload - 일반적으로 사용자 및 토큰 발급 대상에 대한 데이터를 포함한다.
Signature - 메시지가 변경되지 않았는지 확인하는 데 사용된다.

토큰 (Base64로 인코딩된 JSON 객체)

Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJzZWN1cmUtYXBpIiwiYXVkIjoic2VjdXJlLWFwcCIsInN1YiI6InVzZXIiLCJleHAiOjE1NDgyNDI1ODksInJvbCI6WyJST0xFX1VTRVIiXX0.GzUPUWStRofrWI9Ctfv2h-XofGZwcOog9swtuqg1vSkA8kDWLcY3InVgmct7rq4ZU3lxI6CGupNgSazypHoFOA

 

public final class SecurityConstants {
    // JWT token defaults
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String TOKEN_TYPE = "JWT";
    private SecurityConstants() {
        throw new IllegalStateException("Cannot create instance of static util class");
    }
}
@Service
public class JwtService implements IJwtService {

    private static final Logger logger = LoggerFactory.getLogger(JwtService.class);
    
    @Autowired
    HttpServletRequest request;
    
    @Value("${jwt.salt}")
    private String salt;
    
    @Override
    public <T> String createJWT(String key, T data, String subject) throws UnsupportedEncodingException {
        String jwt = Jwts.builder()
                .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
                .setHeaderParam("regDt", System.currentTimeMillis())
                .setSubject(subject)
                .claim(key, data)
                .setExpiration(new Date(System.currentTimeMillis() + 864000000))
                .signWith(SignatureAlgorithm.HS512, generateKey())
                .compact();
        return jwt;
    }
    
    @SuppressWarnings("unused")
    @Override
    public boolean isUsable(String jwt) {
        try {
            Jws<Claims> claims = Jwts.parser()
                                .setSigningKey(generateKey())
                                .parseClaimsJws(jwt);
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token: {}", e.getMessage());
            return null;
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token: {}", e.getMessage());
            return null;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
            return null;
        } catch (SignatureException e) {
            logger.error("Invalid JWT signature: {}", e.getMessage());
            return null;
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
            return null;
        } catch (UnsupportedEncodingException e) {
            logger.error("UnsupportedEncodingException: {}", e.getMessage());
            return null;
        }
        return true;
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public Map<String, Object> get(String key, String jwt) {
        Jws<Claims> claims = null;
        try {
            claims = Jwts.parser()
                         .setSigningKey(salt.getBytes("UTF-8"))
                         .parseClaimsJws(jwt);
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token: {}", e.getMessage());
            return null;
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token: {}", e.getMessage());
            return null;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
            return null;
        } catch (SignatureException e) {
            logger.error("Invalid JWT signature: {}", e.getMessage());
            return null;
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
            return null;
        } catch (UnsupportedEncodingException e) {
            logger.error("UnsupportedEncodingException: {}", e.getMessage());
            return null;
        }
        Map<String, Object> map = (Map<String, Object>) claims.getBody().get(key);
        return map;
    }
    
    /**
     * 암호화 키 생성
     * @return
     * @throws UnsupportedEncodingException
     */
    private byte[] generateKey() throws UnsupportedEncodingException {
        byte[] key = null;
        try {
            key = salt.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            logger.error("UnsupportedEncodingException: {}", e.getMessage());
            throw new UnsupportedEncodingException("UnsupportedEncodingException: " + e.getMessage());
        }
        return key;
    }
}

1) ExpiredJwtException : JWT를 생성할 때 지정한 유효기간 초과했을 경우
2) UnsupportedJwtException : 예상하는 형식과 일치하지 않는 특정 형식이나 구성의 JWT일 경우
3) MalformedJwtException : JWT가 올바르게 구성되지 않았을 경우
4) SignatureException :  JWT의 기존 서명을 확인하지 못했을 경우
5) IllegalArgumentException : JWT claims이 비어있는 경우

// JWT 생성 key, 데이터, subject은 상황에 맞게 정의 하여 생성
String jwtToken = jwtService.createJWT("user", jwtDto, "userInfo");
// JWT 유효토큰 확인 (true, false 반환)
String jwtToken = request.getParameter(SecurityConstants.TOKEN_HEADER);
jwtService.isUsable(jwtToken);
// JWT 토큰정보 확인
String jwtToken = request.getParameter(SecurityConstants.TOKEN_HEADER);
Map<String, Object> jwtMap = jwtService.get("user", jwtToken);

 

JWT 주의점

JWT Claim Set은 암호화를 하지 않는다. 서명 없이도 누구나 열어볼 수 있기 때문에 여기에는 보안이 중요한 데이터를 넣으면 안 된다. base64로 인코딩해서 사용하다 보면 이 부분을 간과하기 쉬운데 필요한 최소한의 정보만 JWT Claim Set에 담아야 한다.

인코딩 특성상 JWT Claim Set의 내용이 많아지면 토큰의 길이도 길어진다. 불필요한 정보까지 많은 정보를 담으면 안 된다. 토큰을 강제로 만료시킬 방법이 없다. 서버가 토큰의 상태를 가지고 있지 않고 토큰 발급 시 해당 토큰이 유효한 조건이 결정되므로 클라이언트가 로그아웃해도 토큰 자체가 만료되는 것은 아니다. 이때 토큰을 탈취한다면 해당 토큰을 만료시간까지는 유효하게 된다

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함