Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import clap.server.adapter.outbound.jwt.JwtClaims;
import clap.server.adapter.outbound.jwt.access.AccessTokenClaimKeys;
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.application.port.outbound.auth.JwtProvider;
import clap.server.exception.JwtException;
import clap.server.exception.code.AuthErrorCode;
Expand All @@ -27,7 +28,6 @@

import java.io.IOException;

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

@Override
protected void doFilterInternal(
Expand Down Expand Up @@ -70,15 +71,15 @@ private String resolveAccessToken(
HttpServletRequest request
) throws ServletException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
String token = accessTokenProvider.resolveToken(authHeader);
String accessToken = accessTokenProvider.resolveToken(authHeader);

if (!StringUtils.hasText(token)) {
if (!StringUtils.hasText(accessToken)) {
log.error("EMPTY_ACCESS_TOKEN");
handleAuthException(AuthErrorCode.EMPTY_ACCESS_KEY);
}

String requestUrl = request.getRequestURI();
boolean isTemporaryToken = isTemporaryToken(token);
boolean isTemporaryToken = isTemporaryToken(accessToken);
JwtProvider tokenProvider = isTemporaryToken ? temporaryTokenProvider : accessTokenProvider;

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

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

if (tokenProvider.isTokenExpired(token)) {
if (tokenProvider.isTokenExpired(accessToken)) {
log.error("EXPIRED_TOKEN");
handleAuthException(AuthErrorCode.EXPIRED_TOKEN);
}

return token;
return accessToken;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package clap.server.adapter.inbound.web.auth;

import clap.server.adapter.inbound.security.SecurityUserDetails;
import clap.server.adapter.inbound.web.dto.auth.LoginRequest;
import clap.server.adapter.inbound.web.dto.auth.LoginResponse;
import clap.server.application.port.inbound.auth.AuthUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

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

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

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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package clap.server.adapter.outbound.infrastructure.redis.forbidden;

import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.ForbiddenToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@InfrastructureAdapter
@RequiredArgsConstructor
public class ForbiddenTokenAdapter implements ForbiddenTokenPort {
private final ForbiddenTokenRepository forbiddenTokenRepository;
private final ForbiddenTokenMapper forbiddenTokenMapper;

public void save(ForbiddenToken forbiddenToken) {
forbiddenTokenRepository.save(forbiddenTokenMapper.toEntity(forbiddenToken));
}

public boolean getIsForbidden(String accessToken) {
return forbiddenTokenRepository.existsById(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package clap.server.adapter.outbound.infrastructure.redis.forbidden;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;

@Getter
@RedisHash("forbiddenToken")
public class ForbiddenTokenEntity {
@Id
private final String accessToken;
private final Long userId;

@TimeToLive
private final long ttl;

@Builder
private ForbiddenTokenEntity(String accessToken, Long userId, long ttl) {
this.accessToken = accessToken;
this.userId = userId;
this.ttl = ttl;
}

public static ForbiddenTokenEntity of(String accessToken, Long userId, long ttl) {
return new ForbiddenTokenEntity(accessToken, userId, ttl);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ForbiddenTokenEntity that)) return false;
return accessToken.equals(that.accessToken) && userId.equals(that.userId);
}

@Override
public int hashCode() {
int result = accessToken.hashCode();
result = ((1 << 5) - 1) * result + userId.hashCode();
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package clap.server.adapter.outbound.infrastructure.redis.forbidden;

import clap.server.domain.model.auth.ForbiddenToken;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface ForbiddenTokenMapper {
@InheritInverseConfiguration
ForbiddenToken toDomain(final ForbiddenTokenEntity entity);

ForbiddenTokenEntity toEntity(final ForbiddenToken domain);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package clap.server.adapter.outbound.infrastructure.redis.forbidden;

import org.springframework.data.repository.CrudRepository;

public interface ForbiddenTokenRepository extends CrudRepository<ForbiddenTokenEntity, String> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import clap.server.application.port.outbound.auth.CommandLoginLogPort;
import clap.server.application.port.outbound.auth.LoadLoginLogPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.LoginLog;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Slf4j
@Component
@InfrastructureAdapter
@RequiredArgsConstructor
public class LoginLogAdapter implements LoadLoginLogPort, CommandLoginLogPort {
private final LoginLogRepository loginLogRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.LoadRefreshTokenPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.domain.model.auth.RefreshToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Slf4j
@Component
@InfrastructureAdapter
@RequiredArgsConstructor
public class RefreshTokenAdapter implements CommandRefreshTokenPort, LoadRefreshTokenPort {
private final RefreshTokenRepository refreshTokenRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package clap.server.adapter.outbound.infrastructure.s3;

import clap.server.application.port.outbound.s3.S3UploadPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.config.s3.KakaoS3Config;
import clap.server.domain.model.task.FilePath;
import clap.server.exception.S3Exception;
import clap.server.exception.code.S3Errorcode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
Expand All @@ -20,7 +20,7 @@
import java.util.UUID;

@Slf4j
@Service
@InfrastructureAdapter
@RequiredArgsConstructor
public class S3UploadAdapter implements S3UploadPort {
private final KakaoS3Config kakaoS3Config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@

public interface AuthUsecase {
LoginResponse login(String nickname, String password, String sessionId, String clientIp);
void logout(Long memberId, String accessToken, String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package clap.server.application.port.outbound.auth;

import clap.server.domain.model.auth.ForbiddenToken;

public interface ForbiddenTokenPort {
void save(ForbiddenToken forbiddenToken);
boolean getIsForbidden(String accessToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus;
import clap.server.application.mapper.response.AuthResponseMapper;
import clap.server.application.port.inbound.auth.AuthUsecase;
import clap.server.application.port.outbound.auth.CommandRefreshTokenPort;
import clap.server.application.port.outbound.auth.ForbiddenTokenPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.auth.CustomJwts;
import clap.server.domain.model.auth.ForbiddenToken;
import clap.server.domain.model.auth.RefreshToken;
import clap.server.domain.model.member.Member;
import clap.server.exception.AuthException;
import clap.server.exception.code.AuthErrorCode;
Expand All @@ -16,34 +18,57 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;

@Slf4j
@ApplicationService
@RequiredArgsConstructor
@Transactional
class AuthService implements AuthUsecase {
private final LoadMemberPort loadMemberPort;
private final CommandRefreshTokenPort commandRefreshTokenPort;
private final IssueTokenService issueTokenService;
private final ManageTokenService manageTokenService;
private final PasswordEncoder passwordEncoder;
private final LoginAttemptService loginAttemptService;
private final RefreshTokenService refreshTokenService;
private final ForbiddenTokenPort forbiddenTokenPort;


@Override
@Transactional
public LoginResponse login(String nickname, String password, String sessionId, String clientIp) {
Member member = getMember(nickname, sessionId, clientIp);

validatePassword(password, member.getPassword(), sessionId, nickname, clientIp);

if (member.getStatus().equals(MemberStatus.APPROVAL_REQUEST)) {
String temporaryToken = issueTokenService.issueTemporaryToken(member.getMemberId());
String temporaryToken = manageTokenService.issueTemporaryToken(member.getMemberId());
return AuthResponseMapper.toLoginResponse(temporaryToken, null, member);
}

CustomJwts jwtTokens = issueTokenService.issueTokens(member);
commandRefreshTokenPort.save(issueTokenService.issueRefreshToken(member.getMemberId()));
CustomJwts jwtTokens = manageTokenService.issueTokens(member);
refreshTokenService.saveRefreshToken(manageTokenService.issueRefreshToken(member.getMemberId()));
loginAttemptService.resetFailedAttempts(sessionId);
return AuthResponseMapper.toLoginResponse(jwtTokens.accessToken(), jwtTokens.refreshToken(), member);
}

@Override
public void logout(Long memberId, String accessToken, String refreshToken) {
RefreshToken refreshTokenFindByMember = refreshTokenService.getRefreshToken(memberId);
refreshTokenService.validateToken(refreshToken, refreshTokenFindByMember);
refreshTokenService.deleteRefreshToken(refreshTokenFindByMember);
deleteAccessToken(memberId, accessToken);
}

private void deleteAccessToken(Long memberId, String accessToken) {
LocalDateTime expiredDate = manageTokenService.getExpiredDate(accessToken);

LocalDateTime now = LocalDateTime.now();
long timeToLive = Duration.between(now, expiredDate).toSeconds();

ForbiddenToken forbiddenToken = ForbiddenToken.of(accessToken, memberId, timeToLive);
forbiddenTokenPort.save(forbiddenToken);
}

private Member getMember(String inputNickname, String sessionId, String clientIp) {
return loadMemberPort.findByNickname(inputNickname).orElseThrow(() ->
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
@RequiredArgsConstructor
@Component
@Slf4j
class IssueTokenService {
class ManageTokenService {
private final JwtProvider accessTokenProvider;
private final JwtProvider refreshTokenProvider;
private final JwtProvider temporaryTokenProvider;
Expand All @@ -35,7 +35,6 @@ public String issueAccessToken(Long memberId) {
return accessTokenProvider.createToken(AccessTokenClaim.of(memberId));
}


public RefreshToken issueRefreshToken(Long memberId) {
String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(memberId));
return RefreshToken.of(
Expand All @@ -57,6 +56,10 @@ public Long resolveRefreshToken(String refreshToken) {
Long::parseLong);
}

public LocalDateTime getExpiredDate(String accessToken) {
return accessTokenProvider.getExpiredDate(accessToken);
}

private long toSeconds(LocalDateTime expiredDate) {
return Duration.between(LocalDateTime.now(), expiredDate).getSeconds();
}
Expand Down
Loading
Loading