본문 바로가기
DB/Redis

TIL 230901 : Redis 2 - RefreshToken구현하기. 발급 및 Redis저장. (Spring Security)

by 우인입니다 2023. 9. 4.

 

 

https://thiswooin.tistory.com/92

 

 

TIL 230830 : Redis 1 - Spring에서 연동하기 (lettuce, jedis, RedisTemplate, CRUDRepository 활용)

지난 시간에 로컬로 Redis서버를 실행하고 RedisInsight라는 GUI를 이용해 Redis 데이터를 저장해보는 기본적인 기능을 테스트 해봤다. 이번엔 Spring에서 Redis로 CRUD 요청을 보내는 세팅과 기본적인 CRUD

thiswooin.tistory.com

 

지난 시간까지 알아 본 Redis를 Spring에서 연동하는 방법.

이번에는 RedisRepository 방식을 활용하여, 기존 Access Token만 발행하던 인증인가 시스템에서

2시간 기한의 AccessToken과 2주 기한의 RefreshToken을 생성하여 AccessToken이 탈취당했을 때의 취약점을 조금이나마 더 보완하고, 유저 경험적으로도 짧은 AccessToken의 기한으로 인한 잦은 재발행을 위한 행동 없이 자동으로 갱신할 수 있도록 개선해보려 한다.

 


0. 현재 코드

build.gradle

    //jwt 부분
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-mail'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-web-services'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    compileOnly 'org.projectlombok:lombok'

 

무엇보다도 Security환경에서 진행되고 있고, 다른 의존성도 혹시 몰라 최대한 끌어와봤습니다.

 

 

Jwt

  • JwtAuthenticationFilter : 인증에 대한 기능을 구현하는 필터 클래스.
  • JwtAuthorizationFilter : 인가 관련 기능 구현 필터 클래스.
  • JwtUtil : Jwt토큰 생성, 가져오기 등 유틀 클래스.

 

WebSecurityConfig

// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

아까 위에서 설명된 두 가지 필터를 WebSecurityConfig클래스에 위 코드처럼 필터로 등록해 두었다.

 

1. JwtAuthorizaionFilter : 우선 인가정보가 있는지 확인. (이미 인가 정보가 있으면 인증이 필요없으니)

2. JwtAuthenticationFilter : 인가정보가 없는 경우 인증과정을 거친다. 결론적으로, UsernamePasswordAuthenticationFilter에서 사용될 Authentication을 set하게 된다.

 

인증인가 순서가 조금 헷갈린다.

필터동작되는 과정 요모조모를 어느정도 타협하고 이해하고 넘어갔어서 더 흔들리는 듯 하다.

 

 

1. RefreshToken 발행하기

 

우선은 AccessToken만 발행되던 기존의 코드에 같은 방식으로 RefreshToken을 생성해준다.

 

JwtUtil

   //Refresh Token 관련 멤버
    public static final String REFRESH_TOKEN_HEADER = "Refresh-Token";
    private final long REFRESH_TOKEN_TIME = 14 * 24 * 60 * 60 * 1000L; // 14일 세팅
    
    ...
    
        public String createRefreshToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)
                        .claim(AUTHORIZATION_KEY, role)
                        .setExpiration(new Date(date.getTime() + REFRESH_TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();
    }

 

 

JwtAuthenticationFilter

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("로그인 성공 및 JWT 생성");

        // refactor 토큰을 한번에 처리할 수 있게 합칠 수도 있지 않을까?
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String accessToken = jwtUtil.createToken(username, role);
        String refreshToken = jwtUtil.createRefreshToken(username, role); //refresh token 생성

        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
        response.addHeader(JwtUtil.REFRESH_TOKEN_HEADER, refreshToken);

    //클라이언트로 헤더에 담아서 데이터를 보내주는 것 까지만 됐다. 이제 서버측에도 저장 해둬야한다.

기존에 가져와진 username, role 데이터를 똑같이 claims에 포함하여 RefreshToken을 만들어 두기로 했다.

이 부분은 다들 많이 다른데 우선은 Token을 갱신할 때, RefreshToken에서 바로 데이터를 가져오고 싶어서 이후에 수정했던 부분이다.

 

 

2. RefreshToken Redis에 저장하기

JwtAuthenticationFilter (추가)

...
String accessToken = jwtUtil.createToken(username, role);
String refreshToken = jwtUtil.createRefreshToken(username, role); //refresh token 생성
...
//생성한 accessToken, refreshToken와 함께 Redis상에서 Key역할을 해 줄 값을 같이 담아서 저장해준다.
refreshTokenRepository.save(new RefreshToken(((UserDetailsImpl) authResult.getPrincipal()).getUser().getUsername(), refreshToken, accessToken));

아까 위에서 수정한 JwtAuthenticationFilter 코드 아래에 Redis저장해주는 코드를 추가해준다.

 

여기서 new RefreshToken은 Dto의 역할로 만든 Entity 이다.

RefreshToken.class

@AllArgsConstructor
@Getter
@RedisHash(value = "refreshToken", timeToLive = 14*24*60*60) // 일*시*분*초 14일로 설정해 둠.
public class RefreshToken {

    @Id // 이 태그를 통해 키 값이 된다.
    private String username;

    private String refreshToken;

    private  String accessToken;

RefreshToken의 만료시간인 14일과 동일하게 설정해두었다.

 

 

Redis 저장 확인

현재 Repository를 활용하여 저장하기 때문에 위와 같은 형태로 저장된 것을 확인할 수 있고,

Time To Live 14일이 제대로 적용된 것도 확인할 수 있다.

 

그리고 @Id에 Username을 설정해두어서 아래처럼 이를 통한 조회도 가능하다.

Username은 이후에 변동가능성이 있는 값이라 PK값인 user_id로 변경해줘야할 듯하다.

 

RefreshTokenRepository

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
    Optional<RefreshToken> findByAccessToken(String accessToken);
}

 

 


느낀점

 

이번 만큼 구글링하며 본 코드들이 각양각색인 적이 또 없었던 것 같다.

그만큼 각기다른 코드들도 동작을 하는 걸 보면 새삼 개발자의 능력이 대단해 보인다.

맞으면 '왜 맞지?', 틀리면 '왜 틀리지?' 끊임없이 고민하며 성장할 수 밖에 없는 직무인 것 같다.

매력적이다.