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

[week6] 6차 세미나 정규 과제 #11

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

[week6] 6차 세미나 정규 과제 #11

wants to merge 3 commits into from

Conversation

seokbeom00
Copy link
Contributor

과제TODO

  • 로그인을 진행할 때 AccessToken과. RefreshToken을 함께 반환하는 로직 구현
  • Redis를 활용해 RefreshToken으로 AccessToken을 재발급 받는 로직 구현

구현사항

로그인 시 RefreshToken도 같이 발급

  • 멤버가입 반환 DTO에 Refresh Token 추가

    public record UserJoinResponse(
    String accessToken,
    String refreshToken,
    String userId
    ) {
    public static UserJoinResponse of(
    String accessToken,
    String refreshToken,
    String userId
    ) {
    return new UserJoinResponse(accessToken, refreshToken, userId);
    }
    }

  • AccessToken 만료시간 줄임, RefreshToken 발급 로직 추가

    private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L;
    private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;
    @Value("${jwt.secret}")
    private String JWT_SECRET;
    public String issueAccessToken(final Authentication authentication) {
    return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
    }
    public String issueRefreshToken(final Authentication authentication) {
    return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
    }

  • Redis 설정파일 추가

    @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;
    }
    }

  • Redis에 RefreshToken 저장을 위한 Token 도메인 추가

    @RedisHash(value = "", timeToLive = 60 * 60 * 24 * 1000L * 14)
    @AllArgsConstructor
    @Getter
    @Builder
    public class Token {
    @Id
    private Long id;
    @Indexed
    private String refreshToken;
    public static Token of(
    final Long id,
    final String refreshToken
    ) {
    return Token.builder()
    .id(id)
    .refreshToken(refreshToken)
    .build();
    }
    }

  • CrudRepository 상속받아서 Redis용 레포지토리 구현

    public interface RedisTokenRepository extends CrudRepository<Token, String> {
    Optional<Token> findByRefreshToken(final String refreshToken);
    Optional<Token> findById(final Long id);
    }

  • AccessToken줄 때 RefreshToken Redis에 저장하고 같이 반환

    @Transactional
    public UserJoinResponse createMember(
    MemberCreateDto memberCreate
    ) {
    Member member = memberRepository.save(
    Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())
    );
    Long memberId = member.getId();
    String accessToken = jwtTokenProvider.issueAccessToken(
    UserAuthentication.createUserAuthentication(memberId)
    );
    String refreshToken = jwtTokenProvider.issueRefreshToken(
    UserAuthentication.createUserAuthentication(memberId)
    );
    //레디스에 저*장
    redisTokenRepository.save(Token.of(memberId, refreshToken));
    return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
    }

RefreshToken으로 AccessToken 재발급

  • 재발급 컨트롤러 구현

    @PostMapping("/refresh")
    public ResponseEntity<UserJoinResponse> refreshToken(){
    UserJoinResponse userJoinResponse = memberService.refreshToken(
    principalHandler.getUserIdFromPrincipal()
    );
    return ResponseEntity.status(HttpStatus.CREATED)
    .header("Location", userJoinResponse.userId())
    .body(
    userJoinResponse
    );
    }

  • 재발급 서비스 구현

    @Transactional
    public UserJoinResponse refreshToken(Long memberId) {
    //Refresh 토큰 만료: Redis에 해당 Refresh 토큰이 존재하지 않음
    if(!redisTokenRepository.existsById(memberId.toString())){
    throw new UnauthorizedException(ErrorMessage.INVALID_REFRESH_TOKEN);
    }
    //DB에 해당하는 유저 아이디가 있는지 확인
    findById(memberId);
    String accessToken = jwtTokenProvider.issueAccessToken(
    UserAuthentication.createUserAuthentication(memberId)
    );
    String refreshToken = jwtTokenProvider.issueRefreshToken(
    UserAuthentication.createUserAuthentication(memberId)
    );
    //레디스에 저*장
    redisTokenRepository.save(Token.of(memberId, refreshToken));
    return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
    }
    }

질문하고 싶은거!!

  1. Refresh 토큰 또한 JWT 토큰이기 때문에 Access 토큰과 동일하게 JWT 검증 필터를 거치게 했습니다.
    때문에 별도의 Refresh 토큰 검증 없이 (어차피 필터에서 다 해주니깐) PricipalHandler를 통해 memberId만 똑 떼와서 Redis에 해당 키값으로 Refresh 토큰이 있는지 (만료 시간 지나면 없을테니깐) 확인만 해주었는데 괜찮은 방법일까요?

  2. Redis는 동일 키값에 대해서 또 저장하면 덮어씌운다길래 기존 Refresh 토큰을 지우는 로직은 뺐는데 확실하게 해주기 위해 지우는 로직도 추가해야 할까요?

  3. Refresh 토큰을 통해 Access 토큰만 계속 재발급해주면 Access 토큰보다 만료시간이 짧아질 수 있다고 생각해서 항상 Refreh 토큰도 새로 만들었는데 괜찮은 방식일까요?

  4. SecurityConfig에서 /refresh 엔드포인트를 화이트리스트에 넣어줘도 계속 JWT 필터를 거치던데 정말 왜그런지 모르겠습니다..

    private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"};
    @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();
    })
    //UsernamePasswordAuthenticationFilter 앞에 jwtAuthenticationFilter를 넣겠다
    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant