๋์ ์ทจ์ง
- ๊ทธ ๋์์ ํ๋ก์ ํธ ๊ฒฝํ์ ํตํด ๋ค์ํ ์ธ์ด, ๊ทธ ์ธ ์คํ ํ๊ฒฝ ๋ฑ์ ๋ํด ๊ณต๋ถํ๋ฉฐ ๊ฐ ํ๋ก์ ํธ๋ง๋ค ํ์ ํ๊ฒฝ์ด ๋ค๋ฅด๊ณ ๊ฐ๋ฐ์์ ๋ก์ปฌ ๋จธ์ ํ๊ฒฝ ๋ํ ๋งค์ฐ ๋ค์ํ๋ฏ๋ก, ์ด ๊ฒฝ์ฐ ๋์ปค๋ฅผ ํ์ฉํ๋ค๋ฉด ํ๊ฒฝ ์กฐ์ฑ์ ๋งค์ฐ ํจ์จ์ ์ผ๋ก ์งํํ ์ ์์ด ๋์ ํด๋ณด๊ฒ ๋์๋ค.
- JWT ํ ํฐ์ ํ์ฉํ ๋ก๊ทธ์ธ ๋ฐฉ์์ ๋ณด์์ ๋ ๊ฐํํ ๋ฐฉ์์ ๋์ ํด๋ณด๊ณ ์, ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ํด ๋ฐฐ์ฐ๊ณ ๋์ ํ๊ธฐ๋ก ๊ฒฐ์ ํ๋ค.
์ฅ์
- ๋ก์ปฌ์์ ์ธ์ด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ์ ๋ฐ๋ก ์ค์นํ์ง ์์๋ ๋์ปค ๋ช ๋ น์ ํตํด ํธ์คํธ ๋จธ์ ์์ ํ๋ก์ ํธ๋ฅผ ์คํํ ์ ์๋ค.
- ์๊ฒฉ ๋ฆฌ๋ชจํธ ๋จธ์ ์ ๋์ปคํํ ํ๋ก์ ํธ๋ฅผ ๋ฐฐํฌํด ํจ์จ์ ์ผ๋ก ๊ฐ๋ฐ์ ์งํํ ์ ์๋ค.
- ์ก์ธ์ค ํ ํฐ์ ์ ํจ๊ธฐ๊ฐ์ ์งง๊ฒ ์ค์ ํด ๋ณด์์ ๊ฐํํ๋ฉด์๋, ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์ฃผ๊ธฐ๋ฅผ ๊ธธ๊ฒ ์ ์ง์ํฌ ์ ์๋ค.
๊ตฌํ ๋ชฉํ
- JWT Refresh Token์ ์ ์ฅํ๋ Redis ๊ตฌํ ๋ง ํ๋ก์ ํธ ์ด๋ฏธ์ง๋ฅผ ๋์ปค ํ๋ธ์ ํธ์
- AWS EC2 ์ธ์คํด์ค๋ฅผ ๋ฆฌ๋ชจํธ ๋จธ์ ์ผ๋ก ํ์ฉํด ํ๋ก์ ํธ ๋ฐฐํฌ
๊ตฌํ ์ ์์๋๋ ํธ๋ฌ๋ธ ์ํ
- Node.js๋ก ํ์ต ๋ฐ ์ค์ตํ๊ธฐ ๋๋ฌธ์ Java ๋ฐ ์คํ๋ง, ์คํ๋ง ๋ถํธ์ ๋์ปค๋ฅผ ์ ์ฉํ๋ ๊ฒ์ ์ํ์ฐฉ์ค๊ฐ ์์๋๋ค.
- ๋ ๋์ค๋ก ๋ฆฌํ๋ ์ฌ ํ ํฐ์ ๊ตฌํํ ๋์ ์ํ์ฐฉ์ค๊ฐ ์์๋๋ค.
์คํ ํ๊ฒฝ
- Java 17
- Springboot 2.7.6
- Gradle
- Redis 2.7.6
1. Redis๋ก Refresh ํ ํฐ ๊ตฌํ
- ๋ฆฌํ๋ ์ ํ ํฐ ๊ตฌํ์ ๋ ๋์ค๋ฅผ ์ฌ์ฉํ๋ ์ด์ : ๋ ๋์ค๋ ๋ฐ์ดํฐ๋ฅผ RAM์ ์ ์ฅํ๋ฏ๋ก ์์์ฑ์ด ์๋ค. ๋์ ๋ฐ์ดํฐ ์์ธ์ค ์๋๊ฐ ๋งค์ฐ ๋น ๋ฅด๊ธฐ ๋๋ฌธ์ ์ฃผ๋ก ์บ์ฑํ ๋ ์ฌ์ฉํ๊ณ , ๋ฆฌํ๋ ์ ํ ํฐ์ ๊ฒฝ์ฐ ์์์ฑ์ด ํ์์๊ณ , ๋น ๋ฅธ ์์ธ์ค ์๋๊ฐ ์ค์ํ๋ฏ๋ก ์ ํํ๊ฒ ๋์๋ค. ๋ํ ๋ ๋์ค์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐ์ดํฐ์ ์ ํจ ๊ธฐ๊ฐ์ ์ง์ ํ ์ ์์ด, ์ผ์ ๊ธฐ๊ฐ ํ ๋ง๋ฃ๋์ด์ผ ํ๋ ํ ํฐ์ ๊ฒฝ์ฐ์ ์ ํฉํ๋ค.
Refresh ํ ํฐ ์๋ฆฌ
- ๋ก๊ทธ์ธ ์ ์์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ JWT ํ ํฐ์ผ๋ก ๋ฐ๊ธ๋ฐ๊ณ , ๋ฆฌํ๋ ์ ํ ํฐ๋ง ์๋ฒ์ DB์ ์ ์ฅํ๊ณ ํด๋ผ์ด์ธํธ์๊ฒ ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ชจ๋ ๋ฐ๊ธํ๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ์๋น์ค๋ฅผ ์ด์ฉํ ๋๋ง๋ค ์์ฒญ์ ์ก์ธ์ค & ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ด์ ๋ณด๋ด๊ณ , ์ก์ธ์ค ํ ํฐ์ ์ ํจ๊ธฐ๊ฐ์ด ๋ง๋ฃ๋์์ ๋ ๋ฆฌํ๋ ์ ํ ํฐ์ด ์ ํจํ๋ค๋ฉด ์๋ฒ์์ ์ก์ธ์ค ํ ํฐ์ ์ฌ๋ฐ๊ธํ๋ค.
์์ง ๋๋ฉ์ธ๋ณ๋ก ๋๋ ์ฝ๋๊ฐ ์๋์ด์, ๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ก๋ฅผ ์ง์ ํธ์ถํ๊ณ ์์ต๋๋ค. ๋ณ๊ฒฝ ํ ๋ค์ ์ ๋ฐ์ดํธ ์์ ์ ๋๋ค.
1) ๋ก๊ทธ์ธ
- JWT ํ ํฐ ๋ฐ๊ธ : ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ ๋, access token์ ๋ฐ๊ธํ ์ฌ์ฉ์์๊ฒ ์๋ต ํค๋์ ์ถ๊ฐ๋๋ค.
@Component
@RequiredArgsConstructor
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long ACCESS_TOKEN_TIME = (long) 60 * 60;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createAccessToken(String username) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
}
- refresh token์ ์์์ ํ ํฐ ํ์์ผ๋ก ๋ฐ๊ธ ํ ์๋ฒ์ ์ ์ฅ๋๋ค. ๋ํ HttpOnly ์ฟ ํค์ ๋ด๊ฒจ ์๋ต ํค๋์ ์ถ๊ฐ๋๋ค.
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final UserRepository userRepository;
private final UserService userService;
private final RefreshTokenRepository refreshTokenRepository;
public String createRefreshToken(String username) {
RefreshToken refreshToken = new RefreshToken(UUID.randomUUID().toString(),
findByUsername(username).getUsername());
saveRefreshToken(refreshToken);
return refreshToken.toString();
}
public Cookie createRefreshCookie(String stringRefreshToken) {
Cookie cookie = new Cookie("refreshToken", stringRefreshToken);
cookie.setMaxAge(60 * 60 * 336);
cookie.setHttpOnly(true);
cookie.setSecure(true);
return cookie;
}
private void saveRefreshToken(RefreshToken refreshToken) {
refreshTokenRepository.save(refreshToken);
}
private User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ ์ ๋ ์กด์ฌํ์ง ์์ต๋๋ค"));
}
}
- ๋ฐ๊ธ๋ ๋ฆฌํ๋ ์ ํ ํฐ์ ์๋ฒ์ ์ ์ฅ๋๋ค.
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
}
- login ์ปจํธ๋กค๋ฌ
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@PostMapping("/login")
public ResponseEntity<MessageResponseDto> login(@RequestBody LoginRequestDto loginRequestDto,
HttpServletResponse response) {
userService.login(loginRequestDto, response);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
- UserService์ Login ๋ฉ์๋
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final RefreshTokenService refreshTokenService;
@Transactional
public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
User user = findByUsername(loginRequestDto.getUsername());
// ๋น๋ฐ๋ฒํธ ํ์ธ
if (!user.getPassword().equals(loginRequestDto.getPassword())) {
throw new IllegalArgumentException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
}
// token ๋ฐ๊ธ
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createAccessToken(user.getUsername()));
response.addCookie(
refreshTokenService.createRefreshCookie(
refreshTokenService.createRefreshToken(user.getUsername())));
}
private User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ ์ ๋ ์กด์ฌํ์ง ์์ต๋๋ค"));
}
}
- Redis๋ ํค : ๊ฐ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ์์ด๋๋ ๋ฌธ์์ด์ด๋ค.
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 336L)
@Getter
public class RefreshToken {
@Id
private String refreshToken;
private String username;
public RefreshToken(String refreshToken, String username) {
this.refreshToken = refreshToken;
this.username = username;
}
2) JWT ํ ํฐ ๊ฒ์ฆ ๋ฐ ์ฌ๋ฐ๊ธ
- ์์ฒญ ์ ๋ง๋ค Spring Security ์ธ์ฆ/์ธ๊ฐ ์งํ
@Slf4j // ๋ก๊น
์ ๋ํด ์ถ์ ๋ ์ด์ด๋ฅผ ์ ๊ณตํ๋ ์ธํฐํ์ด์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String resolveAccessToken = jwtUtil.resolveAccessToken(request);
String refreshToken = separateRefreshToken(resolveRefreshTokenFromCookies(request))[0];
String username = separateRefreshToken(resolveRefreshTokenFromCookies(request))[1];
if (resolveAccessToken != null) {
if (!jwtUtil.validateAccessToken(resolveAccessToken) && jwtUtil.checkExpirationToken(
resolveAccessToken, refreshToken, username, response)) {
jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
}
setAuthentication(jwtUtil.getUserInfoFromToken(resolveAccessToken).getSubject());
}
filterChain.doFilter(request, response);
}
private void setAuthentication(String username) { // ์ธ์ฆ ๊ฐ์ฒด ์์ฑ ๋ฐ ๋ฑ๋ก : ์ฝ๋ ๊ฐ์ํ ๋ฐ ์ฑ
์ ๋ถ๋ฆฌ ๋ชฉ์
SecurityContext context = SecurityContextHolder.createEmptyContext(); // ContextHolder์ ์ธ์ฆ ๊ฐ์ฒด ์ ์ฅ
Authentication authentication = jwtUtil.createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// ์ฟ ํค์์ ๋ฆฌํ๋ ์ ํ ํฐ ์ถ์ถ
public String resolveRefreshTokenFromCookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
String resolveRefreshToken = null;
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refreshToken")) {
resolveRefreshToken = cookie.getValue();
}
}
return resolveRefreshToken;
}
// ํ ํฐ๊ณผ username ๋ถ๋ฆฌ
public String[] separateRefreshToken(String cookieValue) {
return cookieValue.split(":");
}
// ํ ํฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ, Exception ๊ฒฐ๊ณผ๊ฐ์ ์ฌ์ฉ์์๊ฒ ๋ฐํํ๋ค
private void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
try {
// ObjectMapper๋ฅผ ํตํด ๋ณํํ๋ค
String json = new ObjectMapper().writeValueAsString(new MessageResponseDto(msg, statusCode));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
0) ์์ธ์ค ํ ํฐ์ด ๋ง๋ฃ๋์๋ค๋ฉด
1) ๋ฆฌํ๋ ์ ํ ํฐ์ ํ์ธ, ๋ง๋ฃ๋์ด ์์ง ์๋ค๋ฉด ์ก์ธ์ค ํ ํฐ์ ์ฌ๋ฐ๊ธํ๊ณ ์ด๋ฏธ ๋ง๋ฃ๋์๋ค๋ฉด ๋ก๊ทธ์์ํ๋ค.
2) ๋ก๊ทธ์์ ์ ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฆฌํ๋ ์ ํ ํฐ์ ์ญ์ ๋๋ค.
- refreshService
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
private static final String BEARER_PREFIX = "Bearer ";
private static final long ACCESS_TOKEN_TIME = (long) 60 * 60;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
private void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// ๋ฆฌํ๋ ์ ํ ํฐ ๊ฒ์ฆ ๋ฐ ๋ก๊ทธ์์ ์ฐ๊ฒฐ
public void checkRefreshToken(String StringRefreshToken, String username,
HttpServletResponse response) {
if (refreshTokenRepository.existsById(StringRefreshToken)) {
regenerateRefreshToken(username, response);
} else {
RefreshToken refreshToken = refreshTokenRepository.findById(StringRefreshToken).orElseThrow(
() -> new IllegalArgumentException("์กด์ฌํ์ง ์๋ ํ ํฐ์
๋๋ค.")
);
deleteRefreshToken(refreshToken);
}
}
public void deleteRefreshToken(RefreshToken refreshToken) {
refreshTokenRepository.delete(refreshToken);
}
private void regenerateRefreshToken(String username, HttpServletResponse response) {
Date date = new Date();
String newAccessToken = BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, newAccessToken);
}
public RefreshToken findById(String refreshToken) {
return refreshTokenRepository.findById(refreshToken).orElseThrow(
() -> new IllegalArgumentException("์กด์ฌํ์ง ์๋ ํ ํฐ์
๋๋ค.")
);
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@PostMapping("/logout")
public ResponseEntity<MessageResponseDto> logout(HttpServletRequest request,
HttpServletResponse response) {
userService.logout(request, response);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
- userService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final JwtAuthFilter jwtAuthFilter;
private final RefreshTokenService refreshTokenService;
public void logout(HttpServletRequest request, HttpServletResponse response) {
response.setHeader(JwtUtil.AUTHORIZATION_HEADER, null);
refreshTokenService.deleteRefreshToken(refreshTokenService.findById(
jwtAuthFilter.separateRefreshToken(
jwtAuthFilter.resolveRefreshTokenFromCookies(request))[0]));
}
private User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new IllegalArgumentException("ํด๋น ์ ์ ๋ ์กด์ฌํ์ง ์์ต๋๋ค"));
}
}
2. ํ๋ก์ ํธ ๋ฐฐํฌ๋ฅผ ์ํ Dockerfile ์์ฑ
- ๋์ปคํ์ผ์ ์์ฑํด ๋์ปค๊ฐ ๋ช ๋ น์ ์ํํ ์ ์๋๋ก ํ๋ค.
1) ํ๋ก์ ํธ Dockerize
- Gradle์ bootjar๋ฅผ ์ด์ฉํด ์ค๋ ์ท์ ๋ง๋ ๋ค
- Dockfile ์์ฑ
FROM openjdk:17-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
2) Docker-Compose.yaml
version: "2.17.2"
services:
redis:
image: 'redis'
backend:
build:
context: .
args:
JAR_FIlE: build/libs/*.jar
image: backend
depends_on:
- redis
3. EC2์ธ์คํด์ค๋ฅผ ์๊ฒฉ ๋ฆฌ๋ชจํธ ๋จธ์ ์ผ๋ก ํ์ฌ ๋ฐฐํฌํ๊ธฐ
- ๊ฐ์์์ ๋ฐฐ์ด ECS ์๋น์ค๋ ์ค์ ๋ก ๊ฒฐ์ ๊ฐ ๋๊ณ , ๊ทธ ์๋น์ค๊น์ง ํ์ํ๋ค๊ณ ๋ ์๊ฐํ์ง ์์ ์๋ ๋ฐฐํฌ ๋ฐฉ์์ ์ ํํ๊ณ ๋์ ํด๋ณด์๋ค.
https://cdaosldk.tistory.com/242
4. ํธ๋ฌ๋ธ์ํ
- ๋ ๋์ค๋ฅผ ์ฐ๊ฒฐํ๋ ๋์ค ํ ํฐ์ ๊ฒ์ฆํ๊ณ ์ฌ๋ฐ๊ธํ๋ ๊ณผ์ ์์ ๋ฆฌ์์ค๊ฐ ๋ง์ด ์์๋์๋ค. ๊ฒ์์ ํตํ ํ์ต ๋ฐ ์ค์ ์ฌ๋ฌ ์ํ์ฐฉ์ค๋ฅผ ํตํด ๊ตฌํํด๋ณธ ์ฝ๋์ฌ์, ํ์ง ์ฌ๋ถ์ ๊ด๊ณ์์ด ๋งค์ฐ ๋ฟ๋ฏํ๋ค.
- ๊ฐ์์์๋ Node.js๋ฅผ ๋์ปค๋ผ์ด์ฆํ๊ณ ๋ฐฐํฌํด์, ์๋ฐ์ ๊ฒฝ์ฐ์๋ ๋ญ ํฌ๊ฒ ๋ค๋ฅผ๊น ์ถ์์ง๋ง, ์๋ฐ์ ๊ฒฝ์ฐ ์ง๊ธ๊น์ง ๋ด๊ฐ ์ดํดํ ๊ฒ์ผ๋ก๋ ์ค๋ ์ท์ ๋จผ์ ์ฐ๊ณ ์ด ์ค๋ ์ท์ ์ด๋ฏธ์ง๋ก ๋ง๋๋ ๊ฒ์ด์ด์ ๋ฐ์ธ๋ ๋ง์ดํธ์ ๋ํด์๋ ์ด๋ป๊ฒ ์ ์ฉํ ์ง ์์ง์ ๋ฏธ์ง์๋ค. ์ฐ์ ์ค๋ ์ท์ ์ด๋ฏธ์งํํด์ ๋ฐฐํฌํ๋ ๊ฒ๊น์ง๊ฐ ์ด๋ฒ ๊ณผ์ ์ ๋ชฉํ์๊ธฐ ๋๋ฌธ์ ์ด๋ฒ์ ์ด๊ฑธ๋ก ๋ง์กฑํ๋ค.
์ฐธ๊ณ ํ ๊ธ
https://medium.com/@uk960214/refresh-token-%EB%8F%84%EC%9E%85%EA%B8%B0-f12-dd79de9fb0f0
'TIL, WIL > WIL๐' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
4์ ๋ง์ง๋ง ์ฃผ : ์ฌ์ด๋ ํ๋ก์ ํธ ๊ฐ๋ฐ ์คํ (0) | 2024.04.28 |
---|---|
3์ ๋ท์งธ ์ฃผ : 50์ ์ ๋ฆฌ (0) | 2023.03.27 |