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 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
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,28 @@
package team7.inplace.global.exception.code;

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

@AllArgsConstructor
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 status;
}

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

@Override
public String message() {
return message;
}
}
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 @@ -3,7 +3,9 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import team7.inplace.security.handler.CustomSuccessHandler;
import team7.inplace.security.util.CookieUtil;
import team7.inplace.security.util.JwtUtil;
import team7.inplace.token.application.RefreshTokenService;
import team7.inplace.user.application.UserService;

@Configuration
Expand All @@ -12,8 +14,10 @@ public class SecurityHandlerConfig {
@Bean
public CustomSuccessHandler customSuccessHandler(
JwtUtil jwtUtil,
UserService userService
UserService userService,
RefreshTokenService refreshTokenService,
CookieUtil cookieUtil
) {
return new CustomSuccessHandler(jwtUtil, userService);
return new CustomSuccessHandler(jwtUtil, userService, refreshTokenService, cookieUtil);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import team7.inplace.security.util.CookieUtil;
import team7.inplace.security.util.JwtUtil;

@Configuration
Expand All @@ -11,4 +12,9 @@ public class SecurityUtilConfig {
public JwtUtil jwtUtil(JwtProperties jwtProperties) {
return new JwtUtil(jwtProperties);
}

@Bean
public CookieUtil cookieUtil() {
return new CookieUtil();
}
}
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 @@ -7,21 +7,29 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import team7.inplace.security.application.dto.CustomOAuth2User;
import team7.inplace.security.util.CookieUtil;
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;
private final CookieUtil cookieUtil;

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

@Override
Expand All @@ -31,19 +39,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 = cookieUtil.createCookie("access_token", accessToken);
Cookie refreshTokenCookie = cookieUtil.createCookie("refresh_token", refreshToken);

response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);
Expand All @@ -59,13 +70,4 @@ private void setRedirectUrlToResponse(
}
response.sendRedirect("http://localhost:8080/auth");
}

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

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

import jakarta.servlet.http.Cookie;

public class CookieUtil {

public Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(60 * 60);
cookie.setPath("/");
cookie.setHttpOnly(true);
return cookie;
}
}
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);
}

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,10 @@
package team7.inplace.token.persistence;

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> {

}
Loading