Skip to content

Commit

Permalink
Merge pull request #23 from SW-rocket-dan/3-μœ μ €
Browse files Browse the repository at this point in the history
[#21] [#22] μ‚¬μš©μžλŠ” λ‘œκ·Έμ•„μ›ƒ, μžλ™ λ‘œκ·ΈμΈμ„ ν•  수 μžˆλ‹€.
  • Loading branch information
inpink authored Aug 2, 2024
2 parents f7496a5 + 51688df commit 7138444
Show file tree
Hide file tree
Showing 26 changed files with 401 additions and 153 deletions.
11 changes: 9 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ grant_type=authorization_code&code=AUTH_CODE&redirect_uri=YOUR_REDIRECT_URI&clie
- [X] APIλŠ” Swaggerμ—μ„œ 확인 κ°€λŠ₯ν•˜λ‹€.
- [X] http://localhost:8080/swagger-ui.html

- μœ μ €λŠ” μžλ™ λ‘œκ·ΈμΈμ„ ν•  수 μžˆλ‹€.
- μœ μ €λŠ” λ‘œκ·Έμ•„μ›ƒμ„ ν•  수 μžˆλ‹€.
- [X] μœ μ €λŠ” μžλ™ λ‘œκ·ΈμΈμ„ ν•  수 μžˆλ‹€.
- [X] refresh token을 λ°œκΈ‰ν•  수 μžˆλ‹€.
- [X] refresh token을 톡해 access token을 μž¬λ°œκΈ‰ λ°›λŠ”λ‹€.
- [X] ν”„λ‘ νŠΈμ—μ„œ access token 보낼 λ•Œ 401 => refresh token μ•ˆλ³΄λƒ„(μ‚¬μš©μžκ°€ 직접 λ‘œκ·Έμ•„μ›ƒ/λΈ”λž™λ¦¬μŠ€νŠΈ λ“±)
- [X] ν”„λ‘ νŠΈμ—μ„œ access token 보낼 λ•Œ 403 => refresh token이 있으면 보내주세μš₯(λͺ¨μ’…μ˜ 이유둜 토큰이 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŒ. 재인증 ν•„μš”)

- [X] μœ μ €λŠ” λ‘œκ·Έμ•„μ›ƒμ„ ν•  수 μžˆλ‹€.
- μœ μ €λŠ” νšŒμ›νƒˆν‡΄λ₯Ό ν•  수 μžˆλ‹€.
- (μœ μ €λŠ” νšŒμ›μ •λ³΄λ₯Ό μˆ˜μ •ν•  수 μžˆλ‹€.)

- λΈ”λž™λ¦¬μŠ€νŠΈλ₯Ό 직접 λ“±λ‘ν•˜μ—¬ 차단할 수 μžˆλ‹€.
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,30 @@
import app.cardcapture.auth.google.dto.GoogleLoginRequestDto;
import app.cardcapture.auth.google.dto.GoogleTokenResponseDto;
import app.cardcapture.auth.google.service.GoogleAuthService;
import app.cardcapture.auth.jwt.dto.JwtDto;
import app.cardcapture.auth.jwt.domain.Claims;
import app.cardcapture.auth.jwt.dto.JwtResponseDto;
import app.cardcapture.auth.jwt.dto.RefreshTokenRequestDto;
import app.cardcapture.auth.jwt.service.JwtComponent;
import app.cardcapture.common.dto.SuccessResponseDto;
import app.cardcapture.common.utils.TimeUtils;
import app.cardcapture.user.domain.entity.User;
import app.cardcapture.user.dto.UserDto;
import app.cardcapture.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.ZoneId;
import java.util.Date;
import java.util.Optional;

@RestController
Expand All @@ -42,34 +51,54 @@ public ResponseEntity<SuccessResponseDto<GoogleLoginRequestDto>> getGoogleLoginD
.responseType(googleAuthConfig.getResponseType())
.clientId(googleAuthConfig.getClientId())
.build();

SuccessResponseDto responseDto = SuccessResponseDto.create(googleLoginRequestDto);

return ResponseEntity.ok(responseDto);
} // TODO: 쿠킀에 λ„£μ–΄μ£Όλ©΄ ν”„λ‘ νŠΈκ°€ νŽΈν•  수 μžˆλ‹€ κ³ λ €ν•΄λ³Ό 것
}

