diff --git a/week2/src/main/java/server/sopt/week2/auth/SecurityConfig.java b/week2/src/main/java/server/sopt/week2/auth/SecurityConfig.java new file mode 100644 index 0000000..880faf0 --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/auth/SecurityConfig.java @@ -0,0 +1,62 @@ +package server.sopt.week2.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import server.sopt.week2.auth.filter.CustomAccessDeniedHandler; +import server.sopt.week2.auth.filter.CustomJwtAuthenticationEntryPoint; +import server.sopt.week2.auth.filter.JwtAuthenticationFilter; +import server.sopt.week2.auth.service.LogoutService; +//import server.sopt.week2.auth.service.LogoutService; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final LogoutService authService; + private static final String[] AUTH_WHITE_LIST = { + "/api/v1/member", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-resources/**", + "api/v1/token/**" + }; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .requestCache(RequestCacheConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .exceptionHandling(exception -> + { + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); + exception.accessDeniedHandler(customAccessDeniedHandler); + }); + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(AUTH_WHITE_LIST).permitAll(); + auth.anyRequest().authenticated(); + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .logout(logoutConfig -> { + logoutConfig + .logoutUrl("/auth/logout") + .addLogoutHandler(authService) + .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()); + }); + return http.build(); + } +} + + diff --git a/week2/src/main/java/server/sopt/week2/auth/redis/domain/Token.java b/week2/src/main/java/server/sopt/week2/auth/redis/domain/Token.java new file mode 100644 index 0000000..bd3749f --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/auth/redis/domain/Token.java @@ -0,0 +1,30 @@ +package server.sopt.week2.auth.redis.domain; + +import jakarta.persistence.Entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "refreshToken") +@AllArgsConstructor +@Getter +@Builder +public class Token { +// @Indexed + @Id + private Long id; + + @Indexed + private String refreshToken; + + public static Token of(Long id, String refreshToken) { + return Token.builder() + .id(id) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/week2/src/main/java/server/sopt/week2/auth/redis/service/RefreshTokenService.java b/week2/src/main/java/server/sopt/week2/auth/redis/service/RefreshTokenService.java new file mode 100644 index 0000000..d36e371 --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/auth/redis/service/RefreshTokenService.java @@ -0,0 +1,86 @@ +package server.sopt.week2.auth.redis.service; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import server.sopt.week2.auth.UserAuthentication; +import server.sopt.week2.auth.redis.domain.Token; +import server.sopt.week2.auth.repository.RedisTokenRepository; +import server.sopt.week2.common.jwt.JwtTokenProvider; +import server.sopt.week2.common.jwt.JwtValidationType; +import server.sopt.week2.common.jwt.dto.CreateTokenByRefreshTokenResponse; + +import static server.sopt.week2.common.jwt.JwtValidationType.EXPIRED_JWT_TOKEN; +import static server.sopt.week2.common.jwt.JwtValidationType.VALID_JWT; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + private final RedisTemplate redisTemplate; + private final RedisTokenRepository redisTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + + + @Transactional + public void save( + final Long memberId, + final String refreshToken + ) { + redisTokenRepository.save( + Token.of(memberId, refreshToken) + ); + } + + public Long findIdByRefreshToken( + final String refreshToken + ) { + return redisTokenRepository.findByRefreshToken(refreshToken) + .orElseThrow(() -> new RuntimeException("NO TOKEN")).getId(); + } + + @Transactional + public void deleteRefreshToken( + final Long memberId + ) { + Token token = redisTokenRepository.findById(memberId) + .orElseThrow( + () -> new RuntimeException("token없음") + ); + + redisTokenRepository.delete(token); + } + + + public ResponseEntity refreshToken(HttpServletRequest request, HttpServletResponse response) { + final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + final String token; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return null; + } + token = authHeader.substring(7); + JwtValidationType jwtValidationType = jwtTokenProvider.validateToken(token); + + if (jwtValidationType == VALID_JWT) { + Long memberId = jwtTokenProvider.getUserFromJwt(token); + Token refreshToken = redisTokenRepository.findById(memberId).orElseThrow( + () -> new RuntimeException("refresh token 만료 혹은 없음") + ); + UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + return ResponseEntity.status(HttpStatus.CREATED) + .header("Location", memberId.toString()) + .body(CreateTokenByRefreshTokenResponse.of(jwtTokenProvider.issueAccessToken(authentication))); + } + throw new RuntimeException("error~!"); + } +} diff --git a/week2/src/main/java/server/sopt/week2/auth/repository/RedisTokenRepository.java b/week2/src/main/java/server/sopt/week2/auth/repository/RedisTokenRepository.java new file mode 100644 index 0000000..7ed2b7a --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/auth/repository/RedisTokenRepository.java @@ -0,0 +1,14 @@ +package server.sopt.week2.auth.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; +import server.sopt.week2.auth.redis.domain.Token; + +import java.util.Optional; + +//@Repository +public interface RedisTokenRepository extends CrudRepository { + Optional findByRefreshToken(final String refreshToken); + Optional findById(final Long memberId); +} diff --git a/week2/src/main/java/server/sopt/week2/common/jwt/JwtTokenProvider.java b/week2/src/main/java/server/sopt/week2/common/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..7907265 --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/common/jwt/JwtTokenProvider.java @@ -0,0 +1,81 @@ +package server.sopt.week2.common.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Value; + +import javax.crypto.SecretKey; +import java.util.Base64; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; +// private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 1L; + + private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 60 * 60 * 24 * 1000L * 14; + + @Value("${jwt.secret}") + private final String JWT_SCRET = "nowsoptnowsoptnownownononwonwownwownownwowno"; + + public String issueRefreshToken(final Authentication authentication) { + return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); + } + + public String issueAccessToken(final Authentication authentication) { + return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); + } + + + public String generateToken(Authentication authentication, Long tokenExpirationTime) { + final Date now = new Date(); + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 + + claims.put(USER_ID, authentication.getPrincipal()); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header + .setClaims(claims) // Claim + .signWith(getSigningKey()) // Signature + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SCRET.getBytes()); //SecretKey 통해 서명 생성 + return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(USER_ID).toString()); + } +} \ No newline at end of file diff --git a/week2/src/main/java/server/sopt/week2/config/RedisConfig.java b/week2/src/main/java/server/sopt/week2/config/RedisConfig.java new file mode 100644 index 0000000..2618448 --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/config/RedisConfig.java @@ -0,0 +1,31 @@ +package server.sopt.week2.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 host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} diff --git a/week2/src/main/java/server/sopt/week2/controller/TokenController.java b/week2/src/main/java/server/sopt/week2/controller/TokenController.java new file mode 100644 index 0000000..ce2d8b0 --- /dev/null +++ b/week2/src/main/java/server/sopt/week2/controller/TokenController.java @@ -0,0 +1,26 @@ +package server.sopt.week2.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import server.sopt.week2.auth.redis.service.RefreshTokenService; +import server.sopt.week2.common.jwt.dto.CreateTokenByRefreshTokenResponse; + +import java.io.IOException; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api/v1/token") +public class TokenController { + + private final RefreshTokenService refreshTokenService; + @PostMapping("/refreshs") + public ResponseEntity reissueToken(HttpServletRequest request, HttpServletResponse response) throws IOException{ + return refreshTokenService.refreshToken(request, response); + } + +}