Skip to content

Commit

Permalink
Merge pull request #67 from mju-likelion/feature/refreshtoken-#66
Browse files Browse the repository at this point in the history
Feature/#66 refreshtoken 추가
  • Loading branch information
GahBaek authored Jul 30, 2024
2 parents 84c655b + d184187 commit aba2b60
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@

import com.example.mutsideout_mju.exception.UnauthorizedException;
import com.example.mutsideout_mju.exception.errorCode.ErrorCode;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Date;
@Slf4j
@Component
Expand Down Expand Up @@ -52,4 +51,32 @@ public String getPayload(final String token) {
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN, e.getMessage());
}
}
}

public String createRefreshToken() {
Date now = new Date();
Date validity = new Date(now.getTime() + Duration.ofDays(7).toMillis());

return Jwts.builder()
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}

public boolean isTokenExpired(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();

return claims.getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
} catch (JwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN, e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class AuthenticationConfig implements WebMvcConfigurer {
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor)
.addPathPatterns("/planners/**", "/diaries/**", "/rooms/**", "/users/**", "/surveys", "/auth/logout")
.excludePathPatterns("/auth/signup", "/auth/login");
.excludePathPatterns("/auth/signup", "/auth/login", "/auth/refresh");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import com.example.mutsideout_mju.dto.request.auth.SignupDto;
import com.example.mutsideout_mju.dto.response.ResponseDto;
import com.example.mutsideout_mju.dto.response.token.TokenResponseDto;
import com.example.mutsideout_mju.exception.UnauthorizedException;
import com.example.mutsideout_mju.exception.errorCode.ErrorCode;
import com.example.mutsideout_mju.service.AuthService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
Expand Down Expand Up @@ -38,28 +41,82 @@ public ResponseEntity<ResponseDto<Void>> signup(@RequestBody @Valid SignupDto si
public ResponseEntity<ResponseDto<Void>> login(@RequestBody @Valid LoginDto loginDto, HttpServletResponse response) {
TokenResponseDto tokenResponseDto = authService.login(loginDto);
setCookie(response, JwtEncoder.encode(tokenResponseDto.getAccessToken()));
setCookieForRefreshToken(response, tokenResponseDto.getRefreshToken());
return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "로그인 완료"), HttpStatus.OK);
}

private static void setCookie(HttpServletResponse response, String accessToken) {
ResponseCookie cookie = ResponseCookie.from(AuthenticationExtractor.TOKEN_COOKIE_NAME, accessToken)
.maxAge(Duration.ofMillis(1800000))
.maxAge(Duration.ofMinutes(30))
.path("/")
.httpOnly(true)
.sameSite("None").secure(true)
.build();

response.addHeader("set-cookie", cookie.toString());
}

// 로그아웃
@GetMapping("/logout")
public ResponseEntity<ResponseDto<Void>> logout(final HttpServletResponse response) {
ResponseCookie cookie = ResponseCookie.from("AccessToken", null)
private static void setCookieForRefreshToken(HttpServletResponse response, String refreshToken) {
ResponseCookie cookie_refresh = ResponseCookie.from("RefreshToken", refreshToken)
.maxAge(Duration.ofDays(14))
.path("/")
.httpOnly(true)
.sameSite("None")
.secure(true)
.build();

response.addHeader("set-cookie", cookie_refresh.toString());
}

@GetMapping("/refresh")
public ResponseEntity<ResponseDto<Void>> refresh(HttpServletResponse response, HttpServletRequest request) {
String refreshToken = getRefreshTokenFromCookie(request);
TokenResponseDto tokenResponseDto;
try {
tokenResponseDto = authService.refresh(refreshToken);
} catch (UnauthorizedException e) {
return new ResponseEntity<>(ResponseDto.res(HttpStatus.UNAUTHORIZED, "Refresh token 만료 혹은 부적절"), HttpStatus.UNAUTHORIZED);
}
setCookie(response, JwtEncoder.encode(tokenResponseDto.getAccessToken()));
setCookieForRefreshToken(response, tokenResponseDto.getRefreshToken());

return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "Refresh token 재생성 완료"), HttpStatus.OK);
}

private String getRefreshTokenFromCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("RefreshToken".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
throw new UnauthorizedException(ErrorCode.INVALID_TOKEN);
}

private void clearCookies(HttpServletResponse response) {
ResponseCookie accessCookie = ResponseCookie.from("AccessToken", null)
.maxAge(0)
.path("/")
.build();
response.addHeader("set-cookie", cookie.toString());

ResponseCookie refreshCookie = ResponseCookie.from("RefreshToken", null)
.maxAge(0)
.path("/")
.httpOnly(true)
.sameSite("None")
.secure(true)
.build();

response.addHeader("Set-Cookie", accessCookie.toString());
response.addHeader("Set-Cookie", refreshCookie.toString());
}

