-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat] #15 - Redis 적용과 refresh api 개발 구현
- Loading branch information
1 parent
e40a9db
commit d3571d1
Showing
7 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
62 changes: 62 additions & 0 deletions
62
week2/src/main/java/server/sopt/week2/auth/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} | ||
|
||
|
30 changes: 30 additions & 0 deletions
30
week2/src/main/java/server/sopt/week2/auth/redis/domain/Token.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
week2/src/main/java/server/sopt/week2/auth/redis/service/RefreshTokenService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Object> 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<CreateTokenByRefreshTokenResponse> 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~!"); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
week2/src/main/java/server/sopt/week2/auth/repository/RedisTokenRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Token, Long> { | ||
Optional<Token> findByRefreshToken(final String refreshToken); | ||
Optional<Token> findById(final Long memberId); | ||
} |
81 changes: 81 additions & 0 deletions
81
week2/src/main/java/server/sopt/week2/common/jwt/JwtTokenProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
week2/src/main/java/server/sopt/week2/config/RedisConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Object> redisTemplate() { | ||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory()); | ||
redisTemplate.setKeySerializer(new StringRedisSerializer()); | ||
redisTemplate.setValueSerializer(new StringRedisSerializer()); | ||
return redisTemplate; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
week2/src/main/java/server/sopt/week2/controller/TokenController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CreateTokenByRefreshTokenResponse> reissueToken(HttpServletRequest request, HttpServletResponse response) throws IOException{ | ||
return refreshTokenService.refreshToken(request, response); | ||
} | ||
|
||
} |