Skip to content

Commit

Permalink
[feat] #15 - Redis 적용과 refresh api 개발 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
rlarlgnszx committed May 27, 2024
1 parent e40a9db commit d3571d1
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 0 deletions.
62 changes: 62 additions & 0 deletions week2/src/main/java/server/sopt/week2/auth/SecurityConfig.java
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 week2/src/main/java/server/sopt/week2/auth/redis/domain/Token.java
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();
}
}
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~!");
}
}
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);
}
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 week2/src/main/java/server/sopt/week2/config/RedisConfig.java
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;
}
}
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);
}

}

0 comments on commit d3571d1

Please sign in to comment.