Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] #44 TokenRefresh하기~ #48

Merged
merged 7 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/team7/inplace/InplaceApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import team7.inplace.token.persistence.RefreshTokenRepository;

@SpringBootApplication
@ConfigurationPropertiesScan
@EnableJpaRepositories(
basePackages = "team7.inplace",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {RefreshTokenRepository.class}
)
)
public class InplaceApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package team7.inplace.global.exception.code;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@AllArgsConstructor
@Getter
public enum UserErroCode implements ErrorCode {
NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "User is not found");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


private final HttpStatus status;
private final String code;
private final String message;

@Override
public HttpStatus httpStatus() {
return null;
}

@Override
public String code() {
return "";
}

@Override
public String message() {
return "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Getter 로 사용하지 말고 인터페이스에 있는 값 사용 부탁드립니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵!

}
}
17 changes: 17 additions & 0 deletions src/main/java/team7/inplace/security/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package team7.inplace.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
26 changes: 26 additions & 0 deletions src/main/java/team7/inplace/security/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package team7.inplace.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Bean
LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.context.annotation.Configuration;
import team7.inplace.security.handler.CustomSuccessHandler;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.application.RefreshTokenService;
import team7.inplace.user.application.UserService;

@Configuration
Expand All @@ -12,8 +13,9 @@ public class SecurityHandlerConfig {
@Bean
public CustomSuccessHandler customSuccessHandler(
JwtUtil jwtUtil,
UserService userService
UserService userService,
RefreshTokenService refreshTokenService
) {
return new CustomSuccessHandler(jwtUtil, userService);
return new CustomSuccessHandler(jwtUtil, userService, refreshTokenService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ protected void doFilterInternal(
private boolean hasNoTokenCookie(HttpServletRequest request) {
return Optional.ofNullable(request.getCookies())
.flatMap(cookies -> Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals("Authorization"))
.filter(cookie -> cookie.getName().equals("access_token"))
.findAny())
.isEmpty();
}

private Cookie getTokenCookie(HttpServletRequest request) throws InplaceException {
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals("Authorization"))
.filter(cookie -> cookie.getName().equals("access_token"))
.findAny()
.orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import team7.inplace.security.application.dto.CustomOAuth2User;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.application.RefreshTokenService;
import team7.inplace.user.application.UserService;
import team7.inplace.user.application.dto.UserCommand;

public class CustomSuccessHandler implements AuthenticationSuccessHandler {

private final JwtUtil jwtUtil;
private final UserService userService;
private final RefreshTokenService refreshTokenService;

public CustomSuccessHandler(
JwtUtil jwtUtil,
UserService userService
UserService userService, RefreshTokenService refreshTokenService
) {
this.jwtUtil = jwtUtil;
this.userService = userService;
this.refreshTokenService = refreshTokenService;
}

@Override
Expand All @@ -31,19 +34,22 @@ public void onAuthenticationSuccess(
Authentication authentication
) throws IOException {
CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal();
addTokenToResponse(response, customOAuth2User);
UserCommand.Info userInfo = userService.getUserByUsername(customOAuth2User.username());
String accessToken = jwtUtil.createAccessToken(userInfo.username(), userInfo.id(),
userInfo.role().getRoles());
String refreshToken = jwtUtil.createRefreshToken(userInfo.username(), userInfo.id(),
userInfo.role().getRoles());
refreshTokenService.saveRefreshToken(userInfo.username(), refreshToken);
addTokenToResponse(response, accessToken, refreshToken);
setRedirectUrlToResponse(response, customOAuth2User);
}

private void addTokenToResponse(
HttpServletResponse response,
CustomOAuth2User customOAuth2User
String accessToken, String refreshToken
) {
UserCommand.Info user = userService.getUserByUsername(customOAuth2User.username());
Cookie accessTokenCookie = createCookie("access_token",
jwtUtil.createAccessToken(user.username(), user.id(), user.role().getRoles()));
Cookie refreshTokenCookie = createCookie("refresh_token",
jwtUtil.createRefreshToken(user.username(), user.id(), user.role().getRoles()));
Cookie accessTokenCookie = createCookie("access_token", accessToken);
Cookie refreshTokenCookie = createCookie("refresh_token", refreshToken);

response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/team7/inplace/security/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ public String getUsername(String token) throws InplaceException {
}
}

public String getTokenType(String token) throws InplaceException {
public boolean isRefreshToken(String token) throws InplaceException {
try {
return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class);
return jwtParser.parseSignedClaims(token).getPayload().get("tokenType", String.class)
.equals("refreshToken");
} catch (JwtException | IllegalArgumentException e) {
throw InplaceException.of(AuthorizationErrorCode.INVALID_TOKEN);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package team7.inplace.token.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import team7.inplace.global.exception.InplaceException;
import team7.inplace.global.exception.code.AuthorizationErrorCode;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.application.dto.TokenCommand;
import team7.inplace.token.application.dto.TokenCommand.ReIssued;
import team7.inplace.user.application.UserService;
import team7.inplace.user.application.dto.UserCommand;

@Component
@RequiredArgsConstructor
public class RefreshTokenFacade {

private final JwtUtil jwtUtil;
private final RefreshTokenService refreshTokenService;
private final UserService userService;

@Transactional
public ReIssued getReIssuedRefreshTokenCookie(String username, String refreshToken)
throws InplaceException {
if (refreshTokenService.isInvalidRefreshToken(refreshToken)) {
throw InplaceException.of(AuthorizationErrorCode.INVALID_TOKEN);
}

UserCommand.Info userInfo = userService.getUserByUsername(username);
String reIssuedRefreshToken = jwtUtil
.createRefreshToken(userInfo.username(), userInfo.id(), userInfo.role().getRoles());
String reIssuedAccessToken = jwtUtil
.createAccessToken(userInfo.username(), userInfo.id(), userInfo.role().getRoles());
refreshTokenService.saveRefreshToken(username, reIssuedRefreshToken);

return TokenCommand.ReIssued.of(reIssuedRefreshToken, reIssuedAccessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package team7.inplace.token.application;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import team7.inplace.global.exception.InplaceException;
import team7.inplace.global.exception.code.AuthorizationErrorCode;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.domain.RefreshToken;
import team7.inplace.token.persistence.RefreshTokenRepository;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

private final RefreshTokenRepository refreshTokenRepository;
private final JwtUtil jwtUtil;

public boolean isInvalidRefreshToken(String refreshToken) throws InplaceException {
String username = jwtUtil.getUsername(refreshToken);
return !refreshTokenRepository.findById(username).orElseThrow(() ->
InplaceException.of(AuthorizationErrorCode.INVALID_TOKEN))
.getRefreshToken().equals(refreshToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

살짝 읽기 불편한데 orElseThrow 앞에서 줄 바꾸면 안될까요? 아니면 람다식 부분에서
() -> "..." 부분을 한 줄에 하면 좋을 것 같습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인정합니다.

}

public void saveRefreshToken(String username, String token) {
RefreshToken refreshToken = new RefreshToken(username, token);
refreshTokenRepository.save(refreshToken);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package team7.inplace.token.application.dto;

public class TokenCommand {

public record ReIssued(
String accessToken,
String refreshToken
) {

public static ReIssued of(String reIssuedRefreshToken, String reIssuedAccessToken) {
return new ReIssued(reIssuedRefreshToken, reIssuedAccessToken);
}
}
}
19 changes: 19 additions & 0 deletions src/main/java/team7/inplace/token/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package team7.inplace.token.domain;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 필요한가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필요합니다......

@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 1000L)
public class RefreshToken {

@Id
private String username;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package team7.inplace.token.persistence;

import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import team7.inplace.token.domain.RefreshToken;

@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {

Optional<RefreshToken> findById(String id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본 형식 메서드인데 재정의 해둬야 하나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인정합니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package team7.inplace.token.presentation;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import team7.inplace.global.exception.InplaceException;
import team7.inplace.global.exception.code.AuthorizationErrorCode;
import team7.inplace.security.util.AuthorizationUtil;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.application.RefreshTokenFacade;
import team7.inplace.token.application.dto.TokenCommand.ReIssued;

@RestController
@RequiredArgsConstructor
public class RefreshTokenController {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 명세 작성해주세용

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

했습니다!


private final JwtUtil jwtUtil;
private final RefreshTokenFacade refreshTokenFacade;

@GetMapping("/refresh-token")
public ResponseEntity<Void> refreshToken(@CookieValue(value = "refresh_token") Cookie cookie,
HttpServletResponse response) {
if (cannotRefreshToken(cookie)) {
throw InplaceException.of(AuthorizationErrorCode.INVALID_TOKEN);
}

String refreshToken = cookie.getValue();
ReIssued reIssuedToken = refreshTokenFacade.getReIssuedRefreshTokenCookie(
jwtUtil.getUsername(refreshToken), refreshToken);
addTokenToCookie(response, reIssuedToken);

return ResponseEntity.ok().build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 new ResponseEntity() 형식으로 하기로 한 거 아니었나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오케이입니다.

}

private void addTokenToCookie(HttpServletResponse response, ReIssued reIssuedToken) {
Cookie accessTokenCookie = createCookie("access_token", reIssuedToken.accessToken());
Cookie refreshTokenCookie = createCookie("refresh_token", reIssuedToken.refreshToken());
response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
}

private boolean cannotRefreshToken(Cookie cookie) {
return AuthorizationUtil.getUserId() == null || !jwtUtil.isRefreshToken(cookie.getValue());
}

private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(60 * 60);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Util 클래스를 만들어야 할까요... Controller에서 처리할 일은 아닌가 싶기도 하구요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

util만들었습니다!

}
Loading