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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependencies {

// MySQL
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'com.mysql:mysql-connector-j'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
import com.writon.admin.domain.dto.response.auth.LoginResponseDto;
import com.writon.admin.domain.dto.response.auth.ReissueResponseDto;
import com.writon.admin.domain.dto.response.auth.SignUpResponseDto;
import com.writon.admin.domain.dto.wrapper.auth.LoginResponseWrapper;
import com.writon.admin.domain.service.AuthService;
import com.writon.admin.global.config.auth.CookieProvider;
import com.writon.admin.global.response.SuccessDto;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.DeleteMapping;
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;
Expand All @@ -21,33 +28,55 @@
public class AuthController {

private final AuthService authService;
private final CookieProvider cookieProvider;

// ========== 회원가입 API ==========
@PostMapping("/signup")
public SuccessDto<SignUpResponseDto> signup(@RequestBody SignUpRequestDto signUpRequestDto) {
SignUpResponseDto signUpResponseDto = authService.signup(signUpRequestDto);

return new SuccessDto<>(signUpResponseDto);
}


// ========== 로그인 API ==========
@PostMapping("/login")
public SuccessDto<LoginResponseDto> login(@RequestBody LoginRequestDto loginRequestDto) {
LoginResponseDto loginResponseDto = authService.login(loginRequestDto);
public ResponseEntity<SuccessDto<LoginResponseDto>> login(@RequestBody LoginRequestDto loginRequestDto) {
LoginResponseWrapper loginResponseWrapper = authService.login(loginRequestDto);
HttpHeaders cookieHeaders = cookieProvider.createTokenCookie(loginResponseWrapper.getTokenDto());

return new SuccessDto<>(loginResponseDto);
return new SuccessDto<>(loginResponseWrapper.getLoginResponseDto()).toResponseEntity(
cookieHeaders);
}


// ========== 토큰 재발급 API ==========
@PostMapping("/reissue")
public SuccessDto<ReissueResponseDto> reissue(@RequestBody ReissueRequestDto reissueRequestDto) {
ReissueResponseDto reissueResponseDto = authService.reissue(reissueRequestDto);
public ResponseEntity<SuccessDto<Object>> reissue(
@CookieValue(value = "accessToken", required = false) String accessToken,
@CookieValue(value = "refreshToken", required = false) String refreshToken
) {
String newAccessToken = authService.reissue(accessToken, refreshToken);
HttpHeaders cookieHeaders = cookieProvider.createTokenCookie(newAccessToken);

return new SuccessDto<>(reissueResponseDto);
return new SuccessDto<>().toResponseEntity(cookieHeaders);
}


// ========== 로그아웃 API ==========
@DeleteMapping("/logout")
public SuccessDto<Void> logout() {
public ResponseEntity<SuccessDto<Object>> logout() {
authService.logout();
HttpHeaders cookieHeaders = cookieProvider.removeTokenCookie();

return new SuccessDto<>();
return new SuccessDto<>().toResponseEntity(cookieHeaders);
}


// ========== 토큰 유효성 검사 API ==========
@GetMapping("/check")
public SuccessDto<Void> tokenCheck() {

return new SuccessDto<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,10 @@ public SuccessDto<List<String>> editPositions(
private void deleteImage() {
Organization organization = tokenUtil.getOrganization();
String imageUrl = organization.getLogo();
System.out.println("deleteImage 함수 실행" + imageUrl);

if (!Objects.equals(imageUrl, "") &&
!Objects.equals(imageUrl, DEFAULT_LOGO_URL)) {
imageService.deleteImage(imageUrl);
System.out.println("deleteImage 실행" + imageUrl);

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@
@Getter
@AllArgsConstructor
public class LoginResponseDto {

private String accessToken;
private String refreshToken;
private boolean hasOrganization;
private Long organizationId; // nullable
private String organizationName; // nullable
private String themeColor; // nullable
private String organizationLogo; // nullable
private List<ChallengeResponse> challengeList;

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.writon.admin.domain.dto.response.auth;

import com.writon.admin.global.config.auth.TokenDto;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ReissueResponseDto {

private String accessToken;
private String refreshToken;
private TokenDto tokenDto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.writon.admin.domain.dto.wrapper.auth;

import com.writon.admin.domain.dto.response.auth.LoginResponseDto;
import com.writon.admin.global.config.auth.TokenDto;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginResponseWrapper {
private TokenDto tokenDto;
private LoginResponseDto loginResponseDto;
}
26 changes: 13 additions & 13 deletions src/main/java/com/writon/admin/domain/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.writon.admin.domain.dto.response.auth.LoginResponseDto;
import com.writon.admin.domain.dto.response.auth.ReissueResponseDto;
import com.writon.admin.domain.dto.response.auth.SignUpResponseDto;
import com.writon.admin.domain.dto.wrapper.auth.LoginResponseWrapper;
import com.writon.admin.domain.entity.challenge.Challenge;
import com.writon.admin.domain.entity.lcoal.ChallengeResponse;
import com.writon.admin.domain.entity.organization.AdminUser;
Expand All @@ -17,6 +18,7 @@
import com.writon.admin.global.config.auth.TokenProvider;
import com.writon.admin.global.error.CustomException;
import com.writon.admin.global.error.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -59,7 +61,7 @@ public SignUpResponseDto signup(SignUpRequestDto signUpRequestDto) {
}

// ========== Login API ==========
public LoginResponseDto login(LoginRequestDto loginRequestDto) {
public LoginResponseWrapper login(LoginRequestDto loginRequestDto) {

// 1. Login ID/PW 를 기반으로 AuthenticationToken 생성
UsernamePasswordAuthenticationToken authenticationToken = loginRequestDto.toAuthentication();
Expand Down Expand Up @@ -103,37 +105,35 @@ public LoginResponseDto login(LoginRequestDto loginRequestDto) {
.map(entity -> new ChallengeResponse(entity.getId(), entity.getName()))
.toList();

// 8. Response 전달
return new LoginResponseDto(
tokenDto.getAccessToken(),
tokenDto.getRefreshToken(),
LoginResponseDto loginResponseDto = new LoginResponseDto(
organization.isPresent(),
organization.map(Organization::getId).orElse(null),
organization.map(Organization::getName).orElse(null),
organization.map(Organization::getThemeColor).orElse(null),
organization.map(Organization::getLogo).orElse(null),
challengeList
);

// 8. Response 전달
return new LoginResponseWrapper(tokenDto, loginResponseDto);
}

// ========== Reissue API ==========
public ReissueResponseDto reissue(ReissueRequestDto reissueRequestDto) {
public String reissue(String accessToken, String refreshToken) {

// 1. Access Token 에서 identifier 가져오기
String identifier = tokenProvider.getIdentifier(reissueRequestDto.getAccessToken()).getSubject();
String identifier = tokenProvider.getIdentifier(accessToken)
.getSubject();

// 2. Refresh Token 일치여부 확인
String refreshToken = refreshTokenService.getRefreshToken(identifier);
String storedRefreshToken = refreshTokenService.getRefreshToken(identifier);

if (refreshToken == null || !refreshToken.equals(reissueRequestDto.getRefreshToken())) {
if (refreshToken == null || !refreshToken.equals(storedRefreshToken)) {
throw new CustomException(ErrorCode.REFRESH_TOKEN_INCONSISTENCY);
}

// 3. 새로운 Access Token 생성
String accessToken = tokenProvider.createAccessToken(identifier);

// 4. 토큰 발급
return new ReissueResponseDto(accessToken, refreshToken);
return tokenProvider.createAccessToken(identifier);
}

// ========== Logout API ==========
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@AllArgsConstructor
public class RefreshTokenService {
private final RedisTemplate<String, Object> redisTemplate;
private final Long REFRESH_TOKEN_EXPIRE_TIME = 60 * 2L; // TTL: 2시간
private final Long REFRESH_TOKEN_EXPIRE_TIME = 60 * 24L; // TTL: 1일(24시간)

public void saveRefreshToken(String email, String refreshToken) {
redisTemplate.opsForValue().set(email, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MINUTES); // 이메일을 key로 저장
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.writon.admin.global.config.auth;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class CookieProvider {

private static final long ACCESS_TOKEN_MAX_AGE = 60 * (60 + 30) ; // 1시간 30분 (단위: s)
private static final long REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24; // 1일 (단위: s)

// AT, RT 쿠키헤더를 생성하는 메서드
public HttpHeaders createTokenCookie(TokenDto tokenDto) {
ResponseCookie accessTokenCookie = createCookie(
"accessToken",
tokenDto.getAccessToken(),
ACCESS_TOKEN_MAX_AGE
);
ResponseCookie refreshTokenCookie = createCookie(
"refreshToken",
tokenDto.getRefreshToken(),
REFRESH_TOKEN_MAX_AGE
);

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString());
headers.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());

return headers;
}


// AT 쿠키헤더만 생성하는 메서드
public HttpHeaders createTokenCookie(String accessToken) {
ResponseCookie accessTokenCookie = createCookie(
"accessToken",
accessToken,
ACCESS_TOKEN_MAX_AGE
);

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString());

return headers;
}


// AT, RT 쿠키헤더를 제거하는 메서드
public HttpHeaders removeTokenCookie() {
ResponseCookie accessTokenCookie = createCookie(
"accessToken",
"",
0
);
ResponseCookie refreshTokenCookie = createCookie(
"refreshToken",
"",
0
);

HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.SET_COOKIE, accessTokenCookie.toString());
headers.add(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());

return headers;
}


// Cookie를 생성하는 메서드
private ResponseCookie createCookie(String name, String value, long maxAge) {
return ResponseCookie.from(name, value)
.httpOnly(true) // HttpOnly 속성 적용
.secure(true) // HTTPS에서만 전송 (로컬 테스트 시 false 설정)
.path("/") // 모든 경로에서 사용 가능하도록 설정
.sameSite("Strict") // CSRF 방지
.maxAge(maxAge) // 쿠키 만료 시간 설정
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public void commence(
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
// System.out.println("JwtAuthenticationEntryPoint");
String exception = (String) request.getAttribute("exception");
ErrorCode errorCode = ErrorCode.UNAUTHORIZED;

Expand All @@ -33,6 +32,10 @@ public void commence(
errorCode = ErrorCode.ACCESS_TOKEN_EXPIRATION;
}

if (exception.equals(ErrorCode.ACCESS_TOKEN_NOT_FOUND.getCode())) {
errorCode = ErrorCode.ACCESS_TOKEN_NOT_FOUND;
}

if (exception.equals(ErrorCode.REFRESH_TOKEN_EXPIRATION.getCode())) {
errorCode = ErrorCode.REFRESH_TOKEN_EXPIRATION;
}
Expand Down
Loading