Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#52 Backend RefreshToken에 Redis적용 #53

Merged
merged 11 commits into from
Mar 25, 2024
1 change: 1 addition & 0 deletions backend/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'org.jsoup:jsoup:1.15.3'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE'

annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ public class Member extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String oauthId; //로그인한 소셜 타입의 식별자 값
private String oauthId;
private String nickname;
private String email;
private String password;
private String imageUrl;

private String refreshToken;

@Enumerated(EnumType.STRING)
private Role role;

Expand All @@ -37,10 +35,6 @@ public Member update(String email, String imageUrl) {
return this;
}

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

public void signUp(String nickname) {
this.nickname = nickname;
this.role = Role.USER;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,5 @@
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

Optional<Member> findByRefreshToken(String refreshToken);

Optional<Member> findBySocialTypeAndOauthId(SocialType socialType, String oauthId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.rollthedice.backend.domain.member.dto.SignUpDto;
import com.rollthedice.backend.domain.member.entity.Member;
import com.rollthedice.backend.domain.member.query.AuthService;
import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService;
import com.rollthedice.backend.global.jwt.service.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -11,10 +15,18 @@
@Service
public class MemberService {
private final AuthService authService;
private final RefreshTokenService refreshTokenService;
private final JwtService jwtService;
private final HttpServletRequest request;
private final HttpServletResponse response;

@Transactional
public void signUp(SignUpDto dto) {
Member member = authService.getMember();
member.signUp(dto.getNickname());

String refreshToken = jwtService.createRefreshToken();
jwtService.setRefreshTokenHeader(response, refreshToken);
refreshTokenService.updateToken(member.getEmail(), refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.rollthedice.backend.global.advice;

import com.rollthedice.backend.global.jwt.exception.NotFoundTokenException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ExceptionAdvice {
@ExceptionHandler(NotFoundTokenException.class)
public ResponseEntity<HttpEntity> notFoundTokenException() {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.rollthedice.backend.global.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;

@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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rollthedice.backend.domain.member.repository.MemberRepository;
import com.rollthedice.backend.global.jwt.filter.JwtAuthenticationProcessingFilter;
import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService;
import com.rollthedice.backend.global.jwt.service.JwtService;
import com.rollthedice.backend.global.login.filter.CustomJsonUsernamePasswordAuthenticationFilter;
import com.rollthedice.backend.global.login.handler.LoginFailureHandler;
import com.rollthedice.backend.global.login.handler.LoginSuccessHandler;
import com.rollthedice.backend.global.login.service.LoginService;
//import com.rollthedice.backend.global.login.handler.LoginFailureHandler;
//import com.rollthedice.backend.global.login.handler.LoginSuccessHandler;
//import com.rollthedice.backend.global.login.service.LoginService;
import com.rollthedice.backend.global.oauth2.handler.OAuth2LoginFailureHandler;
import com.rollthedice.backend.global.oauth2.handler.OAuth2LoginSuccessHandler;
import com.rollthedice.backend.global.oauth2.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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;
Expand All @@ -31,13 +28,13 @@
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final LoginService loginService;
private final JwtService jwtService;
private final MemberRepository memberRepository;
private final ObjectMapper objectMapper;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final RefreshTokenService refreshTokenService;


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -58,9 +55,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) //customUserService 설정
);
http.addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationProcessingFilter(), CustomJsonUsernamePasswordAuthenticationFilter.class);

http.addFilterAfter(jwtAuthenticationProcessingFilter(), LogoutFilter.class);
return http.build();
}

Expand All @@ -69,36 +64,8 @@ public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(loginService);
return new ProviderManager(provider);
}

@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtService, memberRepository);
}

@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler();
}

@Bean
public CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordAuthenticationFilter() {
CustomJsonUsernamePasswordAuthenticationFilter customJsonUsernamePasswordLoginFilter
= new CustomJsonUsernamePasswordAuthenticationFilter(objectMapper);
customJsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
customJsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
customJsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return customJsonUsernamePasswordLoginFilter;
}