// 로그아웃
@GetMapping("/logout")
public ResponseEntity<ResponseDto<Void>> logout(final HttpServletResponse response) {
clearCookies(response);
return new ResponseEntity<>(ResponseDto.res(HttpStatus.OK, "로그아웃 완료"), HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.example.mutsideout_mju.dto.response.token;

import com.example.mutsideout_mju.entity.RefreshToken;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TokenResponseDto {
private String AccessToken;
private String RefreshToken;
}
52 changes: 52 additions & 0 deletions src/main/java/com/example/mutsideout_mju/entity/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.mutsideout_mju.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.annotation.Id;

import java.time.LocalDateTime;
import java.util.UUID;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "refresh_token")
public class RefreshToken {
@jakarta.persistence.Id
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "uuid2")
@Column(updatable = false, unique = true, nullable = false)
private UUID id;

@Column(nullable = false)
private UUID userId;

@Column(nullable = false)
private String token;

@Column(updatable = false)
private LocalDateTime createdAt;

public RefreshToken(UUID id, String refreshTokenValue) {
this.userId = id;
this.token = refreshTokenValue;
}

@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}

public void setId(UUID id) {
this.id = id;
}

public UUID getId() {
return id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum ErrorCode {
INVALID_EMAIL_OR_PASSWORD("4010", "등록되지 않은 이메일 또는 비밀번호를 잘못 입력했습니다."),
COOKIE_NOT_FOUND("4011", "쿠키를 찾을 수 없습니다."),
INVALID_TOKEN("4012", "유효하지 않은 토큰입니다."),
INVALID_REFRESH_TOKEN("4013", "유효하지 않는 refresh 토큰입니다."),

//ForbiddenException
NO_ACCESS("4030", "접근 권한이 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.mutsideout_mju.repository;

import com.example.mutsideout_mju.entity.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
import java.util.UUID;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, UUID> {
Optional<RefreshToken> findByUserId(UUID userId);
RefreshToken findByToken(String refreshToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@
import com.example.mutsideout_mju.dto.request.auth.LoginDto;
import com.example.mutsideout_mju.dto.request.auth.SignupDto;
import com.example.mutsideout_mju.dto.response.token.TokenResponseDto;
import com.example.mutsideout_mju.entity.RefreshToken;
import com.example.mutsideout_mju.entity.User;
import com.example.mutsideout_mju.exception.ConflictException;
import com.example.mutsideout_mju.exception.NotFoundException;
import com.example.mutsideout_mju.exception.UnauthorizedException;
import com.example.mutsideout_mju.exception.errorCode.ErrorCode;
import com.example.mutsideout_mju.repository.RefreshTokenRepository;
import com.example.mutsideout_mju.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class AuthService {
private final PasswordHashEncryption passwordHashEncryption;
private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final JwtTokenProvider jwtTokenProvider;

/**
Expand Down Expand Up @@ -68,14 +73,48 @@ public TokenResponseDto login(LoginDto loginDto) {
return createToken(user);
}

// accessToken, refreshToken 재생성.
public TokenResponseDto refresh(String refreshToken) {
User user = validateRefreshToken(refreshToken);
return createToken(user);
}

// refreshToken 관리
private User validateRefreshToken(String refreshToken) {
RefreshToken storedRefreshToken = refreshTokenRepository.findByToken(refreshToken);

// 저장된 refreshToken이 없는 경우
if (storedRefreshToken == null) {
throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN, " 저장된 RefreshToken이 null 입니다. ");
}

// 저장된 refreshToken이 만료된 경우
if (jwtTokenProvider.isTokenExpired(storedRefreshToken.getToken())) {
throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN, "토큰이 만료됐습니다.");
}

return userRepository.findById(storedRefreshToken.getUserId())
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));
}

private TokenResponseDto createToken(User user) {
String payload = String.valueOf(user.getId());
String accessToken = jwtTokenProvider.createToken(payload);
// refreshToken 생성.
String refreshTokenValue = jwtTokenProvider.createRefreshToken();

return new TokenResponseDto(accessToken);
RefreshToken refreshToken = refreshTokenRepository.findByUserId(user.getId())
.orElse(new RefreshToken(user.getId(), refreshTokenValue));

// refreshToken db에 저장.
refreshToken.setToken(refreshTokenValue);
refreshTokenRepository.save(refreshToken);

return new TokenResponseDto(accessToken, refreshTokenValue);
}

private User findExistingUserByEmail(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new NotFoundException(ErrorCode.INVALID_EMAIL_OR_PASSWORD));
}

}

0 comments on commit aba2b60

Please sign in to comment.