같은 정보를 담을 필요
는 없습니다."아 우리가 발급해 준 토큰이 맞네!"
라는 판단이 될 경우, 클라이언트의 요청을 처리한 후 응답을 보내준다.만약에 세션 방식이라면 모든 서버가 해당 사용자의 세션 정보를 공유하고 있어야 합니다.
- 안정성
- 암호화 한 토큰을 사용
- 암호화 키를 노출 할 필요 X
- 권한 부여에 용이
- 토큰의 payload(내용물) 안에 어떤 정보에 접근 가능한지 정의
- 클라이언트가 request를 전송할 때 마다 자격 증명 정보를 전송할 필요가 없다.
- JWT의 경우 토큰이 만료되기 전까지는 한번의 인증만 수행하면 된다.
- 인증을 담당하는 시스템을 다른 플랫폼으로 분리하는 것이 용이
- 사용자의 자격 증명 정보를 직접 관리하지 않고, Github, Google 등의 다른 플랫폼의 자격 증명 정보로 인증하는 것이 가능
- 토큰 생성용 서버를 만들거나, 다른 회사에서 토큰 관련 작업을 맡기는 것 등 다양한 활용이 가능
세션의 경우 서버 확장 시, 세션 불일치 문제가 발생할 수 있지만 Sticky Session, Session Clustering, Session 저장소의 외부 분리 등의 작업을 통해 보완을 하고 있습니다.
그리고 토큰의 경우, 기본적으로 토큰 무효화를 할 수 없지만 key/value 쌍의 형태로 저장되는 Redis 같은 인메모리 DB에 무효화 시키고자 하는 토큰의 만료 시간을 짧게 주어 해당 토큰을 사용하지 못하게 하는 등의 방법을 사용해 토큰 무효화 문제를 보완하고 있습니다.
짧은 유효 기간
을 주어 탈취되더라도 오랫동안 사용할 수 없도록 하는 것이 좋습니다.dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
// jjwt fkdlqmfjfl tkdyd
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
public class JwtTokenizer {
// (1) Plain Text 형태인 Secret Key의 byte[]를 Base64 형식의 문자열로 인코딩
// Plain Text 자체를 Secret Key로 사용하는 것을 권장하지 않고 있습니다.
public String encodeBase64SecretKey(String secretKey) {
return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
// 인증된 사용자에게 JWT를 최초로 발급해주기 위한 JWT 생성 메서드
//
public String generateAccessToken(Map<String, Object> claims,
String subject,
Date expiration,
String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); // (2-1)Base64 형식 Secret Key 문자열을 이용해 Key(java.security.Key) 객체를 얻습니다.
return Jwts.builder()
.setClaims(claims) // (2-2)Custom Claims를 추가합니다. Custom Claims에는 주로 인증된 사용자와 관련된 정보를 추가
.setSubject(subject) // (2-3) JWT에 대한 제목을 추가
.setIssuedAt(Calendar.getInstance().getTime()) // (2-4) JWT 발행 일자를 설정
.setExpiration(expiration) // (2-5) JWT의 만료일시를 지정
.signWith(key) // (2-6) 서명을 위한 Key(java.security.Key) 객체를 설정
.compact(); // (2-7) JWT를 생성하고 직렬화
}
// (3) Refresh Token을 생성하는 메서드
public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(expiration)
.signWith(key)
.compact();
}
...
...
// (4) JWT의 서명에 사용할 Secret Key를 생성
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); // (4-1) Base64 형식으로 인코딩 된 Secret Key를 디코딩 한 후, byte array를 반환
Key key = Keys.hmacShaKeyFor(keyBytes); // (4-2) key byte array를 기반으로 적절한 HMAC 알고리즘을 적용한 Key(java.security.Key) 객체를 생성
return key;
}
}
public class JwtTokenizer {
...
...
public void verifySignature(String jws, String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
Jwts.parserBuilder()
.setSigningKey(key) // (1) 서명에 사용된 Secret Key를 설정
.build()
.parseClaimsJws(jws); // (2) JWT를 파싱해서 Claims를 얻습니다.
}
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // (1)동일 출처로부터 들어오는 request만 페이지 렌더링을 허용
.and()
.csrf().disable()
.cors(withDefaults()) // (3) CORS 설정을 추가합니다. .cors(withDefaults()) 일 경우, corsConfigurationSource라는 이름으로 등록된 Bean을 이용
.formLogin().disable() // (4) SSR(Server Side Rendering) 애플리케이션에서 주로 사용하는 폼 로그인 방식
.httpBasic().disable() // (5) HTTP Basic 인증은 request를 전송할 때 마다 Username/Password 정보를 HTTP Header에 실어서 인증을 하는 방식
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll() // (6) 모든 HTTP request 요청에 대해서 접근을 허용
);
return http.build();
}
// (7)
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// (8) CORS를 처리하는 가장 쉬운 방법은 CorsFilter를 사용하는 것인데 CorsConfigurationSource Bean을 제공함으로써 CorsFilter를 적용할 수 있다.
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE")); // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // (8-3) CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성
source.registerCorsConfiguration("\/**", configuration); // (8-4) 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용
return source;
}
}
💡 CORS(Cross-Origin Resource Sharing)
애플리케이션 간에 출처(Origin)가 다를 경우 스크립트 기반의 HTTP 통신(XMLHttpRequest, Fetch API)을 통한 리소스 접근이 제한 되는데, CORS는 출처가 다른 스크립트 기반 HTTP 통신을 하더라도 선택적으로 리소스에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책