diff --git a/build.gradle b/build.gradle index 238bbe7..7ae2b40 100644 --- a/build.gradle +++ b/build.gradle @@ -42,8 +42,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - // Swagger 3.0.0 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/reddiserver/common/ApiResponse.java b/src/main/java/com/example/reddiserver/common/ApiResponse.java index d994f78..be02ccf 100644 --- a/src/main/java/com/example/reddiserver/common/ApiResponse.java +++ b/src/main/java/com/example/reddiserver/common/ApiResponse.java @@ -18,6 +18,10 @@ public static ApiResponse successResponse(T data) { return new ApiResponse<>(SUCCESS_STATUS, data, null); } + public static ApiResponse successResponse(T data, String message) { + return new ApiResponse<>(SUCCESS_STATUS, data, message); + } + public static ApiResponse successWithNoContent() { return new ApiResponse<>(SUCCESS_STATUS, null, null); } diff --git a/src/main/java/com/example/reddiserver/config/RedisConfig.java b/src/main/java/com/example/reddiserver/config/RedisConfig.java new file mode 100644 index 0000000..fb03ad6 --- /dev/null +++ b/src/main/java/com/example/reddiserver/config/RedisConfig.java @@ -0,0 +1,34 @@ +package com.example.reddiserver.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +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 { + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisHost, redisPort); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/example/reddiserver/controller/auth/AuthController.java b/src/main/java/com/example/reddiserver/controller/auth/AuthController.java new file mode 100644 index 0000000..68848cc --- /dev/null +++ b/src/main/java/com/example/reddiserver/controller/auth/AuthController.java @@ -0,0 +1,23 @@ +package com.example.reddiserver.controller.auth; + +import com.example.reddiserver.common.ApiResponse; +import com.example.reddiserver.dto.auth.request.ReissueRequestDto; +import com.example.reddiserver.dto.security.response.TokenDto; +import com.example.reddiserver.service.auth.AuthService; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class AuthController { + private final AuthService authService; + + @PostMapping("/reissue") + public ApiResponse reissue(@RequestBody ReissueRequestDto reissueRequestDto) { + return ApiResponse.successResponse(authService.reissue(reissueRequestDto), "Access Token 재발급 성공"); + } +} diff --git a/src/main/java/com/example/reddiserver/dto/auth/request/ReissueRequestDto.java b/src/main/java/com/example/reddiserver/dto/auth/request/ReissueRequestDto.java new file mode 100644 index 0000000..202a7b3 --- /dev/null +++ b/src/main/java/com/example/reddiserver/dto/auth/request/ReissueRequestDto.java @@ -0,0 +1,9 @@ +package com.example.reddiserver.dto.auth.request; + +import lombok.Getter; + +@Getter +public class ReissueRequestDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/example/reddiserver/dto/security/JwtErrorResponse.java b/src/main/java/com/example/reddiserver/dto/security/response/JwtErrorResponse.java similarity index 93% rename from src/main/java/com/example/reddiserver/dto/security/JwtErrorResponse.java rename to src/main/java/com/example/reddiserver/dto/security/response/JwtErrorResponse.java index ab326c8..89b7570 100644 --- a/src/main/java/com/example/reddiserver/dto/security/JwtErrorResponse.java +++ b/src/main/java/com/example/reddiserver/dto/security/response/JwtErrorResponse.java @@ -1,4 +1,4 @@ -package com.example.reddiserver.dto.security; +package com.example.reddiserver.dto.security.response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/com/example/reddiserver/dto/security/TokenDto.java b/src/main/java/com/example/reddiserver/dto/security/response/TokenDto.java similarity index 73% rename from src/main/java/com/example/reddiserver/dto/security/TokenDto.java rename to src/main/java/com/example/reddiserver/dto/security/response/TokenDto.java index 61fd7fb..10038b5 100644 --- a/src/main/java/com/example/reddiserver/dto/security/TokenDto.java +++ b/src/main/java/com/example/reddiserver/dto/security/response/TokenDto.java @@ -1,4 +1,4 @@ -package com.example.reddiserver.dto.security; +package com.example.reddiserver.dto.security.response; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/example/reddiserver/entity/RefreshToken.java b/src/main/java/com/example/reddiserver/entity/RefreshToken.java index 3bbff62..24da08c 100644 --- a/src/main/java/com/example/reddiserver/entity/RefreshToken.java +++ b/src/main/java/com/example/reddiserver/entity/RefreshToken.java @@ -1,30 +1,25 @@ package com.example.reddiserver.entity; -import com.example.reddiserver.entity.base.BaseTimeEntity; -import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; -@Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RefreshToken extends BaseTimeEntity { +@RedisHash(value = "refreshToken", timeToLive = 60*60*24*3) +public class RefreshToken { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private String refreshToken; - @Column(nullable = false, unique = true) - private String email; + @Indexed + private String providerId; - @Column(nullable = false) - private String refreshToken; @Builder - public RefreshToken(String email, String refreshToken) { - this.email = email; + public RefreshToken(String providerId, String refreshToken) { + this.providerId = providerId; this.refreshToken = refreshToken; } diff --git a/src/main/java/com/example/reddiserver/repository/RefreshTokenRepository.java b/src/main/java/com/example/reddiserver/repository/RefreshTokenRepository.java index 7e54336..b77330f 100644 --- a/src/main/java/com/example/reddiserver/repository/RefreshTokenRepository.java +++ b/src/main/java/com/example/reddiserver/repository/RefreshTokenRepository.java @@ -1,10 +1,12 @@ package com.example.reddiserver.repository; import com.example.reddiserver.entity.RefreshToken; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; -public interface RefreshTokenRepository extends JpaRepository { - Optional findByEmail(String email); +@Repository +public interface RefreshTokenRepository extends CrudRepository { + Optional findByProviderId(String providerId); } diff --git a/src/main/java/com/example/reddiserver/security/jwt/JwtExceptionHandlerFilter.java b/src/main/java/com/example/reddiserver/security/jwt/JwtExceptionHandlerFilter.java index d743a61..ba11a09 100644 --- a/src/main/java/com/example/reddiserver/security/jwt/JwtExceptionHandlerFilter.java +++ b/src/main/java/com/example/reddiserver/security/jwt/JwtExceptionHandlerFilter.java @@ -1,6 +1,6 @@ package com.example.reddiserver.security.jwt; -import com.example.reddiserver.dto.security.JwtErrorResponse; +import com.example.reddiserver.dto.security.response.JwtErrorResponse; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.MalformedJwtException; diff --git a/src/main/java/com/example/reddiserver/security/jwt/TokenProvider.java b/src/main/java/com/example/reddiserver/security/jwt/TokenProvider.java index 610c546..91b0c5c 100644 --- a/src/main/java/com/example/reddiserver/security/jwt/TokenProvider.java +++ b/src/main/java/com/example/reddiserver/security/jwt/TokenProvider.java @@ -1,6 +1,6 @@ package com.example.reddiserver.security.jwt; -import com.example.reddiserver.dto.security.TokenDto; +import com.example.reddiserver.dto.security.response.TokenDto; import com.example.reddiserver.entity.RefreshToken; import com.example.reddiserver.repository.RefreshTokenRepository; import io.jsonwebtoken.*; @@ -27,7 +27,7 @@ public class TokenProvider implements InitializingBean { private static final String AUTHORITIES_KEY = "auth"; private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30L; - private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 242 * 60 * 60 * 1000L; + private static final long REFRESH_TOKEN_EXPIRE_TIME = 60 * 60 * 24 * 3L; @Value("${spring.jwt.secret}") private String secret; @@ -65,17 +65,18 @@ public TokenDto createAccessToken(Authentication authentication) { String accessToken = createToken(authentication.getName(), authorities, "access"); String newRefreshToken = createToken(authentication.getName(), authorities, "refresh"); - Optional oldRefreshToken = refreshTokenRepository.findByEmail(authentication.getName()); + Optional oldRefreshToken = refreshTokenRepository.findByProviderId(authentication.getName()); if (oldRefreshToken.isPresent()) { refreshTokenRepository.save(oldRefreshToken.get().updateToken(newRefreshToken)); } else { RefreshToken refreshToken = RefreshToken.builder() - .email(authentication.getName()) + .providerId(authentication.getName()) .refreshToken(newRefreshToken) .build(); refreshTokenRepository.save(refreshToken); + System.out.println("test"); } return TokenDto.builder() @@ -129,7 +130,7 @@ public boolean validateAccessToken(String accessToken) { public boolean refreshTokenValidation(String token) { if (!validateAccessToken(token)) return false; - Optional refreshToken = refreshTokenRepository.findByEmail(getEmailFromToken(token)); + Optional refreshToken = refreshTokenRepository.findByProviderId(getEmailFromToken(token)); return refreshToken.isPresent() && token.equals(refreshToken.get().getRefreshToken()); } diff --git a/src/main/java/com/example/reddiserver/security/oauth/PrincipalOAuth2Details.java b/src/main/java/com/example/reddiserver/security/oauth/PrincipalOAuth2Details.java index 0015873..a781132 100644 --- a/src/main/java/com/example/reddiserver/security/oauth/PrincipalOAuth2Details.java +++ b/src/main/java/com/example/reddiserver/security/oauth/PrincipalOAuth2Details.java @@ -44,6 +44,6 @@ public String getAuthority() { @Override public String getName() { - return "name"; + return member.getProviderId(); } } diff --git a/src/main/java/com/example/reddiserver/security/oauth/handler/OAuthSuccessHandler.java b/src/main/java/com/example/reddiserver/security/oauth/handler/OAuthSuccessHandler.java index 74b4e5a..f3bf65d 100644 --- a/src/main/java/com/example/reddiserver/security/oauth/handler/OAuthSuccessHandler.java +++ b/src/main/java/com/example/reddiserver/security/oauth/handler/OAuthSuccessHandler.java @@ -1,16 +1,14 @@ package com.example.reddiserver.security.oauth.handler; -import com.example.reddiserver.dto.security.TokenDto; import com.example.reddiserver.security.jwt.TokenProvider; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; @@ -19,15 +17,17 @@ public class OAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final TokenProvider tokenProvider; + private static final ObjectMapper mapper = new ObjectMapper(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - TokenDto tokenDto = tokenProvider.createAccessToken(authentication); + // Access, Refresh Token Body 저장 + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); - String targetUrl = UriComponentsBuilder.fromUriString(getDefaultTargetUrl()) - .queryParam("token", tokenDto) - .build().toUriString(); + String tokenDto = mapper.writeValueAsString(tokenProvider.createAccessToken(authentication)); + response.getWriter().write(tokenDto); - getRedirectStrategy().sendRedirect(request, response, targetUrl); + response.getWriter().flush(); } } diff --git a/src/main/java/com/example/reddiserver/service/auth/AuthService.java b/src/main/java/com/example/reddiserver/service/auth/AuthService.java new file mode 100644 index 0000000..2efddd2 --- /dev/null +++ b/src/main/java/com/example/reddiserver/service/auth/AuthService.java @@ -0,0 +1,27 @@ +package com.example.reddiserver.service.auth; + +import com.example.reddiserver.dto.auth.request.ReissueRequestDto; +import com.example.reddiserver.dto.security.response.TokenDto; +import com.example.reddiserver.security.jwt.TokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + private final TokenProvider tokenProvider; + + @Transactional + public TokenDto reissue(ReissueRequestDto reissueRequestDto) { + if (!tokenProvider.refreshTokenValidation(reissueRequestDto.getRefreshToken())) { + throw new RuntimeException("유효하지 않은 Refresh Token 입니다."); + } + + Authentication authentication = tokenProvider.getAuthentication(reissueRequestDto.getAccessToken()); + + return tokenProvider.createAccessToken(authentication); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a7a76a5..7c1c684 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,6 +27,10 @@ spring: client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} scope: profile, email + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} springdoc: api-docs: