Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/main/java/com/_data/_data/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import com._data._data.auth.dto.LoginRequest;
import com._data._data.auth.dto.LoginResponse;
import com._data._data.auth.dto.RefreshRequest;
import com._data._data.auth.exception.EmailNotFoundException;
import com._data._data.auth.exception.InvalidPasswordException;
import com._data._data.auth.exception.TokenExpiredException;
import com._data._data.auth.exception.TokenNotFoundException;
import com._data._data.auth.jwt.RefreshTokenService;
import com._data._data.auth.service.AuthServiceImpl;
import com._data._data.common.dto.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -26,6 +30,7 @@
public class AuthController {

private final AuthServiceImpl authServiceImpl;
private final RefreshTokenService refreshTokenService;

@Operation(
summary = "로그인",
Expand All @@ -52,4 +57,25 @@ public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
.body(new ApiResponse(false, ex.getMessage()));
}
}


@PostMapping("/refresh")
@Operation(summary = "토큰 갱신", description = "Refresh Token으로 새로운 Access Token을 발급받습니다.")
public ResponseEntity<?> refresh(@Valid @RequestBody RefreshRequest request) {
try {
LoginResponse tokens = refreshTokenService.refreshAccessToken(request.refreshToken());
return ResponseEntity.ok(tokens);
} catch (TokenNotFoundException | TokenExpiredException ex) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse(false, ex.getMessage()));
}
}

@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "Refresh Token을 무효화합니다.")
public ResponseEntity<?> logout(@Valid @RequestBody RefreshRequest request) {
refreshTokenService.logout(request.refreshToken());
return ResponseEntity.ok(new ApiResponse(true, "로그아웃되었습니다."));
}
}
5 changes: 4 additions & 1 deletion src/main/java/com/_data/_data/auth/dto/LoginResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@

public record LoginResponse(
String accessToken,
String refreshToken
String refreshToken,
String tokenType,
Long expiresIn

) {}
36 changes: 36 additions & 0 deletions src/main/java/com/_data/_data/auth/entity/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com._data._data.auth.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;

@Entity
@Table(name = "refresh_tokens")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String token;

@Column(nullable = false)
private Long userId;

@Column(nullable = false)
private LocalDateTime expiryDate;

@Column(nullable = false)
private LocalDateTime createdDate;

public boolean isExpired() {
return LocalDateTime.now().isAfter(this.expiryDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com._data._data.auth.exception;

public class TokenExpiredException extends RuntimeException {
public TokenExpiredException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com._data._data.auth.exception;

public class TokenNotFoundException extends RuntimeException {
public TokenNotFoundException(String message) {
super(message);
}
}
51 changes: 46 additions & 5 deletions src/main/java/com/_data/_data/auth/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com._data._data.auth.jwt;

import com._data._data.auth.dto.LoginResponse;
import com._data._data.auth.entity.RefreshToken;
import com._data._data.auth.repository.RefreshTokenRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
Expand All @@ -11,8 +13,10 @@
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
Expand All @@ -23,17 +27,19 @@ public class JwtTokenProvider {
private final Key key;
private final long accessTokenExpMillis;
private final long refreshTokenExpMillis;
private final RefreshTokenRepository refreshTokenRepository;

public JwtTokenProvider(
@Value("${jwt.secret}") String secretKey,
@Value("${jwt.expiration_time}") long accessTokenExpMillis,
@Value("${jwt.refresh_token_expiration_time}") long refreshTokenExpMillis

@Value("${jwt.refresh_token_expiration_time}") long refreshTokenExpMillis,
RefreshTokenRepository refreshTokenRepository
) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.accessTokenExpMillis = accessTokenExpMillis;
this.refreshTokenExpMillis = refreshTokenExpMillis;
this.refreshTokenRepository = refreshTokenRepository;
}

public String createAccessToken(Long userId, String email) {
Expand Down Expand Up @@ -61,15 +67,44 @@ private String createToken(Long userId, String email, long expireMillis) {
}

public LoginResponse generateTokenDto(Long userId, String email) {
String accessToken = createAccessToken(userId, email);
String refreshToken = createRefreshToken(userId, email);
return new LoginResponse(accessToken, refreshToken);
String accessToken = createAccessToken(userId, email);
String refreshToken = generateAndSaveRefreshToken(userId);

return new LoginResponse(
accessToken,
refreshToken,
"Bearer",
accessTokenExpMillis / 1000
);
}

private String generateAndSaveRefreshToken(Long userId) {
// 기존 Refresh Token 삭제
refreshTokenRepository.deleteByUserId(userId);

// 새 Refresh Token 생성
String tokenValue = UUID.randomUUID().toString();
LocalDateTime expiryDate = LocalDateTime.now()
.plusSeconds(refreshTokenExpMillis / 1000);

RefreshToken refreshToken = new RefreshToken();
refreshToken.setToken(tokenValue);
refreshToken.setUserId(userId);
refreshToken.setExpiryDate(expiryDate);
refreshToken.setCreatedDate(LocalDateTime.now());

refreshTokenRepository.save(refreshToken);

return tokenValue;
}

public Long getUserId(String token) {
return parseClaims(token).get("userId", Long.class);
}

public String getEmail(String token) {
return parseClaims(token).get("email", String.class);
}

public Claims parseClaims(String accessToken) {
try {
Expand All @@ -94,4 +129,10 @@ public boolean validateToken(String token) {
}
return false;
}

public boolean validateRefreshToken(String refreshToken) {
return refreshTokenRepository.findByToken(refreshToken)
.map(token -> !token.isExpired())
.orElse(false);
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/_data/_data/auth/jwt/RefreshTokenService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com._data._data.auth.jwt;

import com._data._data.auth.dto.LoginResponse;
import com._data._data.auth.entity.RefreshToken;
import com._data._data.auth.exception.TokenExpiredException;
import com._data._data.auth.exception.TokenNotFoundException;
import com._data._data.auth.jwt.JwtTokenProvider;
import com._data._data.auth.repository.RefreshTokenRepository;
import com._data._data.user.entity.Users;
import com._data._data.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

private final RefreshTokenRepository refreshTokenRepository;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
public LoginResponse refreshAccessToken(String refreshTokenValue) {
RefreshToken refreshToken = refreshTokenRepository.findByToken(refreshTokenValue)
.orElseThrow(() -> new TokenNotFoundException("유효하지 않은 Refresh Token입니다."));

if (refreshToken.isExpired()) {
refreshTokenRepository.delete(refreshToken);
throw new TokenExpiredException("만료된 Refresh Token입니다.");
}

Users user = userRepository.findById(refreshToken.getUserId())
.orElseThrow(() -> new TokenNotFoundException("사용자를 찾을 수 없습니다."));

return jwtTokenProvider.generateTokenDto(user.getId(), user.getEmail());
}

@Transactional
public void logout(String refreshTokenValue) {
refreshTokenRepository.deleteByToken(refreshTokenValue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com._data._data.auth.repository;


import com._data._data.auth.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Long userId);
void deleteByUserId(Long userId);
void deleteByToken(String token);
}
2 changes: 1 addition & 1 deletion src/main/java/com/_data/_data/user/entity/Nation.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class Nation {
@Id
private Long id;

@Column(unique = true, nullable = false, length = 3)
@Column(unique = true, nullable = false)
private String code;

@Column(nullable = false, length = 100)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading