JWT - Json Web Token
JSON으로 표현된 정보를 안전하게 주고 받기 위한 Token의 일종이다.
(여기서의 토큰은 신분증에 가깝다. 이 토큰에 사용자는 누구이며, 발급일 만료일 나오고, 언제 로그인했는지까지 나오게 됩니다. )
JWT 내부에 서비스를 사용하기 위한 인증정보가 담기며, JWT를 받은 서비스는 JWT가 위변조가 되었는지 알아차릴 수 있기 때문에 Token 기반의 인증 시스템에서 많이 활용됩니다.
JWT 특징
- 사용자 확인을 위한 인증 정보
- 위변조 확인이 용이 → 위변조가 어려움
- 토큰 기반 인증 시스템에서 많이 활용
기본적으로 JWT는 세부분으로 나뉘어져 만들어집니다.
header.payload.signature
header.payload.signature
- header : JWT의 부수적 정보 (어떤 방식으로 암호화 되었는지)
- payload : JWT로 전달하고자 하는 정보가 담긴 부분(여기에는 sub(subject)사용자, iat(issued at) 발급 일자, exp(expires) 만료일자 )등의 정보가 담기며, 일반적으로 JWT를 사용할 때 사용자가 누구인지 알아보며 서비스를 활용하기에 충분한 데이터를 담습니다.
- signature : JWT의 위변조 판단을 위한 부분, header와 payload의 길이, 그리고 사전에 공유된 암호키를 기반으로 signature를 계산해 JWT의 위변조를 감지합니다.
JWT는 해석은 간단하지만 암호키가 없다면 변조가 불가능한 구조로 만들어지기 때문에, 상태가 저장되지 않는, 즉, 세션을 만들지 않는 상태로 사용자를 인증하기 위해 많이 활용됩니다.
이런식으로 특정 토큰을 사용자가 소유하도록 하고, 모든 요청에 쿠키를 포함하듯 모든 요청에 이 토큰을 포함하여 보내도록 하는 인증을 Token Based Authentication이라고 합니다.
Token Based Authentication
세션을 저장하지 않고 토큰의 소유를 통해 인증 판단
- 상태를 저장하지 않기 때문에 서버의 세션 관리가 불필요함
- 세션 소유가 곧 인증 → 여러 서버에 걸쳐서 인증이 가능함
- 쿠키는 요청을 보낸 클라이언트에 종속되지만, 토큰은 쉽게 첨부가 가능함(주로 Header에)
- 로그인 상태라는 개념이 사라져서 로그아웃이 불가하다. (기본적으로는 )
java 언어를 사용하면서 JWT를 쉽게 사용하게 해주는 JJWT 라이브러리를
build.gradle에 첨부하여 JWT를 발급해봅니다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
JJWT 발급
JWT와 관련된 기능만 따로 클래스에 나눈다고 가정을 한 뒤,
JwtTokenUtils 을 생성한다.
@Slf4j
@Component
public class JwtTokenUtils {}
암호키 준비
JWT를 만들기 위해서는 JWT의 위변조 상태를 감지하기 위한 암호키가 필요한데, 이는 개발단계에서는 복잡하게 생각할 필요 없이 적당한 길이의 문자열을 사용하면 됩니다.
사용하기 위한 임의의 암호키는 application.yaml에 작성하면 됩니다.
jwt:
secret: aaaabbbsdifqbvaesoioegwaaaabbbsdifqbvaesoioegwaaaabbbsdifqbvaes
JwtTokenUtils 클래스를 생성
→ application.yaml에 작성된 설정은 필드나 생성자 인자에 @Value 어노테이션을 통해 할당할 수 있습니다.
이를 이용해 JWT 암호화를 하기 위한 암호키를 나타내는 Key 인터페이스를 필드로 할당합니다.
@Slf4j
@Component
// JWT 관련 기능들을 넣어두기 위한 기능성 클래스
public class JwtTokenUtils {
private final Key signingKey;
public JwtTokenUtils(
@Value("${jwt.secret}")
String jwtSecret
){
this.signingKey
= Keys.hmacShaKeyFor(
Decoders.BASE64.decode(jwtSecret));
}
generateToken()
JwtTokenUtils 에 JWT를 발급하는 메소드를 생성합니다.
// 주어진 사용자 정보를 바탕으로 JWT를 문자열로 생성
public String generateToken(UserDetails userDetails){
// Claims : JWT에 담기는 정보의 단위를 Claim이라 부른다.
// Claims는 Claim들을 담기위한 Map의 상속 인터페이스
Claims jwtClaims = Jwts.claims()
.setSubject(userDetails.getUsername())
.setIssuedAt(Date.from(Instant.now()))
.setExpiration(Date.from(Instant.now().plusSeconds(3600))); //한시간동안 Jwt가 유효하다.
return Jwts.builder()
.setClaims(jwtClaims)
.signWith(signingKey)
.compact();
}
}
Claims : JJWT에 정의된 Map 인터페이스를 상속받는 인터페이스로, JWT에 담길 정보를 의미합니다. Map처럼 데이터를 추가하여 JWT에 포함시킬 데이터를 정의할 수 있습니다.
지금은 사용자를 식별할 수 있는 username과 발급일과 만료일을 나타내는 iat, exp를 설정하고 있습니다.
Jwts.builder() : 실제 JWT를 만드는 빌더이며, .compact()메소드가 실제 Token을 문자열로 만들어 반환해 줍니다.
TokenController 생성
JWT를 발급 받을 엔드포인트를 생성합니다.
@Slf4j
@RestController
@RequestMapping("token") // <http://localhost:8080/token/**> 부터 시작하는 요청들
public class TokenController {
// UserDetailsManager: 사용자 정보 회수
// PasswordEncoder: 비밀번호 대조용 인코더
private final UserDetailsManager manager;
private final PasswordEncoder passwordEncoder;
private final JwtTokenUtils jwtTokenUtils;
public TokenController(
UserDetailsManager manager,
PasswordEncoder passwordEncoder,
JwtTokenUtils jwtTokenUtils
) {
this.manager = manager;
this.passwordEncoder = passwordEncoder;
this.jwtTokenUtils = jwtTokenUtils;
}
JwtTokenUtils : 실제로 JWT를 발급하기 위해 필요한 Bean
UserDetailsManager : 사용자 정보를 확인하기 위한 Bean
PasswordEncoder : 사용자가 JWT 발급을 위해 제출하는 비밀번호가 일치하는지 확인하기 위한 암호화 Bean
등이 필요합니다.
실제 issue 엔드 포인트 생성하기
@PostMapping("/issue")
public JwtTokenDto issueJwt(@RequestBody JwtRequestDto dto) {
UserDetails userDetails
= manager.loadUserByUsername(dto.getUsername());
// passwordEncoder.matches(rawPassword, encodedPassword)
// 평문 비밀번호와 암호화 비밀번호를 비교할 수 있다.
if (!passwordEncoder.matches(dto.getPassword(), userDetails.getPassword()))
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
JwtTokenDto response = new JwtTokenDto();
response.setToken(jwtTokenUtils.generateToken(userDetails));
return response;
}
JwtTokenDto 생성
@Datapublic class JwtTokenDto {
private String token
;}
JwtRequestDto 생성
@Data
public class JwtRequestDto {
private String username;
private String password;
}
WebSecurityConfig
Security 설정을 진행합니다.
우선 JWT를 사용하기 시작하면 본래 만들었던 formLogin은 사용하지 않기 때문에 logout과 함께 제거합니다.
그리고 토큰 기반 인증방식인 만큼 세션을 관리하지 않도록 설정합니다.
추가로 토큰 발급을 위한 /token/issue URL을 인증이 필요없는 상태로 설정합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authHttp -> authHttp
.requestMatchers(
"/no-auth",
"/token/issue"
)
.permitAll()
.requestMatchers(
"/re-auth",
"/users/my-profile"
)
.authenticated()
.requestMatchers(
"/",
"/users/register"
)
.anonymous()
)
.sessionManagement(
sessionManagement -> sessionManagement
.sessionCreationPolicy(
SessionCreationPolicy.STATELESS)
);
return http.build();
}
/token/issue 로 요청 보내기
- 포스트맨에서 http://localhost:8080/token/issue 입력 후 username password를 입력하면 토큰이 랜덤으로 생성되는 걸 확인할 수 있습니다.
{
"username" : "user",
"password" : "asdf"
}
아래와 같은 결과가 출력되면 성공입니다.
{
"token": "eyJhbGciOiJIUzI1NiJ9
.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjg4NjcwODgxLCJleHAiOjE2ODg2NzA4OTF9
.gC-E2jiRC58V5irNzamiG5QlFyl_KOHMRQS4EvwAvC4"
}
사용자 정보를 담고 있는 JWT를 발급하는데에 성공했으므로 JWT를 이용해 인증을 진행합니다.
SecurityFilterChain
스프링 시큐리티는 기본적으로 기능을 Servlet API의 필터를 기반으로 제공합니다.
JWT를 이용한 인증
Token 기반의 인증방식에서는 일반적으로 HTTP Header에 Authroization 이라는 Header를 추가해서 보냅니다.
그 중 JWT를 비롯한 토큰을 활용해서 진행할 경우, Bearer {token 값}의 형태로 추가해 보내게 됩니다. 이를 Bearer Token 인증방식이라고 합니다.
즉. 저희가 받는 요청에 이 헤더의 값이 없다면 인증되지 않은 사용자로, 이 헤더의 값이 있다면 ,해당 Token이 유효한지 확인한 후 사용자의 인증 상태를 판단합니다.
이 Bearer Token 인증방식의 경우 결국 요청을 보내는 시점에 Header에 Authorization값을 추가해주는 행위를 해야하며, 이는 서버에서 조작하는 쿠키-세션 방식과는 달리 프론트엔드쪽에서 직접 해주어야 합니다.
토큰의 보관도 마찬가지로 프론트엔드에서 직접 진행해야 합니다. 대신 서버측에서 세션을 기록할 필요가 없어집니다.
JwtTokenFilter
저희가 들어온 HTTP요청에 Authorization 이라는 헤더가 존재하는지, 존재한다면 거기에 담긴 값이 Bearer로 시작하는지, 그리고 담겨있는 Token이 유효한지를 판단하는 JwtToeknFilter를 만들어야합니다.
'Spring' 카테고리의 다른 글
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (1) (0) | 2023.07.23 |
---|---|
WebSocket으로 채팅 구현하기 (0) | 2023.07.14 |
[Spring] Logging (0) | 2023.07.09 |
[Spring] 테스트 클래스란? (0) | 2023.07.09 |
[Spring] 유효성 검사 (0) | 2023.07.09 |