diff --git a/src/main/java/Ness/Backend/domain/auth/AuthController.java b/src/main/java/Ness/Backend/domain/auth/AuthController.java new file mode 100644 index 0000000..e27dc2e --- /dev/null +++ b/src/main/java/Ness/Backend/domain/auth/AuthController.java @@ -0,0 +1,31 @@ +package Ness.Backend.domain.auth; + +import Ness.Backend.domain.auth.dto.request.PostRefreshTokenDto; +import Ness.Backend.domain.auth.dto.response.GetJwtTokenDto; +import Ness.Backend.domain.member.entity.Member; +import Ness.Backend.global.auth.AuthUser; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +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.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping(value = "/auth") +public class AuthController { + private final AuthService authService; + + @PostMapping("/logout") + @Operation(summary = "로그아웃 요청", description = "로그아웃 요청 API 입니다.") + public void logout(@AuthUser Member member, @RequestBody PostRefreshTokenDto postRefreshTokenDto) { + authService.logout(member, postRefreshTokenDto); + } + + @PostMapping("/reIssuance") + @Operation(summary = "JWT access 토큰 재발급 요청", description = "JWT access 토큰 재발급 요청 API 입니다.") + public GetJwtTokenDto reIssuance(@AuthUser Member member, @RequestBody PostRefreshTokenDto postRefreshTokenDto) { + return authService.reIssuance(member, postRefreshTokenDto); + } +} diff --git a/src/main/java/Ness/Backend/domain/auth/AuthService.java b/src/main/java/Ness/Backend/domain/auth/AuthService.java new file mode 100644 index 0000000..d46c58e --- /dev/null +++ b/src/main/java/Ness/Backend/domain/auth/AuthService.java @@ -0,0 +1,53 @@ +package Ness.Backend.domain.auth; + +import Ness.Backend.domain.auth.dto.request.PostRefreshTokenDto; +import Ness.Backend.domain.auth.dto.response.GetJwtTokenDto; +import Ness.Backend.domain.auth.inmemory.RefreshTokenRepository; +import Ness.Backend.domain.auth.jwt.JwtTokenProvider; +import Ness.Backend.domain.member.entity.Member; +import Ness.Backend.global.error.ErrorCode; +import Ness.Backend.global.error.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Date; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + public void logout(Member member, PostRefreshTokenDto postRefreshTokenDto) { + /* refreshToken 만료 여부 확인 */ + if(refreshTokenRepository.findRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()).isEmpty()){ + throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN); + } + + refreshTokenRepository.deleteRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()); + SecurityContextHolder.clearContext(); + } + + @Transactional + public GetJwtTokenDto reIssuance(Member member, PostRefreshTokenDto postRefreshTokenDto) { + /* refreshToken 유효성 확인 */ + if (!jwtTokenProvider.validRefreshToken(postRefreshTokenDto.getJwtRefreshToken())) { + throw new UnauthorizedException(ErrorCode.INVALID_TOKEN); + } + + /* refreshToken 만료 여부 확인 */ + if(refreshTokenRepository.findRefreshTokenByJwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()).isEmpty()){ + throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN); + } + + final GetJwtTokenDto generateToken = GetJwtTokenDto.builder() + .jwtAccessToken(jwtTokenProvider.generateAccessToken(member.getEmail(), new Date())) + .jwtRefreshToken(postRefreshTokenDto.getJwtRefreshToken()) + .build(); + + return generateToken; + } +} diff --git a/src/main/java/Ness/Backend/domain/auth/dto/LoginRequestDto.java b/src/main/java/Ness/Backend/domain/auth/dto/LoginRequestDto.java deleted file mode 100644 index e5e3004..0000000 --- a/src/main/java/Ness/Backend/domain/auth/dto/LoginRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package Ness.Backend.domain.auth.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class LoginRequestDto { - /* 사용자의 이메일과 비밀번호로 로그인 */ - @Schema(description = "사용자의 이메일 주소", example = "1234@email.com") - private String email; - - @Schema(description = "사용자의 비밀번호", example = "abc123!#") - private String password; -} diff --git a/src/main/java/Ness/Backend/domain/auth/dto/RegisterRequestDto.java b/src/main/java/Ness/Backend/domain/auth/dto/RegisterRequestDto.java deleted file mode 100644 index 8eaf9f5..0000000 --- a/src/main/java/Ness/Backend/domain/auth/dto/RegisterRequestDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package Ness.Backend.domain.auth.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class RegisterRequestDto { - @Schema(description = "사용자의 이메일 주소", example = "1234@email.com") - private String email; - - @Schema(description = "사용자의 비밀번호", example = "abc123!#") - private String password; -} - diff --git a/src/main/java/Ness/Backend/domain/auth/dto/ResourceDto.java b/src/main/java/Ness/Backend/domain/auth/dto/ResourceDto.java deleted file mode 100644 index 3dd5db4..0000000 --- a/src/main/java/Ness/Backend/domain/auth/dto/ResourceDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package Ness.Backend.domain.auth.dto; - -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@EqualsAndHashCode -@NoArgsConstructor -public class ResourceDto { - private String id; - - private String email; - - private String picture; - - private String nickname; - - private String name; - - @Builder - public ResourceDto(String id, String email, String picture, String nickname, String name){ - this.id = id; - this.email = email; - this.picture = picture; - this.nickname = nickname; - this.name = name; - } -} - diff --git a/src/main/java/Ness/Backend/domain/auth/dto/request/PostRefreshTokenDto.java b/src/main/java/Ness/Backend/domain/auth/dto/request/PostRefreshTokenDto.java new file mode 100644 index 0000000..4575a6a --- /dev/null +++ b/src/main/java/Ness/Backend/domain/auth/dto/request/PostRefreshTokenDto.java @@ -0,0 +1,12 @@ +package Ness.Backend.domain.auth.dto.request; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class PostRefreshTokenDto { + @NotNull + private String jwtRefreshToken; +} diff --git a/src/main/java/Ness/Backend/domain/auth/dto/response/GetJwtTokenDto.java b/src/main/java/Ness/Backend/domain/auth/dto/response/GetJwtTokenDto.java new file mode 100644 index 0000000..edce01c --- /dev/null +++ b/src/main/java/Ness/Backend/domain/auth/dto/response/GetJwtTokenDto.java @@ -0,0 +1,21 @@ +package Ness.Backend.domain.auth.dto.response; + + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor +public class GetJwtTokenDto { + private String jwtAccessToken; + private String jwtRefreshToken; + + @Builder + public GetJwtTokenDto(String jwtAccessToken, String jwtRefreshToken) { + this.jwtAccessToken = jwtAccessToken; + this.jwtRefreshToken = jwtRefreshToken; + } +} \ No newline at end of file diff --git a/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenRepository.java b/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenRepository.java index f2ed706..38cd055 100644 --- a/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenRepository.java +++ b/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenRepository.java @@ -3,5 +3,10 @@ import Ness.Backend.domain.auth.inmemory.entity.RefreshToken; import org.springframework.data.repository.CrudRepository; +import java.util.Optional; + public interface RefreshTokenRepository extends CrudRepository { + Optional findRefreshTokenByJwtRefreshToken(String refreshToken); + + void deleteRefreshTokenByJwtRefreshToken(String refreshToken); } \ No newline at end of file diff --git a/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenService.java b/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenService.java index 1ddcdd4..e074ea0 100644 --- a/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenService.java +++ b/src/main/java/Ness/Backend/domain/auth/inmemory/RefreshTokenService.java @@ -5,6 +5,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -19,4 +21,10 @@ public void saveRefreshToken(String refreshToken, String authKey) { .build(); refreshTokenRepository.save(token); } + + @Transactional + public void removeRefreshToken(String refreshToken) { + refreshTokenRepository.findRefreshTokenByJwtRefreshToken(refreshToken) + .ifPresent(token -> refreshTokenRepository.delete(token)); + } } diff --git a/src/main/java/Ness/Backend/domain/auth/inmemory/entity/RefreshToken.java b/src/main/java/Ness/Backend/domain/auth/inmemory/entity/RefreshToken.java index b76fa1a..a9e9633 100644 --- a/src/main/java/Ness/Backend/domain/auth/inmemory/entity/RefreshToken.java +++ b/src/main/java/Ness/Backend/domain/auth/inmemory/entity/RefreshToken.java @@ -10,14 +10,16 @@ @Getter @NoArgsConstructor -@RedisHash(value = "refreshToken") +@RedisHash(value = "refreshToken", timeToLive = 60*60*24*14) public class RefreshToken { /* Redis에 저장해서 RefreshToken이 유효한지 검증 */ @Id @Indexed private String jwtRefreshToken; + // 맴버 이메일로 설정 private String authKey; + //리프레시 토큰의 생명 주기(14일) @TimeToLive private Long ttl; diff --git a/src/main/java/Ness/Backend/domain/auth/jwt/JwtAuthorizationFilter.java b/src/main/java/Ness/Backend/domain/auth/jwt/JwtAuthorizationFilter.java index 17e152d..a99486c 100644 --- a/src/main/java/Ness/Backend/domain/auth/jwt/JwtAuthorizationFilter.java +++ b/src/main/java/Ness/Backend/domain/auth/jwt/JwtAuthorizationFilter.java @@ -67,7 +67,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } catch (TokenExpiredException e){ log.error(e + " EXPIRED_TOKEN"); - //request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN.getCode()); setResponse(response, ErrorCode.EXPIRED_TOKEN); } catch (SignatureVerificationException e){ log.error(e + " INVALID_TOKEN_SIGNATURE"); diff --git a/src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java b/src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java index 62a9223..5bbcb55 100644 --- a/src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java @@ -148,4 +148,27 @@ public Member validJwtToken(String jwtToken){ return tokenMember; } + + public boolean validRefreshToken(String refreshToken){ + /* email 값이 null이 아닌지 확인 */ + String authKey = getAuthKeyClaim(refreshToken); + if (authKey == null){ + return false; + } + + /* JWT_EXPIRATION_TIME이 지나지 않았는지 확인 */ + Date expiresAt = getExpireTimeClaim(refreshToken); + if (!this.validExpiredTime(expiresAt)) { + return false; + } + + /* email 값이 정상적으로 있고, JWT_EXPIRATION_TIME도 지나지 않았다면, + * 해당 토큰의 email 정보를 가진 맴버가 있는지 DB에서 확인 */ + Member tokenMember = memberRepository.findMemberByEmail(authKey); + if (tokenMember == null) { + return false; + } + + return true; + } } diff --git a/src/main/java/Ness/Backend/domain/auth/oAuth/OAuthSuccessHandler.java b/src/main/java/Ness/Backend/domain/auth/oAuth/OAuthSuccessHandler.java index 951be06..f029667 100644 --- a/src/main/java/Ness/Backend/domain/auth/oAuth/OAuthSuccessHandler.java +++ b/src/main/java/Ness/Backend/domain/auth/oAuth/OAuthSuccessHandler.java @@ -1,5 +1,6 @@ package Ness.Backend.domain.auth.oAuth; +import Ness.Backend.domain.auth.inmemory.RefreshTokenService; import Ness.Backend.domain.auth.jwt.JwtTokenProvider; import Ness.Backend.domain.auth.jwt.entity.JwtToken; import Ness.Backend.domain.auth.security.AuthDetails; @@ -10,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; @@ -19,6 +21,7 @@ @RequiredArgsConstructor public class OAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenService refreshTokenService; @Value("${frontend.redirect-url}") private String frontRedirectUrl; @@ -32,11 +35,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo /*인증에 성공한 사용자*/ AuthDetails oAuth2User = (AuthDetails) authentication.getPrincipal(); - /*JwtToken 생성*/ + /* JwtToken 생성*/ JwtToken jwtToken = jwtTokenProvider.generateJwtToken(oAuth2User.getUsername()); - //TODO: RefreshToken update - //refreshTokenService.saveRefreshToken(jwtToken.getJwtRefreshToken(),oAuth2User.getUser().getAuthKey()); + /* RefreshToken 저장하기 */ + refreshTokenService.saveRefreshToken(jwtToken.getJwtRefreshToken(), oAuth2User.getUsername()); /*JwtToken과 함께 리다이렉트*/ String targetUrl = UriComponentsBuilder.fromUriString(setRedirectUrl(request.getServerName())) diff --git a/src/main/java/Ness/Backend/domain/auth/security/SecurityConfig.java b/src/main/java/Ness/Backend/domain/auth/security/SecurityConfig.java index 694205e..c0b862a 100644 --- a/src/main/java/Ness/Backend/domain/auth/security/SecurityConfig.java +++ b/src/main/java/Ness/Backend/domain/auth/security/SecurityConfig.java @@ -42,7 +42,7 @@ public AuthenticationManager authenticationManager() throws Exception { @Bean public OAuthSuccessHandler oAuthSuccessHandler(){ - return new OAuthSuccessHandler(jwtTokenProvider()); + return new OAuthSuccessHandler(jwtTokenProvider(), refreshTokenService); } @Bean diff --git a/src/main/java/Ness/Backend/global/error/ErrorCode.java b/src/main/java/Ness/Backend/global/error/ErrorCode.java index fc3ecb7..544e91e 100644 --- a/src/main/java/Ness/Backend/global/error/ErrorCode.java +++ b/src/main/java/Ness/Backend/global/error/ErrorCode.java @@ -18,9 +18,9 @@ public enum ErrorCode { /* Auth */ UNAUTHORIZED_ACCESS(UNAUTHORIZED, "AUTH000", "권한이 없습니다."), - EXPIRED_TOKEN(BAD_REQUEST, "AUTH001", "만료된 엑세스 토큰입니다."), - INVALID_REFRESH_TOKEN(BAD_REQUEST, "AUTH002", "리프레시 토큰이 유효하지 않습니다."), - MISMATCH_REFRESH_TOKEN(BAD_REQUEST, "AUTH003", "리프레시 토큰의 유저 정보가 일치하지 않습니다."), + EXPIRED_TOKEN(UNAUTHORIZED, "AUTH001", "만료된 엑세스 토큰입니다."), + INVALID_REFRESH_TOKEN(UNAUTHORIZED, "AUTH002", "리프레시 토큰이 유효하지 않습니다."), + MISMATCH_REFRESH_TOKEN(UNAUTHORIZED, "AUTH003", "리프레시 토큰의 유저 정보가 일치하지 않습니다."), INVALID_AUTH_TOKEN(UNAUTHORIZED, "AUTH004", "권한 정보가 없는 토큰입니다."), UNAUTHORIZED_USER(UNAUTHORIZED, "AUTH005", "현재 내 계정 정보가 존재하지 않습니다."), REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "AUTH006", "로그아웃 된 사용자입니다."), diff --git a/src/main/java/Ness/Backend/global/error/exception/UnauthorizedException.java b/src/main/java/Ness/Backend/global/error/exception/UnauthorizedException.java new file mode 100644 index 0000000..ba0474e --- /dev/null +++ b/src/main/java/Ness/Backend/global/error/exception/UnauthorizedException.java @@ -0,0 +1,17 @@ +package Ness.Backend.global.error.exception; + +import Ness.Backend.global.error.ErrorCode; +import lombok.Getter; + +@Getter +public class UnauthorizedException extends BaseException { + public UnauthorizedException() { + super(ErrorCode.UNAUTHORIZED_ACCESS, ErrorCode.UNAUTHORIZED_ACCESS.getMessage()); + } + public UnauthorizedException(String message) { + super(ErrorCode.UNAUTHORIZED_ACCESS, message); + } + public UnauthorizedException(ErrorCode errorCode) { + super(errorCode, errorCode.getMessage()); + } +} \ No newline at end of file diff --git a/src/main/java/Ness/Backend/infra/redis/RedisConfig.java b/src/main/java/Ness/Backend/infra/redis/RedisConfig.java index 8532e27..5e0bc3f 100644 --- a/src/main/java/Ness/Backend/infra/redis/RedisConfig.java +++ b/src/main/java/Ness/Backend/infra/redis/RedisConfig.java @@ -1,5 +1,6 @@ package Ness.Backend.infra.redis; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,6 +14,7 @@ import java.time.Duration; @Configuration +@Slf4j public class RedisConfig { @Value("${spring.data.redis.host}") @@ -35,10 +37,12 @@ public RedisConnectionFactory redisConnectionFactory() { .commandTimeout(Duration.ofSeconds(1)) //No longer than 1 second .shutdownTimeout(Duration.ZERO) //Immediately shutdown after application shutdown .build(); + + log.info("Connected to Redis at {}:{}", host, port); return new LettuceConnectionFactory(redisConfig, clientConfig); } - /* Use redis template for redis*/ + /* Use redis template for redis */ @Bean public RedisTemplate redisTemplate() { /* Most basic configuration */