@GetMapping("/redirect") // TODO: λ§Œμ•½ access token이 만료됐으면, ν”„λ‘ νŠΈμ—μ„œ refreshtoken이 있으면 λ³΄λ‚΄λ‹¬λΌλŠ” 의미둜 μ—λŸ¬μ½”λ“œλ₯Ό λ‹€λ₯Έ 걸둜 λ³΄λ‚΄μ€˜μ•Όν•¨(상황에 따라 ν”„λ‘ νŠΈμ—μ„œ κΈ°λŒ€λ˜λŠ” λ™μž‘μ΄ λ‹€λ₯΄κΈ° λ•Œλ¬Έμ— 이λ₯Ό 사전에 μ •ν•΄λ†“λŠ” 것 ν•„μš”)
// μ§€κΈˆμ€ λ‹€ 403λ³΄λ‚΄κ³ μžˆλŠ”λ°, 이λ₯Ό κ΅¬λΆ„ν•΄μ£ΌκΈ°μœ„ν•œ μš©λ„λ‘œ μ—λŸ¬μ½”λ“œλ₯Ό μ΄μš©ν•  수 μžˆλ‹€.
@GetMapping("/redirect")
@Operation(summary = "ꡬ글 λ¦¬λ‹€μ΄λ ‰νŠΈ μ—”λ“œν¬μΈνŠΈ", description = "ꡬ글 λ¦¬λ‹€μ΄λ ‰νŠΈλ₯Ό 톡해 받은 auth codeλ₯Ό λ°›μŠ΅λ‹ˆλ‹€. auth codeλ₯Ό μ΄μš©ν•˜μ—¬ μœ μ € 정보λ₯Ό κ°€μ Έμ˜¬ κ²ƒμž…λ‹ˆλ‹€.")
public ResponseEntity<SuccessResponseDto<JwtDto>> getGoogleRedirect(@RequestParam(name = "code") String authCode)
{
// TODO: Service둜 μ’€ 감좔기 (ν…ŒμŠ€νŠΈ κ΄€μ μ—μ„œλ„ λ³΅μž‘λ„ κ°μ†Œ κ°€λŠ₯)
GoogleTokenResponseDto googleTokenResponseDto = googleAuthService.getGoogleToken(authCode);
UserDto userDto = googleAuthService.getUserInfo(googleTokenResponseDto.getAccessToken());
@Transactional
public ResponseEntity<SuccessResponseDto<JwtResponseDto>> getGoogleRedirect(
@RequestParam(name = "code") String authCode
) {
GoogleTokenResponseDto googleTokenResponseDto = googleAuthService.getGoogleToken(authCode);
UserDto userDto = googleAuthService.getUserInfo(googleTokenResponseDto.getAccessToken());

Optional<User> existingUserOpt = userService.findByGoogleId(userDto.getGoogleId());

User user = existingUserOpt.orElseGet(() -> {
return userService.save(userDto);
});

String jwt = jwtComponent.createAccessToken(user.getId(), "ROLE_USER", Date.from(user.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()));
String refreshToken = jwtComponent.createRefreshToken(user.getId());
JwtResponseDto jwtResponseDto = new JwtResponseDto(jwt, refreshToken);

SuccessResponseDto responseDto = SuccessResponseDto.create(jwtResponseDto);

Optional<User> existingUserOpt = userService.findByGoogleId(userDto.getGoogleId());
return ResponseEntity.ok(responseDto);
}

@PostMapping("/refresh")
@Operation(summary = "JWT κ°±μ‹ ", description = "λ¦¬ν”„λ ˆμ‹œ 토큰을 μ΄μš©ν•˜μ—¬ μƒˆλ‘œμš΄ JWTλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.")
public ResponseEntity<SuccessResponseDto<JwtResponseDto>> refreshJwt(
@RequestBody @Valid RefreshTokenRequestDto refreshTokenRequest
) {
String refreshToken = refreshTokenRequest.refreshToken();

User user = existingUserOpt.orElseGet(() -> {
// μ‹ κ·œ μœ μ €λΌλ©΄ μ €μž₯
return userService.save(userDto);
});
Claims claims = jwtComponent.verifyRefreshToken(refreshToken);

String jwt = jwtComponent.create(user.getId(), "ROLE_USER");
JwtDto jwtDto = new JwtDto(jwt);
Long userId = claims.getId();
UserDto userDto = userService.findUserById(userId);

SuccessResponseDto responseDto = SuccessResponseDto.create(jwtDto);
Date userCreatedAt = TimeUtils.toDate(userDto.getCreatedAt());
String newJwt = jwtComponent.createAccessToken(userId, "ROLE_USER", userCreatedAt);
String newRefreshToken = jwtComponent.createRefreshToken(userId);

return ResponseEntity.ok(responseDto);
JwtResponseDto jwtResponseDto = new JwtResponseDto(newJwt, newRefreshToken);
SuccessResponseDto responseDto = SuccessResponseDto.create(jwtResponseDto);

return ResponseEntity.ok(responseDto);
}
}
}
20 changes: 15 additions & 5 deletions src/main/java/app/cardcapture/auth/jwt/config/JwtConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;
import java.util.Date;

