Skip to content

Commit 599ec8d

Browse files
authored
Merge pull request #137 from TaskFlow-CLAP/CLAP-144
CLAP-144 로그아웃 API 및 토큰 블랙리스트 로직 구현
2 parents c184f9f + 0cf3f57 commit 599ec8d

18 files changed

Lines changed: 230 additions & 49 deletions

File tree

src/main/java/clap/server/adapter/inbound/security/filter/JwtAuthenticationFilter.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import clap.server.adapter.outbound.jwt.JwtClaims;
44
import clap.server.adapter.outbound.jwt.access.AccessTokenClaimKeys;
5+
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
56
import clap.server.application.port.outbound.auth.JwtProvider;
67
import clap.server.exception.JwtException;
78
import clap.server.exception.code.AuthErrorCode;
@@ -27,7 +28,6 @@
2728

2829
import java.io.IOException;
2930

30-
// 요청에서 JWT 토큰을 추출하고 유효성을 검사합니다.
3131
@Slf4j
3232
@Component
3333
@RequiredArgsConstructor
@@ -37,6 +37,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
3737
private final JwtProvider accessTokenProvider;
3838
private final JwtProvider temporaryTokenProvider;
3939
private final AccessDeniedHandler accessDeniedHandler;
40+
private final ForbiddenTokenPort forbiddenTokenPort;
4041

