diff --git a/docker-compose.yml b/docker-compose.yml index 5309a0a..5910b71 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,13 @@ version: '3' services: + redis: + image: redis + container_name: redis + ports: + - 6379:6379 + restart: always + web: container_name: web image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} diff --git a/src/main/java/ewha/lux/once/domain/user/controller/UserController.java b/src/main/java/ewha/lux/once/domain/user/controller/UserController.java index e232cb7..8bb5f87 100644 --- a/src/main/java/ewha/lux/once/domain/user/controller/UserController.java +++ b/src/main/java/ewha/lux/once/domain/user/controller/UserController.java @@ -3,13 +3,12 @@ import ewha.lux.once.domain.home.dto.FCMTokenDto; import ewha.lux.once.domain.home.service.FirebaseCloudMessageService; import ewha.lux.once.domain.user.dto.*; -import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.domain.user.service.UserService; import ewha.lux.once.global.common.CommonResponse; import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; import ewha.lux.once.global.common.UserAccount; -import ewha.lux.once.global.security.JwtProvider; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.repository.query.Param; import org.springframework.http.MediaType; @@ -35,13 +34,7 @@ public class UserController { @PostMapping("/signup") public CommonResponse signup(@RequestBody SignupRequestDto request) throws ParseException { try { - Users users = userService.signup(request); - - String accessToken = jwtProvider.generateAccessToken(users.getLoginId()); - String refreshToken = jwtProvider.generateRefreshToken(users.getLoginId()); - - LoginResponseDto loginResponseDto = new LoginResponseDto(users.getId(), accessToken, refreshToken); - return new CommonResponse<>(ResponseCode.SUCCESS, loginResponseDto); + return new CommonResponse<>(ResponseCode.SUCCESS, userService.signup(request)); } catch (CustomException e) { return new CommonResponse<>(e.getStatus()); } @@ -51,14 +44,18 @@ public CommonResponse signup(@RequestBody SignupRequestDto request) throws Pa @PostMapping("/login") public CommonResponse signin(@RequestBody SignInRequestDto request) { try { - Users user = userService.authenticate(request); - - String accessToken = jwtProvider.generateAccessToken(user.getLoginId()); - String refreshToken = jwtProvider.generateRefreshToken(user.getLoginId()); - - LoginResponseDto loginResponseDto = new LoginResponseDto(user.getId(), accessToken, refreshToken); + return new CommonResponse<>(ResponseCode.SUCCESS, userService.authenticate(request)); + } catch (CustomException e) { + return new CommonResponse<>(e.getStatus()); + } + } - return new CommonResponse<>(ResponseCode.SUCCESS, loginResponseDto); + // [Post] 로그아웃 + @PostMapping("/logout") + public CommonResponse logout(HttpServletRequest request, @AuthenticationPrincipal UserAccount userAccount) { + try { + userService.postLogout(userAccount.getUsers(), request); + return new CommonResponse<>(ResponseCode.SUCCESS); } catch (CustomException e) { return new CommonResponse<>(e.getStatus()); } diff --git a/src/main/java/ewha/lux/once/domain/user/service/RedisService.java b/src/main/java/ewha/lux/once/domain/user/service/RedisService.java new file mode 100644 index 0000000..c5b3232 --- /dev/null +++ b/src/main/java/ewha/lux/once/domain/user/service/RedisService.java @@ -0,0 +1,30 @@ +package ewha.lux.once.domain.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + + public void setValueWithTTL(String key, Object value, long timeout, TimeUnit unit) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value, timeout, unit); + } + + public Object getValue(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + Object object = valueOperations.get(key); + return object; + } + + public void deleteValue(String key) { + redisTemplate.delete(key); + } +} diff --git a/src/main/java/ewha/lux/once/domain/user/service/UserService.java b/src/main/java/ewha/lux/once/domain/user/service/UserService.java index 0aca133..e5bc956 100644 --- a/src/main/java/ewha/lux/once/domain/user/service/UserService.java +++ b/src/main/java/ewha/lux/once/domain/user/service/UserService.java @@ -10,7 +10,9 @@ import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.ResponseCode; import ewha.lux.once.global.repository.*; + import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -40,8 +42,10 @@ public class UserService implements UserDetailsService { private final FavoriteRepository favoriteRepository; private final FCMTokenRepository fcmTokenRepository; private final S3Uploader s3Uploader; + private final RedisTemplate redisTemplate; + private final RedisService redisService; - public Users signup(SignupRequestDto request) throws CustomException, ParseException { + public LoginResponseDto signup(SignupRequestDto request) throws CustomException, ParseException { String loginId = request.getLoginId(); String username = request.getUsername(); String password = request.getPassword(); @@ -77,10 +81,18 @@ public Users signup(SignupRequestDto request) throws CustomException, ParseExcep usersBuilder.birthday(birthday); } - return usersRepository.save(usersBuilder.benefitGoal(100000).build()); + Users newUser = usersRepository.save(usersBuilder.benefitGoal(100000).build()); + + String accessToken = jwtProvider.generateAccessToken(newUser.getLoginId()); + String refreshToken = jwtProvider.generateRefreshToken(newUser.getLoginId()); + + LoginResponseDto loginResponseDto = new LoginResponseDto(newUser.getId(), accessToken, refreshToken); + redisService.setValueWithTTL(refreshToken, newUser.getId().toString(), 14L, TimeUnit.DAYS); + + return loginResponseDto; } - public Users authenticate(SignInRequestDto request) throws CustomException { + public LoginResponseDto authenticate(SignInRequestDto request) throws CustomException { String loginId = request.getLoginId(); String password = request.getPassword(); @@ -93,7 +105,19 @@ public Users authenticate(SignInRequestDto request) throws CustomException { users.setLastLogin(); usersRepository.save(users); - return users; + + String accessToken = jwtProvider.generateAccessToken(users.getLoginId()); + String refreshToken = jwtProvider.generateRefreshToken(users.getLoginId()); + LoginResponseDto loginResponseDto = new LoginResponseDto(users.getId(), accessToken, refreshToken); + redisService.setValueWithTTL(refreshToken, users.getId().toString(), 14L, TimeUnit.DAYS); + + return loginResponseDto; + } + + public void postLogout(Users nowUser, HttpServletRequest request) throws CustomException { + String token = jwtProvider.resolveAccessToken(request); + Long expiration = jwtProvider.getExpiration(token); + redisTemplate.opsForValue().set(token, "logout", expiration, TimeUnit.MILLISECONDS); } public void deleteUsers(Users nowUser) throws CustomException { diff --git a/src/main/java/ewha/lux/once/global/config/JwtSecurityConfig.java b/src/main/java/ewha/lux/once/global/config/JwtSecurityConfig.java new file mode 100644 index 0000000..6d8b823 --- /dev/null +++ b/src/main/java/ewha/lux/once/global/config/JwtSecurityConfig.java @@ -0,0 +1,21 @@ +package ewha.lux.once.global.config; + +import ewha.lux.once.domain.user.service.RedisService; +import ewha.lux.once.global.security.JwtAuthFilter; +import ewha.lux.once.global.security.JwtProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@RequiredArgsConstructor +public class JwtSecurityConfig extends SecurityConfigurerAdapter { + private final JwtProvider jwtProvider; + private final RedisService redisService; + + @Override + public void configure(HttpSecurity http) throws Exception{ + http.addFilterBefore(new JwtAuthFilter(jwtProvider, redisService), UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/ewha/lux/once/global/config/RedisConfig.java b/src/main/java/ewha/lux/once/global/config/RedisConfig.java new file mode 100644 index 0000000..f8f86b0 --- /dev/null +++ b/src/main/java/ewha/lux/once/global/config/RedisConfig.java @@ -0,0 +1,33 @@ +package ewha.lux.once.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +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.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@RequiredArgsConstructor +@Configuration +@EnableRedisRepositories +public class RedisConfig { + private final RedisProperties redisProperties; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } + +} diff --git a/src/main/java/ewha/lux/once/global/config/SecurityConfig.java b/src/main/java/ewha/lux/once/global/config/SecurityConfig.java index fbffa45..54a607a 100644 --- a/src/main/java/ewha/lux/once/global/config/SecurityConfig.java +++ b/src/main/java/ewha/lux/once/global/config/SecurityConfig.java @@ -1,5 +1,6 @@ package ewha.lux.once.global.config; +import ewha.lux.once.domain.user.service.RedisService; import ewha.lux.once.global.security.JwtAuthFilter; import ewha.lux.once.global.security.JwtProvider; import lombok.RequiredArgsConstructor; @@ -25,7 +26,7 @@ @Configuration public class SecurityConfig { private final JwtProvider jwtProvider; - + private final RedisService redisService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -60,7 +61,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/user/card/**").permitAll() .anyRequest().authenticated() ) - .addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(new JwtAuthFilter(jwtProvider, redisService), UsernamePasswordAuthenticationFilter.class); return http.build(); diff --git a/src/main/java/ewha/lux/once/global/security/JwtAuthFilter.java b/src/main/java/ewha/lux/once/global/security/JwtAuthFilter.java index 7b27331..6d7441d 100644 --- a/src/main/java/ewha/lux/once/global/security/JwtAuthFilter.java +++ b/src/main/java/ewha/lux/once/global/security/JwtAuthFilter.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import ewha.lux.once.domain.user.dto.LoginResponseDto; +import ewha.lux.once.domain.user.service.RedisService; +import ewha.lux.once.global.common.CustomException; import ewha.lux.once.global.common.UserAccount; import ewha.lux.once.domain.user.entity.Users; import ewha.lux.once.global.common.ResponseDto; @@ -28,6 +30,7 @@ @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; + private final RedisService redisService; public static final String HEADER_KEY = "Authorization"; public static final String REFRESH_HEADER_KEY = "Authorization-refresh"; public static final String PREFIX = "Bearer "; @@ -37,12 +40,19 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String accessToken = resolveToken(request, HEADER_KEY); + String refreshToken = resolveToken(request, REFRESH_HEADER_KEY); + // 자동 로그인 요청인 경우 if(request.getRequestURI().equals("/user/auto")) { - - String accessToken = resolveToken(request, HEADER_KEY); - String refreshToken = resolveToken(request, REFRESH_HEADER_KEY); - + if ("Deprecated".equals(redisService.getValue(refreshToken))) { + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseEntity.ok(ResponseDto.response(1000, true, "refresh token이 만료되었습니다. 다시 로그인해주세요")))); + return; + } + // 토큰 유효성 검사 if (!jwtProvider.validateAccessTokenExpiration(accessToken)) { // accesstoken이 유효한 경우 Users users = jwtProvider.validateTokenAndGetUsers(accessToken); LoginResponseDto loginResponseDto = new LoginResponseDto(users.getId(), accessToken, refreshToken); @@ -50,7 +60,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseEntity.ok(ResponseDto.response(1000,true, "access token이 검증되었습니다.", loginResponseDto)))); + response.getWriter().write(new ObjectMapper().writeValueAsString(ResponseEntity.ok(ResponseDto.response(1000, true, "access token이 검증되었습니다.", loginResponseDto)))); return; } else if (!jwtProvider.validateAccessTokenExpiration(refreshToken)) { // accesstoken 만료, refreshtoken이 유효한 경우 Users users = jwtProvider.validateTokenAndGetUsers(refreshToken); @@ -70,9 +80,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } } - - String accessToken = resolveToken(request, HEADER_KEY); - if(StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken)){ Users users = jwtProvider.validateTokenAndGetUsers(accessToken); @@ -80,9 +87,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); return; } - - - filterChain.doFilter(request, response); } public void saveAuthentication(Users users) { diff --git a/src/main/java/ewha/lux/once/global/security/JwtProvider.java b/src/main/java/ewha/lux/once/global/security/JwtProvider.java index 65e522d..1203095 100644 --- a/src/main/java/ewha/lux/once/global/security/JwtProvider.java +++ b/src/main/java/ewha/lux/once/global/security/JwtProvider.java @@ -4,11 +4,13 @@ import ewha.lux.once.global.repository.UsersRepository; import ewha.lux.once.domain.user.service.UserService; import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.util.*; @@ -17,8 +19,9 @@ public class JwtProvider { private static final long ACCESS_EXPIRE_TIME = 1000l * 60 * 60 * 2; // 2시간 private static final long REFRESH_EXPIRE_TIME = 1000l * 60 * 60 * 24 * 14; // 2주 - private final UserService userService; private final UsersRepository usersRepository; + public static final String TOKEN_PREFIX = "Bearer "; + public static final String HEADER_STRING = "Authorization"; @Getter @Value("${spring.jwt.secret}") @@ -95,5 +98,18 @@ public boolean validateAccessTokenExpiration(String token) { } } + // accessToken 값 가져오기 + public String resolveAccessToken(HttpServletRequest request) { + String token = request.getHeader(JwtProvider.HEADER_STRING); + if (StringUtils.hasText(token) && token.startsWith(JwtProvider.TOKEN_PREFIX)) { + return token.replace(JwtProvider.TOKEN_PREFIX, ""); + } + return null; + } + // accessToken 만료 시간 반환 + public Long getExpiration(String token) { + Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); + return claims.getExpiration().getTime(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d2da036..6502a92 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,8 @@ spring: + data: + redis: + host: redis + port: 6379 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${SPRING_DATABASE_URL}