📌 개선 이유
이번 프로젝트에서 대부분의 주요 기능을 구현한 후, 서버 부하를 줄이고 유지보수성을 향상시키기 위해 리팩토링을 진행하기로 했습니다.
이번 프로젝트에서 가장 많은 공부를 하였고, 신경을 썼던 부분은 Spring Security와 JWT를 사용한 토큰 및 회원 관리입니다. 백엔드 파트에서 신경쓰고자 한다면 무수히 많은 부분이 있겠지만, 아무래도 회원 관리의 보안에 대한 부분이 가장 기본적이라고 생각했기 때문입니다. 그래서 이 부분에 대해 더욱 깊이 이해하고 개선하고자 하는 욕심이 있었습니다.
- Refresh Token은 유효기간이 있기 때문에, 이를 DB에 저장하면 주기적으로 삭제해주어야 하는 번거로움이 존재합니다.
- Refresh Token에 대한 깊은 이해가 부족했고 일단 구현만하자 라는 생각으로 Cookie에 저장하고 사용했습니다. 하지만 쿠키는 CSRF 공격에 취약하다는 점을 가지고 있어 오히려 보안성을 떨어뜨린다고 생각합니다.
- Refresh Token을 세션 스토리지에 저장하는 것도 XSS 공격의 취약성을 가지고 있습니다.
이 부분에 대해 자세히 설명해보자면
Refresh Token은 일정 기간 동안만 사용할 수 있으며, 그 기간이 지나면 토큰은 더 이상 사용할 수 없다.
유효기간 내에서 사용자가 다시 로그인 하지 않아도 새로운 Access Token을 발급받기 위해 Refresh Token을 DB에 저장한다.
시간이 지나면서 유효기간이 만료된 Refresh Token들이 DB에 쌓이게 되고, 주기적으로 DB에서 삭제를 해주어야 한다.
따라서 Refresh Token을 Redis에 저장하는 방식을 채택했습니다. 그 이유는
- Key - Value 방식, 인메모리 DB 방식으로 빠르게 접근할 수 있습니다.
- 브라우저에 비해 탈취 가능성이 낮다고 생각하는 redis 서버에 저장하는 방식입니다.
- Refresh Token은 영구적으로 저장되는 데이터가 아닙니다.
Redis는 메모리 기반의 고속 데이터 저장소로, 토큰과 같은 일시적인 데이터를 관리하는 데 적합합니다.
Refresh Token은 영구적으로 저장될 필요가 없기 때문에 In-Memory DB를 사용해도 충분하며, 성능 이점을 챙길 수 있습니다.
📌 설치
기본적으로 Redis는 Unix 환경만 지원합니다.
Windows에서 사용하려면, 기존 Redis를 Windows 환경에 맞춰 사용할 수 있도록 바꿔야 하는데, github에 Redis for Windows를 릴리즈 해 주는 팀이 있습니다.
Linux/Unix : https://redis.io/download (Stable)
Windows 64Bit https://github.com/microsoftarchive/redis/releases
저의 경우, 여기에서 Windows 주소로 들어가
둘 중에 하나 다운 받으면 되는데, 저는 zip 파일을 다운 받았습니다.
📌 실행
🔺압축 풀기를 하면 Redis-x64-3.0.504 폴더가 생깁니다.
해당 폴더 내의 redis-server.exe를 실행하면 redis 서버가 구동되는데, 기본적으로 Listen Port는 6379 port 입니다.
🔺 정상적으로 Redis 서버가 구동된 모습입니다.
🔺 폴더 내의 redis-cli.exe를 실행하면 Redis Client Command가 실행되는데, 명령어를 실행할 수 있는 파일입니다.
📌 Command
ping
→ PONG이 나오면 실행된 것이 확인됨
set key “value”
→ key에 value값을 지정
get key
→ 해당 key에 따른 값이 나온다.
keys *
→ 모든 key 값 출력
📌 Springboot에서 사용해보자
🔺 Spring boot에서 Redis 설정
이제 스프링부트에서 spring-data-redis 라이브러리를 활용해 Redis를 사용해봅시다.
build.gradle에 spring-boot-starter-data-redis 를 추가하고 빌드해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
application.yaml 을 사용한다면 host와 port를 설정해야 합니다.
하지만 저의 경우엔 properties를 쓰고있으니 패스해줍니다.
🔺 RedisRepository
Redis Template 방식이 아닌 Redis Repository 방식을 사용합니다.
저는 CrudRepository를 상속받는 RedisRepository 방식을 이용했습니다.
별도의 Configuration 의존성 추가가 필요하지 않고 Redis Template 방식보다 훨씬 구현이 간편하기 때문입니다.
CrudRepository 를 상속받는 Repository 클래스를 추가합니다.
public interface PersonRedisRepository extends CrudRepository<User, String> {
}
🔺UserController
로그인 시에 사용자의 인증 정보를 확인하고, 인증이 성공하면 JWT 토큰을 발급할 것이기 때문에 login() 메서드를 수정해줍니다.
로그인 (login 메서드)
사용자의 로그인 정보를 받아 인증을 시도합니다.
인증이 성공하면 JWT 토큰을 발급합니다.
발급된 JWT 토큰을 클라이언트에게 반환합니다.
@PostMapping("/login")
public ResponseEntity<JwtToken> login(@RequestBody LoginDTO loginDTO, HttpServletResponse response) {
String userId = loginDTO.getUserId();
String password = loginDTO.getPassword();
JwtToken tokenInfo = userService.login(userId, password, response);
// 클라이언트에는 액세스 토큰만 반환
return ResponseEntity.ok(new JwtToken(tokenInfo.getAccessToken(), null));
}
🔺UserService
/**
* 기능: 로그인
*/
public JwtToken login(String userId, String password, HttpServletResponse response) {
try {
// 1. Login ID/PW 를 기반으로 Authentication 객체 생성
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userId, password);
// 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 3. 인증 정보를 기반으로 JWT 토큰 생성
JwtToken tokenInfo = jwtTokenProvider.generateToken(authentication);
// 리프레시 토큰을 Redis에 저장
storeRefreshToken(authentication.getName(), tokenInfo.getRefreshToken());
// HTTP-Only 쿠키에 리프레시 토큰 설정
Cookie cookie = new Cookie("refreshToken", tokenInfo.getRefreshToken());
cookie.setMaxAge(7 * 24 * 60 * 60); // 쿠키 만료 시간 설정 (7일)
cookie.setSecure(true); // Secure 속성 설정
cookie.setHttpOnly(true); // HttpOnly 속성 설정
cookie.setPath("/"); // 쿠키의 경로 설정
response.addCookie(cookie); // 응답에 쿠키 추가
return tokenInfo;
} catch (BadCredentialsException e) {
throw new RuntimeException("비밀번호 불일치", e);
} catch (Exception e) {
System.err.println(e);
throw new RuntimeException("로그인 중 오류 발생", e);
}
}
🔺RedisService
// 리프레시 토큰을 Redis에 저장
public void storeRefreshToken(String userId, String refreshToken) {
stringRedisTemplate.opsForValue().set(userId, refreshToken); // 사용자 ID를 키로 하여 리프레시 토큰 저장
}
// Redis에 이미 존재하는 리프레시 토큰이 있다면 업데이트
String existingRefreshToken = stringRedisTemplate.opsForValue().get(authentication.getName());
if (existingRefreshToken != null) {
// 기존 리프레시 토큰이 있는 경우 갱신
stringRedisTemplate.opsForValue().set(authentication.getName(), tokenInfo.getRefreshToken());
} else {
// 새로운 리프레시 토큰 저장
stringRedisTemplate.opsForValue().set(authentication.getName(), tokenInfo.getRefreshToken());
}
로그인 할 때 마다 계속 새로운 값을 레디스에 저장하게 되는 것을 방지하기 위해
Redis에서 이미 존재하는 리프레시 토큰을 확인하고, 존재하면 갱신하고 존재하지 않으면 새로 저장하는 로직으로 구현하였습니다.
stringRedisTemplate.opsForValue().set(userId, refreshToken);
- stringRedisTemplate: StringRedisTemplate 객체는 Redis와 상호작용하는 데 사용됩니다.
- opsForValue(): StringRedisTemplate에서 제공하는 메서드로, Redis의 값 작업을 처리합니다. 주로 문자열(String) 값을 저장하고 조회하는 데 사용됩니다.
- set(userId, refreshToken):
- userId: 저장할 키.
- refreshToken: 저장할 값.
- Redis에 userId를 키로 하고 refreshToken을 값으로 설정합니다.
'Project 댕린이집' 카테고리의 다른 글
[Refactoring] Axios Interceptor 적용 (0) | 2024.07.29 |
---|---|
[소셜로그인] 카카오 로그인 (0) | 2024.07.03 |
[알림장] 작성 / 조회 (0) | 2024.07.01 |
[게시판] 댓글 (0) | 2024.06.29 |
[게시판] 좋아요 (0) | 2024.06.28 |