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
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import DiffLens.back_end.domain.library.dto.LibraryResponseDTO;
import DiffLens.back_end.global.responses.exception.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "라이브러리 API")
@RestController
@RequestMapping("/libraries")
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ public abstract class AbstractSocialAuthStrategy implements AuthStrategy {
protected final JwtTokenProvider jwtTokenProvider;

@Override
public Boolean signUp(AuthRequestDTO.SignUpDto request) {
public Boolean signUp(AuthRequestDTO.SignUp request) {
// 소셜 로그인은 보통 signUp 단독 호출 필요 없음
return true;
}

@Override
public TokenResponseDTO login(Object request) {

AuthRequestDTO.SocialLoginDto body = (AuthRequestDTO.SocialLoginDto) request;
AuthRequestDTO.SocialLogin body = (AuthRequestDTO.SocialLogin) request;
String code = body.getCode();
String decoded = URLDecoder.decode(code, StandardCharsets.UTF_8);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class AuthGeneralStrategy implements AuthStrategy {
private final JwtTokenProvider jwtTokenProvider;

@Override
public Boolean signUp(AuthRequestDTO.SignUpDto request) {
public Boolean signUp(AuthRequestDTO.SignUp request) {

// 요청 정보 추출
String email = request.getEmail();
Expand All @@ -49,7 +49,7 @@ public Boolean signUp(AuthRequestDTO.SignUpDto request) {
@Override
public TokenResponseDTO login(Object request) {

AuthRequestDTO.LoginDto body = (AuthRequestDTO.LoginDto) request;
AuthRequestDTO.Login body = (AuthRequestDTO.Login) request;

// 유저 가져옴
Member member = memberRepository.findByEmail(body.getEmail())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface AuthStrategy {
* @param request 회원가입 시 클라이언트에서 보내는 Request Body
* @return 회원가입 성공 여부
*/
public Boolean signUp(AuthRequestDTO.SignUpDto request);
public Boolean signUp(AuthRequestDTO.SignUp request);


/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
import DiffLens.back_end.global.responses.exception.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
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;
import org.springframework.web.bind.annotation.*;

@Tag(name = "로그인 관련 API")
@Tag(name = "인증 API")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
Expand All @@ -35,7 +33,7 @@ public class AuthController {
로그인 성공 여부를 반환합니다.

""")
public ApiResponse<AuthResponseDTO.SignUpDto> signUp(@RequestBody @Valid AuthRequestDTO.SignUpDto request){
public ApiResponse<AuthResponseDTO.SignUpDto> signUp(@RequestBody @Valid AuthRequestDTO.SignUp request){
AuthResponseDTO.SignUpDto signUpDto = authService.signUp(request);
return ApiResponse.onSuccess(signUpDto);
}
Expand All @@ -55,7 +53,7 @@ public ApiResponse<AuthResponseDTO.SignUpDto> signUp(@RequestBody @Valid AuthReq
인증에 필요한 토큰정보를 포함합니다.

""")
public ApiResponse<AuthResponseDTO.LoginDto> localLogin(@RequestBody @Valid AuthRequestDTO.LoginDto request){
public ApiResponse<AuthResponseDTO.LoginDto> localLogin(@RequestBody @Valid AuthRequestDTO.Login request){
AuthResponseDTO.LoginDto login = authService.login(request);
return ApiResponse.onSuccess(login);
}
Expand All @@ -80,9 +78,33 @@ public ApiResponse<AuthResponseDTO.LoginDto> localLogin(@RequestBody @Valid Auth


""")
public ApiResponse<AuthResponseDTO.LoginDto> googleLogin(@RequestBody @Valid AuthRequestDTO.SocialLoginDto request){
public ApiResponse<AuthResponseDTO.LoginDto> googleLogin(@RequestBody @Valid AuthRequestDTO.SocialLogin request){
AuthResponseDTO.LoginDto login = authService.login(request);
return ApiResponse.onSuccess(login);
}

@PostMapping("/reissue")
@Operation(summary = "인증 토큰 재발급",
description = """

## 개요
기존 토큰 만료 시 해당 API를 호출하여 새로운 인증토큰을 발급 받습니다.<br>
재발급 시 기존 토큰은 만료됩니다.

## Request Header
Authorization 헤더에 refresh token 을 담아 호출하세요.<br>
ex) Bearer eyJhbGciOi...re7neDrYl9gJM6c

## 응답
AccessToken과 RefreshToken 이 반환됩니다.
- 재발급 시 기존 AccessToken은 사용 불가능합니다.
- Refresh Token은 계정마다 14일 유효합니다.

""")
public ApiResponse<AuthResponseDTO.LoginDto> reIssue(HttpServletRequest request) {
AuthResponseDTO.LoginDto loginDto = authService.reIssue(request);
return ApiResponse.onSuccess(loginDto);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class AuthRequestDTO {
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class SignUpDto {
public static class SignUp {

@Schema(description = "email 형식으로 입력해야 합니다.")
@NotBlank(message = "이메일은 필수 입력 항목입니다.")
Expand All @@ -42,25 +42,28 @@ public static class SignUpDto {
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class LoginDto implements LoginRequest {
public static class Login implements LoginRequest {

@NotBlank(message = "이메일은 필수 입력 항목입니다.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
@Schema(description = "이메일", example = "[email protected]")
@NotBlank
@Email
private String email;

@NotBlank(message = "비밀번호는 필수 입력 항목입니다.")
@Size(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하로 입력해주세요.")
@Schema(description = "비밀번호", example = "password123")
@NotBlank
@Size(min = 8, max = 20)
private String password;

@NotNull(message = "로그인 타입은 필수 항목입니다.")
@Schema(description = "로그인 타입", example = "GENERAL")
@NotNull
private LoginType loginType;

}


@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class SocialLoginDto implements LoginRequest {
public static class SocialLogin implements LoginRequest {

@NotBlank(message = "code 는 필수 입력 항목입니다.")
private String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package DiffLens.back_end.domain.members.dto.auth;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -13,7 +14,9 @@ public class AuthResponseDTO {
@NoArgsConstructor
public static class LoginDto{

@JsonProperty(value = "access_token")
private String accessToken;
@JsonProperty(value = "refresh_token")
private String refreshToken;

}
Expand All @@ -24,8 +27,20 @@ public static class LoginDto{
@NoArgsConstructor
public static class SignUpDto{

@JsonProperty(value = "is_success")
private Boolean isSuccess;

}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ReIssue{

@JsonProperty(value = "access_token")
private String accessToken;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package DiffLens.back_end.domain.members.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class AccessTokenRepository {

private final StringRedisTemplate redisTemplate;

private final String ACCESS_TOKEN_KEY = "accessToken:";
private final String MEMBER_ACCESS_TOKEN_KEY = "memberAccessToken:";

public void saveToken(Long memberId, String accessToken, long expirationMillis) {
// 기존 memberAccessToken 삭제
String oldToken = redisTemplate.opsForValue().get(MEMBER_ACCESS_TOKEN_KEY + memberId);
if (oldToken != null) {
redisTemplate.delete(ACCESS_TOKEN_KEY + oldToken);
}

// 새 토큰 저장
redisTemplate.opsForValue().set(ACCESS_TOKEN_KEY + accessToken, memberId.toString(), expirationMillis, TimeUnit.MILLISECONDS);
redisTemplate.opsForValue().set(MEMBER_ACCESS_TOKEN_KEY + memberId, accessToken, expirationMillis, TimeUnit.MILLISECONDS);
}

public Optional<String> getCurrentToken(Long memberId) {
return Optional.ofNullable(redisTemplate.opsForValue().get(MEMBER_ACCESS_TOKEN_KEY + memberId));
}

public void deleteToken(String accessToken) {
String memberId = redisTemplate.opsForValue().get(ACCESS_TOKEN_KEY + accessToken);
if (memberId != null) {
redisTemplate.delete(MEMBER_ACCESS_TOKEN_KEY + memberId);
}
redisTemplate.delete(ACCESS_TOKEN_KEY + accessToken);
}

public boolean exists(String accessToken) {
return redisTemplate.hasKey(ACCESS_TOKEN_KEY + accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Repository
public class RefreshTokenRepository {

private final String REFRESH_TOKEN_REDIS_KEY = "refreshToken: ";
private final String REFRESH_TOKEN_REDIS_KEY = "refreshToken:";

private final StringRedisTemplate redisTemplate;

Expand All @@ -18,18 +19,17 @@ public RefreshTokenRepository(StringRedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}

public void saveToken(Long memberId, String refreshToken, long expiration){
String key = REFRESH_TOKEN_REDIS_KEY + refreshToken;
redisTemplate
.opsForValue()
.set(key, memberId.toString(), expiration / 1000, TimeUnit.SECONDS);
public void saveToken(Long memberId, String refreshToken, long expiration) {
String key = REFRESH_TOKEN_REDIS_KEY + refreshToken;
redisTemplate.opsForValue().set(key, memberId.toString(), expiration / 1000, TimeUnit.SECONDS);
redisTemplate.opsForValue().set("member:" + memberId + ":refresh", refreshToken, expiration / 1000, TimeUnit.SECONDS);
}

// RefreshToken 으로 memberId 가져오기
public Long getMemberIdByToken(String token){
public Optional<Long> getMemberIdByToken(String token) {
String key = REFRESH_TOKEN_REDIS_KEY + token;
String memberId = redisTemplate.opsForValue().get(key);
return memberId != null ? Long.parseLong(memberId) : null;
return memberId != null ? Optional.of(Long.parseLong(memberId)) : Optional.empty();
}

// RefreshToken 삭제
Expand All @@ -43,4 +43,38 @@ public boolean existsRefreshToken(String token){
return redisTemplate.hasKey(key);
}

// memberId로 Redis에 저장된 토큰 조회 (단순 스캔, 소규모 시스템에서만 추천)
public String getTokenByMemberId(Long memberId) {
for (String key : redisTemplate.keys(REFRESH_TOKEN_REDIS_KEY + "*")) {
String value = redisTemplate.opsForValue().get(key);
if (value != null && value.equals(memberId.toString())) {
return key.replace(REFRESH_TOKEN_REDIS_KEY, "");
}
}
return null;
}

// memberId로 Redis에 저장된 토큰의 남은 만료시간 반환
public Optional<Long> getExpirationByMemberId(Long memberId) {
String token = getTokenByMemberId(memberId);
if (token == null) return Optional.empty();
Long expiration = getExpirationByToken(token);
return Optional.of(expiration);
}

public Long getExpirationByToken(String token){

String key = REFRESH_TOKEN_REDIS_KEY + token;
return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
}

public void deleteByMemberId(Long memberId) {
String refreshToken = redisTemplate.opsForValue().get("member:" + memberId + ":refresh");
if (refreshToken != null) {
redisTemplate.delete(REFRESH_TOKEN_REDIS_KEY + refreshToken);
redisTemplate.delete("member:" + memberId + ":refresh");
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@
import DiffLens.back_end.domain.members.dto.auth.AuthResponseDTO;
import DiffLens.back_end.domain.members.dto.auth.TokenResponseDTO;
import DiffLens.back_end.domain.members.enums.LoginType;
import DiffLens.back_end.global.responses.code.status.error.AuthStatus;
import DiffLens.back_end.global.responses.exception.handler.ErrorHandler;
import DiffLens.back_end.global.security.JwtTokenProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* 로그인과 회원가입 처리
*/
@Service
@RequiredArgsConstructor
public class AuthService {

private final StrategyFactory strategyFactory;
private final JwtTokenProvider jwtTokenProvider;

@Transactional
public AuthResponseDTO.SignUpDto signUp(AuthRequestDTO.SignUpDto request) {
public AuthResponseDTO.SignUpDto signUp(AuthRequestDTO.SignUp request) {

// 요청 정보에서 loginType 추출
LoginType loginType = request.getLoginType();
Expand Down Expand Up @@ -51,5 +59,29 @@ public <T extends AuthRequestDTO.LoginRequest> AuthResponseDTO.LoginDto login(T
.build();
}

// 토큰 재발급
@Transactional
public AuthResponseDTO.LoginDto reIssue(HttpServletRequest request) {

// 헤더 추출
String header = request.getHeader("Authorization");

// 헤더 없으면
if (header == null)
throw new ErrorHandler(AuthStatus.TOKEN_NOT_FOUND);

// 헤더에서 토큰 추출
String refreshToken = header.substring(7);

// 재발급
TokenResponseDTO tokenDTO = jwtTokenProvider.reissueTokens(refreshToken);

// 응답 반환
return AuthResponseDTO.LoginDto.builder()
.accessToken(tokenDTO.getAccessToken())
.refreshToken(tokenDTO.getRefreshToken())
.build();
}


}
Loading
Loading