개요
기존의 서비스에서 Access Token만을 사용하여 로그인 후 api에 대한 인가 처리를 진행해주었다.
하지만 이 방식은 Access Token의 긴 유효시간으로 인해 replay, MITM 공격에 취약하다는 문제점이 있다.
- Access Token이 만약 탈취된다면 공격자는 그 Access Token을 사용해서 정당한 유저를 사칭하여 모든 정보에 액세스할 수 있는 문제가 생긴다.
따라서 만료시간이 긴 Refresh Token과 만료시간이 짧은 Access Token을 두고 Access Token의 짧은 만료시간을 Refresh Token으로 좀 더 길게 유지시킬 수 있고 Access Token의 만료시간이 짧기 때문에 탈취당한다고 하더라도 공격의 강도가 약해질 수 있다.
따라서 기존의 Access Token만 발급하던 로그인 인가 처리를 Refresh Token을 추가하여 보안을 강화해보았다.
본론
기존의 인가 처리 과정
먼저 기존의 api 인가 과정을 살펴보자.
- 로그인 api 요청 후 인증 과정이 성공적으로 종료되면 서버는 Access Token 하나만 발급한다.
- 클라이언트는 Local Storage에 Access Token을 저장해둔다.
- 클라이언트는 인가 처리가 필요한 api에 요청을 보낼 때마다 Access Token을 담아서 전송한다.
- 서버는 Access Token의 유효성을 검사한 후 해당 api에 접근 권한이 있다면 api 요청을 처리하여 응답한다.
- 기존의 서비스는 하나의 토큰을 사용하여 인가처리를 진행한다.
Refresh Token을 추가한 인가 처리 과정
- 로그인 api 요청 후 인증 과정이 성공적으로 종료되면 서버는 Access Token과 Refresh Token 두 개를 발급한다.
- 클라이언트는 Local Storage에 Access Token과 Refresh Token을 저장해둔다.
- 클라이언트는 인가 처리가 필요한 api에 요청을 보낼 때마다 Access Token을 담아서 전송한다.
- 서버는 Access Token의 유효성을 검사한 후 해당 api에 접근 권한이 있다면 api 요청을 처리하여 응답한다.
- 만약 처리 도중 Access Token의 만료시간이 지났다면 서버는 유효성이 인증되지 않았기 때문에 401 error로 응답한다.
- 401 에러를 응답받은 클라이언트는 Access Token의 만료시간이 지났다는 것을 알고 Refresh Token을 사용하여 갱신 요청을 보낸다.
- 서버는 Refresh Token의 유효성을 검사하고 새로운 Refresh Token과 Access Token을 발급하여 응답한다.
- 만약 Refresh Token의 만료 시간이 지났다면 401 에러를 반환하고 클라이언트는 다시 로그인을 해야한다.
Refresh Token을 통해 토큰을 갱신할 때 새로운 Access Token과 함께 Refresh Token도 새로 발급받는데 이는 Refresh Token Rotation 방식이다.
📌 Token DTO 수정
기존에 제공하는 TokenDTO에는 refresh Token이 없기 때문에 수정하여 다음과 같이 만들었다.
// Token Data Transfer Object
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Getter
@Setter
@Builder
public class TokenDTO {
private String refreshToken;
private String accessToken;
private String email;
private String role;
}
- 기존과 달라진 점은 token필드밖에 없던 DTO였지만 refreshToken과 accessToken으로 나누어주었다.
📌 로그인 로직 수정
기존에 Access Token만 발급하던 로직에서 만료시간이 긴 Refresh Token도 같이 발급해주었다.
private TokenDTO makeToken(String email, String role){
return TokenDTO.builder()
.email(email)
.role(role)
.refreshToken(this.jwtTokenProvider.createToken(email, Arrays.asList(role), TokenType.REFRESH))
.accessToken(this.jwtTokenProvider.createToken(email, Arrays.asList(role), TokenType.ACCESS)
).build();
}
public enum TokenType {
REFRESH, ACCESS
}
- enum을 사용하여 어떤 토큰을 발급할 것인지를 매개변수로 받았다.
📌 토큰 발급 로직 수정
기존에는 AccessToken만 발급했기 때문에 따로 Refresh Token을 발급하는 과정을 만들었다.
public String createToken(String userUid, List<String> roles, TokenType tokenType){
...
String token = Jwts.builder()
.setClaims(claims)
// 토큰 생성 시간
.setIssuedAt(now)
// 토큰의 만료 기간을 설정, AccessToken과 RefreshToken의 만료기간을 다르게 지정
.setExpiration(new Date(now.getTime() +
(tokenType.equals(TokenType.ACCESS) ? tokenValidMillisecond : tokenValidMillisecond * 24)))
// 암호화 알고리즘 및 암호화에 사용되는 키 설정
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
📌 Refresh Token으로 토큰을 갱신하는 로직 추가
기존에는 Refresh Token이 없었기 때문에 새롭게 토큰을 갱신하는 로직을 만들어주었다.
// Controller
@GetMapping("/signin")
public ResponseEntity<TokenDTO> refreshToken(
@RequestHeader("Authorization") String refreshToken
){
try {
logger.info(refreshToken);
logger.info("refreshToken으로 토큰 갱신");
return ResponseEntity.status(201).body(this.authService.refreshToken(refreshToken));
}catch (RefreshTokenNotValidException e){
return ResponseEntity.status(400).body(TokenDTO.builder().accessToken(e.getMessage()).build());
}
}
// Service
@Override
public TokenDTO refreshToken(String token) throws RefreshTokenNotValidException{
String email = this.jwtTokenProvider.getEmailByToken(token);
if(this.jwtTokenProvider.validateToken(token) && this.userDAO.existUserByEmail(email)){
UserEntity userEntity = this.userDAO.readUser(email);
return this.makeToken(userEntity.getEmail(), userEntity.getRoles().get(0));
}else throw new RefreshTokenNotValidException("refresh token not valid");
}
결론
서비스의 Access Token을 통한 모든 인가 처리는 replay, MITM에 취약했다.
- 토큰을 탈취당할 경우 문제가 크다.
따라서 만료시간이 짧은 AccessToken과 만료시간이 긴 Refresh Token을 두어 토큰이 탈취되었을 경우 발생하는 문제에대해 좀 더 피해를 최소화할 수 있게 되었다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 프로젝트에 카카오 로그인, 회원가입 구현 - (1) (0) | 2024.03.31 |
---|---|
[Spring] Spring boot 프로젝트를 javadoc 문서로 만들기 (0) | 2024.03.29 |
[JPA] 영속성 컨텍스트 (0) | 2024.03.04 |
[Spring Test] MockMvc Response로 검증하기 (0) | 2024.02.05 |
[Spring Boot] 환경변수로 .jar와 docker run 프로파일 결정 (0) | 2024.02.05 |