4142
@Override
4243
protected void doFilterInternal(
@@ -70,15 +71,15 @@ private String resolveAccessToken(
7071
HttpServletRequest request
7172
) throws ServletException {
7273
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
73-
String token = accessTokenProvider.resolveToken(authHeader);
74+
String accessToken = accessTokenProvider.resolveToken(authHeader);
7475

75-
if (!StringUtils.hasText(token)) {
76+
if (!StringUtils.hasText(accessToken)) {
7677
log.error("EMPTY_ACCESS_TOKEN");
7778
handleAuthException(AuthErrorCode.EMPTY_ACCESS_KEY);
7879
}
7980

8081
String requestUrl = request.getRequestURI();
81-
boolean isTemporaryToken = isTemporaryToken(token);
82+
boolean isTemporaryToken = isTemporaryToken(accessToken);
8283
JwtProvider tokenProvider = isTemporaryToken ? temporaryTokenProvider : accessTokenProvider;
8384

8485
log.info("Token is Temporary {}", isTemporaryToken);
@@ -88,14 +89,17 @@ private String resolveAccessToken(
8889
handleAuthException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN);
8990
}
9091

91-
// TODO: 블랙리스트 토큰 처리 로직 추가 필요
92+
if (forbiddenTokenPort.getIsForbidden(accessToken)) {
93+
log.error("FORBIDDEN_ACCESS_TOKEN");
94+
handleAuthException(AuthErrorCode.FORBIDDEN_ACCESS_TOKEN);
95+
}
9296

93-
if (tokenProvider.isTokenExpired(token)) {
97+
if (tokenProvider.isTokenExpired(accessToken)) {
9498
log.error("EXPIRED_TOKEN");
9599
handleAuthException(AuthErrorCode.EXPIRED_TOKEN);
96100
}
97101

98-
return token;
102+
return accessToken;
99103
}
100104

101105

src/main/java/clap/server/adapter/inbound/web/auth/AuthController.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
package clap.server.adapter.inbound.web.auth;
22

3+
import clap.server.adapter.inbound.security.SecurityUserDetails;
34
import clap.server.adapter.inbound.web.dto.auth.LoginRequest;
45
import clap.server.adapter.inbound.web.dto.auth.LoginResponse;
56
import clap.server.application.port.inbound.auth.AuthUsecase;
67
import clap.server.common.annotation.architecture.WebAdapter;
78
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.Parameter;
810
import io.swagger.v3.oas.annotations.tags.Tag;
911
import jakarta.servlet.http.HttpServletRequest;
1012
import lombok.RequiredArgsConstructor;
1113
import lombok.extern.slf4j.Slf4j;
1214
import org.springframework.http.ResponseEntity;
13-
import org.springframework.web.bind.annotation.PostMapping;
14-
import org.springframework.web.bind.annotation.RequestBody;
15-
import org.springframework.web.bind.annotation.RequestHeader;
16-
import org.springframework.web.bind.annotation.RequestMapping;
15+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
16+
import org.springframework.web.bind.annotation.*;
1717

1818
import static clap.server.common.utils.ClientIpParseUtil.getClientIp;
1919

@@ -35,5 +35,13 @@ public ResponseEntity<LoginResponse> login(@RequestHeader(name = "sessionId") St
3535
return ResponseEntity.ok(response);
3636
}
3737

38+
@Operation(summary = "로그아웃 API")
39+
@DeleteMapping("/logout")
40+
public void logout(@AuthenticationPrincipal SecurityUserDetails userInfo,
41+
@Parameter(hidden = true) @RequestHeader(value = "Authorization") String authHeader,
42+
@RequestHeader(value = "refreshToken") String refreshToken) {
43+
String accessToken = authHeader.split(" ")[1];
44+
authUsecase.logout(userInfo.getUserId(), accessToken, refreshToken);
45+
}
3846

3947
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package clap.server.adapter.outbound.infrastructure.redis.forbidden;
2+
3+
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
4+
import clap.server.common.annotation.architecture.InfrastructureAdapter;
5+
import clap.server.domain.model.auth.ForbiddenToken;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
9+
@Slf4j
10+
@InfrastructureAdapter
11+
@RequiredArgsConstructor
12+
public class ForbiddenTokenAdapter implements ForbiddenTokenPort {
13+
private final ForbiddenTokenRepository forbiddenTokenRepository;
14+
private final ForbiddenTokenMapper forbiddenTokenMapper;
15+
16+
public void save(ForbiddenToken forbiddenToken) {
17+
forbiddenTokenRepository.save(forbiddenTokenMapper.toEntity(forbiddenToken));
18+
}
19+
20+
public boolean getIsForbidden(String accessToken) {
21+
return forbiddenTokenRepository.existsById(accessToken);
22+
}
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package clap.server.adapter.outbound.infrastructure.redis.forbidden;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import org.springframework.data.annotation.Id;
6+
import org.springframework.data.redis.core.RedisHash;
7+
import org.springframework.data.redis.core.TimeToLive;
8+
9+
@Getter
10+
@RedisHash("forbiddenToken")
11+
public class ForbiddenTokenEntity {
12+
@Id
13+
private final String accessToken;
14+
private final Long userId;
15+
16+
@TimeToLive
17+
private final long ttl;
18+
19+
@Builder
20+
private ForbiddenTokenEntity(String accessToken, Long userId, long ttl) {
21+
this.accessToken = accessToken;
22+
this.userId = userId;
23+
this.ttl = ttl;
24+
}
25+
26+
public static ForbiddenTokenEntity of(String accessToken, Long userId, long ttl) {
27+
return new ForbiddenTokenEntity(accessToken, userId, ttl);
28+
}
29+
30+
@Override
31+
public boolean equals(Object o) {
32+
if (this == o) return true;
33+
if (!(o instanceof ForbiddenTokenEntity that)) return false;
34+
return accessToken.equals(that.accessToken) && userId.equals(that.userId);
35+
}
36+
37+
@Override
38+
public int hashCode() {
39+
int result = accessToken.hashCode();
40+
result = ((1 << 5) - 1) * result + userId.hashCode();
41+
return result;
42+
}
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package clap.server.adapter.outbound.infrastructure.redis.forbidden;
2+
3+
import clap.server.domain.model.auth.ForbiddenToken;
4+
import org.mapstruct.InheritInverseConfiguration;
5+
import org.mapstruct.Mapper;
6+
7+
@Mapper(componentModel = "spring")
8+
public interface ForbiddenTokenMapper {
9+
@InheritInverseConfiguration
10+
ForbiddenToken toDomain(final ForbiddenTokenEntity entity);
11+
12+
ForbiddenTokenEntity toEntity(final ForbiddenToken domain);
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package clap.server.adapter.outbound.infrastructure.redis.forbidden;
2+
3+
import org.springframework.data.repository.CrudRepository;
4+
5+
public interface ForbiddenTokenRepository extends CrudRepository<ForbiddenTokenEntity, String> {
6+
}

src/main/java/clap/server/adapter/outbound/infrastructure/redis/log/LoginLogAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import clap.server.application.port.outbound.auth.CommandLoginLogPort;
44
import clap.server.application.port.outbound.auth.LoadLoginLogPort;
5+
import clap.server.common.annotation.architecture.InfrastructureAdapter;
56
import clap.server.domain.model.auth.LoginLog;
67
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
8-
import org.springframework.stereotype.Component;
99

1010
import java.util.Optional;
1111

1212
@Slf4j
13-
@Component
13+
@InfrastructureAdapter
1414
@RequiredArgsConstructor
1515
public class LoginLogAdapter implements LoadLoginLogPort, CommandLoginLogPort {
1616
private final LoginLogRepository loginLogRepository;

src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
44
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
5+
import clap.server.common.annotation.architecture.InfrastructureAdapter;
56
import clap.server.domain.model.auth.RefreshToken;
67
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
8-
import org.springframework.stereotype.Component;
99

1010
import java.util.Optional;
1111

1212
@Slf4j
13-
@Component
13+
@InfrastructureAdapter
1414
@RequiredArgsConstructor
1515
public class RefreshTokenAdapter implements CommandRefreshTokenPort, LoadRefreshTokenPort {
1616
private final RefreshTokenRepository refreshTokenRepository;

src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package clap.server.adapter.outbound.infrastructure.s3;
22

33
import clap.server.application.port.outbound.s3.S3UploadPort;
4+
import clap.server.common.annotation.architecture.InfrastructureAdapter;
45
import clap.server.config.s3.KakaoS3Config;
56
import clap.server.domain.model.task.FilePath;
67
import clap.server.exception.S3Exception;
78
import clap.server.exception.code.S3Errorcode;
89
import lombok.RequiredArgsConstructor;
910
import lombok.extern.slf4j.Slf4j;
10-
import org.springframework.stereotype.Service;
1111
import org.springframework.web.multipart.MultipartFile;
1212
import software.amazon.awssdk.services.s3.S3Client;
1313
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
@@ -20,7 +20,7 @@
2020
import java.util.UUID;
2121

2222
@Slf4j
23-
@Service
23+
@InfrastructureAdapter
2424
@RequiredArgsConstructor
2525
public class S3UploadAdapter implements S3UploadPort {
2626
private final KakaoS3Config kakaoS3Config;

src/main/java/clap/server/application/port/inbound/auth/AuthUsecase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
public interface AuthUsecase {
66
LoginResponse login(String nickname, String password, String sessionId, String clientIp);
7+
void logout(Long memberId, String accessToken, String refreshToken);
78
}

0 commit comments

Comments
 (0)