@Configuration
Expand All @@ -17,13 +18,22 @@
public class JwtConfig {
private String issuer;
private String secret;
private Long expirationInSeconds;
private Long accessExpirationInSeconds;
private Long refreshExpirationInSeconds;

public long getExpirationMillis(long current) {
return current + expirationInSeconds * 1000L;
public long getAccessExpirationMillis(long current) {
return current + accessExpirationInSeconds * 1000L;
}

public Date getExpirationDate(Date current) {
return new Date(getExpirationMillis(current.getTime()));
public Date getAccessExpirationDate(Date current) {
return new Date(getAccessExpirationMillis(current.getTime()));
}

public LocalDateTime getAccessExpirationDate(LocalDateTime current) {
return current.plusSeconds(accessExpirationInSeconds.intValue());
}

public Date getRefreshExpirationDate(Date current) {
return new Date(current.getTime() + refreshExpirationInSeconds * 1000L);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package app.cardcapture.auth.jwt.controller;

import app.cardcapture.auth.jwt.service.TokenBlacklistService;
import app.cardcapture.common.dto.SuccessResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Tag(name = "token", description = "The token API")
@RequestMapping("/api/v1/token")
@AllArgsConstructor
public class TokenController {

private final TokenBlacklistService tokenBlacklistService;

@GetMapping("/logout")
@Operation(summary = "λ‘œκ·Έμ•„μ›ƒ", description = "JWT 토큰을 λΈ”λž™λ¦¬μŠ€νŠΈμ— μΆ”κ°€ν•˜μ—¬ λ‘œκ·Έμ•„μ›ƒ μ²˜λ¦¬ν•©λ‹ˆλ‹€.")
public ResponseEntity<SuccessResponseDto<String>> logout(
@RequestHeader("Authorization") String authHeader
) {
tokenBlacklistService.addToBlacklist(authHeader);
return ResponseEntity.ok(SuccessResponseDto.create("λ‘œκ·Έμ•„μ›ƒ 성곡"));
}
}
13 changes: 11 additions & 2 deletions src/main/java/app/cardcapture/auth/jwt/domain/Claims.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package app.cardcapture.auth.jwt.domain;

import app.cardcapture.common.utils.TimeUtils;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.Date;

@Getter
public class Claims {
public class Claims { //TODO: Access Token, Refresh Token 용 Claims κ΅¬λΆ„ν•˜κΈ°(NullpointerException 방지)
private Long id;
private String[] roles;
private Date issuedAt;
private Date expiresAt;
private String issuer;
private Date createdAt;

private Claims() {}

Expand All @@ -21,17 +24,23 @@ public Claims(DecodedJWT decodedJWT) {
this.issuedAt = decodedJWT.getIssuedAt();
this.expiresAt = decodedJWT.getExpiresAt();
this.issuer = decodedJWT.getIssuer();
this.createdAt = decodedJWT.getClaim("created_at").asDate();
}

public static Claims of(Long id, String role, String issuer) {
public static Claims of(Long id, String role, String issuer, Date createdAt) {
Claims claims = new Claims();

claims.id = id;
claims.roles = new String[]{role};
claims.issuedAt = new Date();
claims.expiresAt = null;
claims.issuer = issuer;
claims.createdAt = createdAt;

return claims;
}

public LocalDateTime getLoalDateCreatedAt() {
return TimeUtils.toLocalDateTime(createdAt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package app.cardcapture.auth.jwt.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

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

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

@Column(nullable = false)
@CreatedDate
private LocalDateTime createdAt;

@Column(nullable = false)
private LocalDateTime expiryDate;
} // TODO: 배치둜 만료된 토큰 μ‚­μ œν•˜κΈ°
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package app.cardcapture.auth.jwt.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class JwtAuthorizationDto {
@NotBlank
private String aceessToken;
}
public record JwtAuthorizationDto(
@NotBlank String aceessToken
) {
}
12 changes: 0 additions & 12 deletions src/main/java/app/cardcapture/auth/jwt/dto/JwtDto.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.cardcapture.auth.jwt.dto;

import jakarta.validation.constraints.NotBlank;

public record JwtResponseDto(
@NotBlank String accessToken,
@NotBlank String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.cardcapture.auth.jwt.dto;

import jakarta.validation.constraints.NotBlank;

public record RefreshTokenRequestDto(
@NotBlank String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app.cardcapture.auth.jwt.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.FORBIDDEN)
public class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package app.cardcapture.auth.jwt.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class TokenBlacklistedException extends RuntimeException {
public TokenBlacklistedException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
authHeader = authHeader.substring(7);
}
String authToken = authHeader;
Claims claims = jwtComponent.verify(authToken);
Claims claims = jwtComponent.verifyAccessToken(authToken);
if (claims != null) {
Long userId = claims.getId();
if (userId != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.cardcapture.auth.jwt.repository;

import app.cardcapture.auth.jwt.domain.entity.TokenBlacklist;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TokenBlacklistRepository extends JpaRepository<TokenBlacklist, Long> {
boolean existsByToken(String token);
}
Loading

0 comments on commit 7138444

Please sign in to comment.