@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
return new JwtAuthenticationProcessingFilter(jwtService, memberRepository);
return new JwtAuthenticationProcessingFilter(jwtService, refreshTokenService, memberRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.rollthedice.backend.global.jwt.exception;

public class NotFoundTokenException extends RuntimeException {
public NotFoundTokenException() {
}

public NotFoundTokenException(String message) {
super(message);
}

public NotFoundTokenException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.rollthedice.backend.domain.member.entity.Member;
import com.rollthedice.backend.domain.member.repository.MemberRepository;
import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken;
import com.rollthedice.backend.global.jwt.refresh.service.RefreshTokenService;
import com.rollthedice.backend.global.jwt.service.JwtService;
import com.rollthedice.backend.global.jwt.util.PasswordUtil;
import jakarta.servlet.FilterChain;
Expand All @@ -27,14 +29,16 @@ public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
private static final String NO_CHECK_URL = "/login";

private final JwtService jwtService;
private final RefreshTokenService refreshTokenService;
private final MemberRepository memberRepository;

private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().equals(NO_CHECK_URL)) {
filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
filterChain.doFilter(request, response);
return;
}
String refreshToken = jwtService.extractRefreshToken(request)
Expand All @@ -46,29 +50,26 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
return;
}

if (refreshToken == null) {
log.info("refresh token is null");
checkAccessTokenAndAuthentication(request, response, filterChain);
}
log.info("refresh token is null");
checkAccessTokenAndAuthentication(request, response, filterChain);
}

public void checkRefreshTokenAndReIssueAccessToken(HttpServletResponse response, String refreshToken) {
memberRepository.findByRefreshToken(refreshToken)
.ifPresent(member -> {
String reIssueRefreshToken = reIssueRefreshToken(member);
jwtService.sendAccessAndRefreshToken(response, jwtService.createAccessToken(member.getEmail()),
reIssueRefreshToken);
});
RefreshToken refresh = refreshTokenService.findByToken(refreshToken);
String reIssuedRefreshToken = reIssueRefreshToken(refresh.getEmail());
jwtService.sendAccessAndRefreshToken(response,
jwtService.createAccessToken(refresh.getEmail()), reIssuedRefreshToken);
}

private String reIssueRefreshToken(Member member) {
private String reIssueRefreshToken(String email) {
String reIssuedRefreshToken = jwtService.createRefreshToken();
member.updateRefreshToken(reIssuedRefreshToken);
memberRepository.saveAndFlush(member);

refreshTokenService.updateToken(email, reIssuedRefreshToken);
return reIssuedRefreshToken;
}

private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
private void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
jwtService.extractAccessToken(request)
.filter(jwtService::isTokenValid)
.ifPresent(accessToken -> jwtService.extractEmail(accessToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.rollthedice.backend.global.jwt.refresh.domain;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@RedisHash(value = "refreshToken")
public class RefreshToken {

@Id
private String email;

@Indexed
private String refreshToken;

@TimeToLive()
private Long expirationPeriod;

public RefreshToken(String email) {
this.email = email;
}

public void createRefreshToken(String refreshToken, Long expiration) {
this.refreshToken = refreshToken;
this.expirationPeriod = expiration;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.rollthedice.backend.global.jwt.refresh.repository;

import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.rollthedice.backend.global.jwt.refresh.service;

import com.rollthedice.backend.global.jwt.exception.NotFoundTokenException;
import com.rollthedice.backend.global.jwt.refresh.domain.RefreshToken;
import com.rollthedice.backend.global.jwt.refresh.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;

@Value("${jwt.refresh.expiration}")
private Long expirationPeriod;

public void updateToken(String email, String token) {
RefreshToken refreshToken = refreshTokenRepository.findById(email).orElse(new RefreshToken(email));
refreshToken.createRefreshToken(token, expirationPeriod / 1000);
log.info("토큰 값 : " + refreshToken.getRefreshToken());
log.info("이메일 : " + refreshToken.getEmail());
log.info("유효기간 : " + refreshToken.getExpirationPeriod());
refreshTokenRepository.save(refreshToken);
}

public RefreshToken findByToken(String token) {
return refreshTokenRepository.findByRefreshToken(token)
.orElseThrow(NotFoundTokenException::new);
}

}
Loading