Skip to content

Commit

Permalink
Merge pull request #104 from git1uv/feature/#102
Browse files Browse the repository at this point in the history
[Feat] 레디스 DB 추가
  • Loading branch information
cowboysj authored Nov 27, 2024
2 parents 2ad55e8 + 0e93f82 commit 273bfed
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 31 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.jetbrains:annotations:24.0.1'
implementation 'org.json:json:20230227'
//Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-json'
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/simter/config/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
Expand All @@ -18,14 +19,23 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String token = request.getHeader("Authorization");
JwtTokenDto jwtTokenDto = jwtTokenProvider.resolveToken(request);

System.out.println(jwtTokenDto.getAccessToken());
System.out.println(isTokenBlacklisted(jwtTokenDto.getAccessToken()));
if (token != null && token.startsWith("Bearer ")) {
if (isTokenBlacklisted(jwtTokenDto.getAccessToken())) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("로그아웃 처리된 엑세스 토큰입니다.");
return;
}
if (!jwtTokenProvider.validateToken(jwtTokenDto.getAccessToken())) {
throw new ErrorHandler(ErrorStatus.JWT_UNSUPPORTED_TOKEN);
}
Expand All @@ -34,4 +44,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}
filterChain.doFilter(request, response);
}

private boolean isTokenBlacklisted(String accessToken) {
String blackToken = (String) redisTemplate.opsForValue().get(accessToken);
return StringUtils.hasText(blackToken);
}
}
25 changes: 22 additions & 3 deletions src/main/java/com/simter/config/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.simter.apiPayload.code.status.ErrorStatus;
import com.simter.apiPayload.exception.handler.ErrorHandler;
import com.simter.domain.member.dto.JwtTokenDto;
import com.simter.domain.member.dto.TokenResponse.Token;
import com.simter.domain.member.entity.Member;
import com.simter.domain.member.repository.MemberRepository;
import io.jsonwebtoken.ExpiredJwtException;
Expand Down Expand Up @@ -44,6 +45,7 @@ public class JwtTokenProvider {
private String secretKey = Base64.getEncoder().encodeToString(
Objects.requireNonNull(env.get("JWT_SECRET")).getBytes());
private final MemberRepository memberRepository;
private final TokenRedisRepository tokenRedisRepository;
private static final String AUTHORITIES_KEY = "ROLE_USER";

public JwtTokenDto generateToken(Authentication authentication, String email) {
Expand Down Expand Up @@ -73,7 +75,6 @@ public JwtTokenDto generateToken(Authentication authentication, String email) {

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND));
member.setRefreshToken(refreshToken);
memberRepository.save(member);

return JwtTokenDto.builder()
Expand Down Expand Up @@ -109,6 +110,9 @@ public JwtTokenDto generateSocialToken(String email) {
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();

tokenRedisRepository.save(
new TokenRedis(email, accessToken, refreshToken));

return JwtTokenDto.builder()
.grantType("Bearer")
.accessToken(accessToken)
Expand All @@ -134,13 +138,20 @@ public Authentication getAuthentication(String accessToken) {
}

//액세스 토큰과 리프레시 토큰 함께 재발행
public JwtTokenDto reissueToken(String email) {
public JwtTokenDto reissueToken(String email, String accessToken) {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new ErrorHandler(ErrorStatus.JWT_TOKEN_NOT_FOUND));

TokenRedis tokenRedis = tokenRedisRepository.findByAccessToken(accessToken)
.orElseThrow();
Authentication authentication = getAuthentication(email);

return generateToken(authentication, member.getEmail());
JwtTokenDto newToken = generateToken(authentication, member.getEmail());

tokenRedis.updateToken(newToken.getAccessToken(), newToken.getRefreshToken());
tokenRedisRepository.save(tokenRedis);

return newToken;
}

public JwtTokenDto resolveToken(HttpServletRequest request) {
Expand Down Expand Up @@ -185,4 +196,12 @@ public String getEmail(String token) {
Claims claims = getClaims(token);
return claims.get("email", String.class);
}

public Long getRemainingExpirationTime(String token) {
Claims claims = getClaims(token);
Date expirationDate = claims.getExpiration();
long now = (new Date()).getTime();

return expirationDate.getTime() - now;
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/simter/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.simter.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories(basePackageClasses = TokenRedisRepository.class)
public class RedisConfig {

@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}

@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}

@Bean
public HashOperations<String, String, String> hashOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForHash();
}
}
26 changes: 26 additions & 0 deletions src/main/java/com/simter/config/TokenRedis.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.simter.config;

import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@AllArgsConstructor
@RedisHash(value = "token", timeToLive = 60 * 60 * 24 * 7)
public class TokenRedis {

@Id
private String id;

@Indexed
private String accessToken;

private String refreshToken;

public void updateToken(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/simter/config/TokenRedisRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.simter.config;

import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

public interface TokenRedisRepository extends CrudRepository<TokenRedis, String> {

Optional<TokenRedis> findByAccessToken(String accessToken);

}
18 changes: 10 additions & 8 deletions src/main/java/com/simter/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.simter.config;

import com.simter.domain.member.dto.JwtTokenDto;
import com.simter.domain.member.service.OAuth2UserService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
Expand All @@ -20,6 +25,7 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;


@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
Expand All @@ -30,6 +36,7 @@ public class WebSecurityConfig {
private final OAuth2UserService oAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final UserDetailsService userDetailsService;
private final RedisTemplate redisTemplate;

@Bean
public BCryptPasswordEncoder encoder() {
Expand Down Expand Up @@ -63,7 +70,7 @@ public AuthenticationManager authenticationManager() throws Exception {
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http, RedisTemplate redisTemplate) throws Exception {

return http
.csrf(AbstractHttpConfigurer::disable)
Expand All @@ -74,7 +81,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authorizeHttpRequests(requests -> requests
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
.requestMatchers("/api/v1/*").permitAll()
.requestMatchers("/", "/api/v1/login/general", "/api/v1/register/general").permitAll()
.requestMatchers("/", "/api/v1/login/general", "/api/v1/register/general", "/api/v1/logout").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
Expand All @@ -86,13 +93,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
.successHandler(oAuth2SuccessHandler)
)
.logout(LogoutConfigurer::permitAll)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate),
UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/api/v1/logout")
.permitAll()
)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import io.swagger.v3.oas.annotations.Operation;
import jakarta.mail.MessagingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
Expand Down Expand Up @@ -69,8 +71,9 @@ public ApiResponse<LoginResponseDto> login(@RequestBody LoginRequestDto loginDto
@Operation(summary = "로그아웃 API", description = "리프레시토큰을 파괴하는 API")
@DeleteMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request) {
String token = jwtTokenProvider.resolveToken(request).getAccessToken();
memberService.logout(token);
JwtTokenDto token = jwtTokenProvider.resolveToken(request);
String email = jwtTokenProvider.getEmail(token.getRefreshToken());
memberService.logout(token, email);
return ApiResponse.onSuccess(null);
}

Expand All @@ -79,7 +82,7 @@ public ApiResponse<Void> logout(HttpServletRequest request) {
public ApiResponse<JwtTokenDto> reissue(HttpServletRequest request) {
JwtTokenDto token = jwtTokenProvider.resolveToken(request);
String email = jwtTokenProvider.getEmail(token.getRefreshToken());
JwtTokenDto newToken = jwtTokenProvider.reissueToken(email);
JwtTokenDto newToken = jwtTokenProvider.reissueToken(email, token.getAccessToken());
return ApiResponse.onSuccess(newToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public ApiResponse<SocialLoginDto> login(@RequestParam("code") String code)
.email(email)
.isMember(memberRepository.existsByEmail(email))
.build();

return ApiResponse.onSuccess(response);

}
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/com/simter/domain/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ public class Member implements UserDetails {
@Column(nullable = true)
private LocalDateTime inactiveDate;

@Column(nullable = true)
private String refreshToken;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
Expand Down Expand Up @@ -101,10 +98,6 @@ public void setId(Long id) {
this.id = id;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public void setPassword(String password) {
this.password = password;
}
Expand Down
24 changes: 15 additions & 9 deletions src/main/java/com/simter/domain/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.simter.apiPayload.code.status.ErrorStatus;
import com.simter.apiPayload.exception.handler.ErrorHandler;
import com.simter.config.JwtTokenProvider;
import com.simter.config.TokenRedis;
import com.simter.config.TokenRedisRepository;
import com.simter.domain.member.converter.MemberConverter;
import com.simter.domain.member.dto.JwtTokenDto;
import com.simter.domain.member.dto.MainDto;
Expand All @@ -19,8 +21,10 @@
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -39,6 +43,8 @@ public class MemberService extends DefaultOAuth2UserService {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final JavaMailSender mailSender;
private final TokenRedisRepository tokenRedisRepository;
private final RedisTemplate redisTemplate;

//회원가입
public void register(RegisterDto registerRequestDto) {
Expand Down Expand Up @@ -76,7 +82,6 @@ public void register(SocialRegisterDto socialRegisterDto) {
.build();
Member member = MemberConverter.convertToEntity(newRegisterDto);
if (!memberRepository.existsByEmail(email)) {
member.setRefreshToken(token.getRefreshToken());
memberRepository.save(member);
} else {
Member orgMember = memberRepository.findByEmail(email).orElseThrow();
Expand Down Expand Up @@ -111,21 +116,22 @@ public LoginResponseDto login(String email, String password) {
Authentication authentication
= authenticationManager.authenticate(token);
JwtTokenDto jwtToken = jwtTokenProvider.generateToken(authentication, email);

tokenRedisRepository.save(
new TokenRedis(email, jwtToken.getAccessToken(), jwtToken.getRefreshToken()));
return LoginResponseDto.builder()
.token(jwtToken)
.build();
}

}

//로그아웃
public void logout(String token) {
String email = jwtTokenProvider.getEmail(token);
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new ErrorHandler(ErrorStatus.MEMBER_NOT_FOUND));
member.setRefreshToken(null);
memberRepository.save(member);
public void logout(JwtTokenDto token, String email) {
String accessToken = token.getAccessToken();
Long expirationTime = jwtTokenProvider.getRemainingExpirationTime(accessToken);
if (expirationTime != null && expirationTime > 0) {
redisTemplate.opsForValue().set(accessToken, "logout", expirationTime, TimeUnit.MILLISECONDS);
redisTemplate.delete("token:" + email);
}
}

//비밀번호 재발송
Expand Down
Loading

0 comments on commit 273bfed

Please sign in to comment.