카테고리 없음

JWT + Security 구현하며 생긴 고민&생각&해결

Mon Groy 2024. 9. 6. 13:51

refresh JWT를 발급할 때는 확인해야 하는 사항이 없는 것일까?

 

- refreshToken은 accessToken을 발급하면서 함께 발급이 된다

- 그렇다고 매개변수로 아무것도 받지 않는 것은 좋지 못하다고 생각하는 이유가, 위변조를 하기 쉬워질 것이기 때문이다

- 따라서 authentication을 매개변수로 받아서 username 이나 authorities를 세팅해 놓는 게 좋겠다

 

>>>>>

createToken 메서드 작성시 Jwt를 build 하는데 설정할 수 있는 것들 중에

ID도 설정할 수 있다는 사실을 알았다!

일반적으로 jwi라고 부르며, 설정 메서드는 .setID()

 

추가로 

1. setIssuer() // 발행한 사람 기록

2. addClaims() 또는 setClaims() 

도 쓸모있는 것 같아서 적어 놓는다

 

Map<String, Object> claims = new HashMap<>();
claims.put("role", "admin");
claims.put("email", "user@example.com");

//위 코드를 작성해 놓고 한번에 설정하거나 추가설정할 때 사용!

 


refreshToken에 설정해야 하는 사항은 accessToken 이랑 다를까?

 

- accessToken 을 재발급할 때 사용되는 것이다

- 그 때 refreshToken을 확인하므로 accessToken 과 연관된 정보가 들어가 있으면 좋지 않을까?

왜냐하면 다른 브라우저에서 사용하던 refreshToken이 같이 왔는데 대충 확인해서 accessToken을 재발급해줘버리면 안 될 테니까

- 따라서 만료된 accessToken의 발급시간과 refreshToken의 발급시간이 동일한지 확인해보면 좋을 것 같다 (이 부분은 accessToken 재발급 메서드에서 설정해야 겠다)

 


setID() 를 제대로 사용하기 위해서는 테이블이 필요할까?

- 그렇다. ID 값을 저장하고 상태를 관리해야 하므로 테이블이 필요하다

- 일정 기간마다 삭제하도록 설정하는 것도 필요하겠다

- 따라서 jwt 객체도 설정해야 한다

 

더보기

(생각해본) 테이블을 따로 설정하지 않고 확인해서 새 accessToken을 발급하는 방법

 

accessToken을 create 할 때 UUID를 생성하고

그 UUID 와 같은 값을 refreshToken에도 사용

대신 UUID를 암호화 알고리즘을으로 바꿔 놓는다

 

accessToken 을 재발급 할 때, 만료된 accessToken 의 UUID 와

refresh 토큰의 알고리즘화된 UUID를 디코딩한다음에

두 UUID를 비교해서 동일할 경우 accessToken 을 발급해 준다

 

>> 성능과 비용상 문제 발생 가능


.setIssuer(ISSUER)

 

jwt 발급시 위 메서드를 사용했을 때의 장점

 

- 누가 발급했는지 명시되므로 클라이언트에 신뢰를 줄 수 있다 >> 조작하면 되니까 그다지?

- Issuer가 명시되지 않거나 발급자가 다른 경우 골라내 차단할 수 있음 >> 이건 좋은 것 같음

- 다른 서비스에서 사용할 때 신뢰성 확보 >>이것도 조작하면 되니까 그다지?

- 한 사이트의 여러 페이지나 시스템에서 다른 토큰들을 발급하는 경우 각각 설정해 두면 구별할 수 있으므로, 유지보수에 도움이 된다 >> 조작되지 않는다는 가정하에 유용한 듯


jwt 관련 filter 와 security 관련 filter 순서?

 

jwt 와 security를 동시에 적용하려고 하다보니 filter의 순서에 대해서 생각하게 되었다

JwtConfig의  securityFilterChain() 을 작성하면서

jwt 관련 필터인 JwtAuthenticationFilter, JwtAuthorizationFilter 두 가지와 UsernamePasswordAuthenticationFilter 

이 세 가지의 순서를 정하는 중인데,

 

기존 수업에서는

http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

이 순서로 가르침 받았다

* jwtAuthorizationFilter가 JwtAuthenticationFilter 앞, JwtAuthenticationFilter 가 UsernamePasswordAuthenticationFilter 앞

* 즉, JwtAuthorizationFilter >> JwtAuthenticationFilter >> UsernamePasswordAuthenticationFilter 인 것

 

로그인(인증)을 한다면

security 를 거친 후, jwt를 발급하고

 

인가를 한다면 jwt를 이미 가지고 있는 상태이기 때문에

로그인처럼 해도 되지만

jwt를 확인하고 security를 거쳐도 상관이 없지 않을까?

 

안전을 생각한다면 security를 먼저 거치는 것이 낫지 않을까? 라는 생각이 들어서

security를 앞에 두는 걸로 생각이 기울고 있다

 

또한,

jwt 발급을 받을 때 매개변수로 Authentication을 넣어 놓았기 때문에 security가 먼저 오는 걸로 하는게 맞는지도 모르겠다

 

결정

JwtAuthorizationFilter   >> UsernamePasswordAuthenticationFilter  >>   JwtAuthenticationFilter 

 

이유

1. 로그인시,

UsernamePasswordAuthenticationFilter 를 통해서 인증받고, 그 결과물인 Authentication을 context에 저장,

JwtAuthenticationFilter 에서 토큰을 발행한다

 

2. 데이터 요청시

Http에서 jwt를 추출하여 JwtAuthorizationFilter 에서 인가를 받고 context에 저장한다

그 후 UsernamePasswordAuthenticationFilter >> JwtAuthenticationFilter 과정은 생략한다

 

따라서 주어진 내게 주어진 과제는

두 경우를 따로 설정하고 따로 적용할 수 있도록 코드 짜기


위의 과제를 해내는 중에
antMatchers 랑 requestMatchers 가 사용되는 것을 발견했고, 차이점이 궁금해졌다

 

antMatchers

- URL 패턴에 따라 요청을 매칭하고, 이 요청에 대한 보안 규칙을 설정함

- 패턴 매칭을 위해 Ant 스타일의 패턴 사용

- 예시

http
        .authorizeRequests()
        .antMatchers("/public/**").permitAll()  // 모든 사용자에게 허용
        .antMatchers("/admin/**").hasRole("ADMIN")  // ADMIN 역할을 가진 사용자만 접근 허용
        .anyRequest().authenticated();  // 그 외의 모든 요청은 인증이 필요

 

 

  • 특징:
    • Ant 스타일의 패턴을 사용하여 경로를 매칭합니다.
    • 상대적으로 간단한 패턴 매칭을 제공합니다.

 

 

requestMatchers

- 요청 매칭을 위한 더 유연한 방법을 제공하며, URL 외에도 HTTP 메서드, 헤더, 파라미터 등 다양한 속성을 기반으로 요청을 매칭할 수 있습니다.

- RequestMatcher 인터페이스를 사용하여 맞춤형 매칭을 구현할 수 있습니다.

- 예시

http
        .authorizeRequests()
        .requestMatchers("/public/**").permitAll()  // 요청 매칭
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers(new AntPathRequestMatcher("/**")).authenticated();  // Ant 스타일 패턴 사용

 

 

  • 특징:
    • URL뿐만 아니라 HTTP 메서드, 헤더, 파라미터 등으로 매칭 조건을 설정할 수 있습니다.
    • RequestMatcher 인터페이스를 직접 구현하여 복잡한 매칭 조건을 설정할 수 있습니다.

공통

  • 패턴:
    • * : 하나의 디렉토리 또는 파일을 매칭합니다 (예: /images/*).
    • ** : 모든 디렉토리 및 파일을 재귀적으로 매칭합니다 (예: /images/**)
    • ? : 하나의 문자와 매칭합니다 (예: /file?.txt).

요약

  • antMatchers는 Ant 스타일의 패턴을 사용하여 요청 URL을 매칭하는 방법입니다. 간단하고 직관적인 패턴 매칭을 제공합니다.
  • requestMatchers는 RequestMatcher 인터페이스를 사용하여 더 유연한 매칭이 가능합니다. URL, HTTP 메서드, 헤더 등 다양한 속성을 기반으로 매칭을 설정할 수 있습니다.

Spring Security 필터 리스트?

 

https://velog.io/@seongwon97/Spring-Security-Filter%EB%9E%80

 

[Spring Security] Filter란?

Spring Security의 Filter에 관한 전반적인 설명입니다.

velog.io

https://memodayoungee.tistory.com/134

 

[Spring Security] 스프링 시큐리티와 FilterChain

Spring Security란? Spring Security: Spring MVC 기반 애플리케이션의 인증(Authentication)과 인가(Authorization or 권한 부여) 기능을 지원하는 보안 프레임워크. Spring 기반 애플리케이션 보안을 위한 사실상의 표

memodayoungee.tistory.com

https://gngsn.tistory.com/160

 

Spring Security, 제대로 이해하기 - FilterChain

Spring Security의 인증, 인가 과정을 FilterChain을 살펴보며 이해하는 것이 본 포스팅의 목표입니다. 해당 포스팅은 1부 Spring Security, 어렵지 않게 설정하기의 이은 포스팅이지만, 읽는데 순서는 상관

gngsn.tistory.com

 


인증된 유저를 담는  repository 를 

일반적으로 사용하던 유저repository 와

동일시 할 것인가 (즉, 일반적으로 사용하던 유저repository를 그대로 사용할 것인가)

아니면 따로 만들어서 각각 사용할 것인가?

 

구글링을 해 보면, 두 경우로 나뉘는데, 일반적 유저 repository를 함께 사용하는 경우가 많았다

그래서 고민해보게 되었다

 

인증유저와 일반적유저 엔티티 각각에

적히는 정보가 다르기때문에 따로 사용하는 게 낫다는 판단

 

토큰 관련한 정보를 인증된 유저를 담는 repository에 넣어두면 편할 거라는 판단

 

따로 해 두면 너무 많은 정보가 프론트에 나가지 않을 거라는 생각

특히 password 를 삭제처리해 버리기 때문에 좋을 것 같다는 생각

 

따라서 따로 사용하기로 함!

 


AuthenticationFilter는 UsernamePasswordAuthenticationFilter를 구현하는데

작동 순서에 UsernamePasswordAuthenticationFilter도 포함이 되어있다 (필터이므로)

그렇다면 UsernamePasswordAuthenticationFilter가 두 번 사용되는 것이 아닌가?

 

- 아니라고 함. 구체적인 건 나중에 적겠음

 


 

successfulAuthentication() 메서드를 오버라이드 하려고 보면

매개변수로 FilterChain chain 을 받고 있다

이는 다음 필터로 보낼 때

chain.doFilter() 를 사용하기 위해서 존재하는데,

현재의 successfulAuthentication()  메서드에서는 인증이 끝나고 JWT를 발급받는 메서드이기때문에

다음으로 진행할 사항이 없다

따라서 doFilter()를 사용할 필요 없이 응답하고 끝나면 되므로, chain을 사용할 일이 없다

 


 

authenticatedUser 클래스를 사용하는 때는 언제?

 

처음 이 클래스를 만들 때, User user를 사용하기에는 정보가 너무 많이 담겨있기 때문에

인증관련만 필요할 때 사용하기 위해서 만들었었다

그래서 refresh 토큰을 담아두고 기한이 지난 것들은 정기적으로 삭제를 하면 되겠거니 생각했다

 

@Service
public class TokenCleanupService {

    @Autowired
    private AuthenticatedUserRepository authenticatedUserRepository;

    @Scheduled(cron = "0 0 0 * * ?") // 매일 자정에 실행
    public void cleanupExpiredTokens() {
        // 만료된 토큰을 삭제하는 로직
        authenticatedUserRepository.deleteExpiredTokens();
    }
}

 

>>시간마다 삭제하는 경우

 

@Service
public class TokenService {

    @Autowired
    private AuthenticatedUserRepository authenticatedUserRepository;

    public void updateRefreshToken(String userId, String newToken) {
        // 기존의 토큰 삭제
        authenticatedUserRepository.deleteByUserId(userId);

        // 새로운 토큰 저장
        authenticatedUserRepository.save(new AuthenticatedUserDetails(userId, newToken));
    }
}

>> 새로운 토큰이 들어오면 기존 repository의 열은 삭제하는 경우

 

이렇게 사용하려면 repository를 생성해서 사용해야 하는데

 


OncePerRequestFilter()

 

이 필터는 크게 하는 일이 없는 것 같다

속 내용을 살펴보면 상속을 받아서 원하는 기능을 넣도록 짜여져 있는 것으로 보인다

 

일단 필터 이름으로 실행된 필터에 속하는지확인하고,

속해있으면 다음 필터를 실행하도록 한다

속행있지 않을 경우는 doFilterInternal() 메서드를 실행하도록 짜여져 있다

 

그래서 ONcePerRequestFilter를 상속받은 클래스에서 doFilterInternal() 메서드를 작성하면 원하는 Filter 기능을 넣을 수 있고, Filter 단에서 실행하도록 만들 수 있는 것이다

 

그래서 AuthorizationFilter 를 커스텀할 때 일반적으로 이 클래스를 상속받아서 작성하는 것인가 보다


AuthorizationFilter 의 doFilterInternal() 메서드를 작성하기만하면

Filter 단에서 알아서 실행이 되니까

원하는 Url에서의 실행순서만 잘 설정해주고 메서드를 작성하면 되는 것

 

메서드에 들어갈 내용으로는

일단, 액세스JWT가 있는지 확인한다

 

 

1. 있을 경우 통과 (데이터 허용) 후 응답이 필요한 경우 응답하기

 

2. 없을 경우 리프레시JWT 확인

>> 살아있을 경우 액세스JWT 재발급 (재발급 조건에 맞는지 확인과정 필요) 후 필요시 응답하기

>> 만료거나 없을 경우 그에 맞는 조치 (나의 경우는 로그인하라고 보낼 것) 후 필요시 응답하기

 

코딩할 내용

1. JWT 가져오기

- HEADER에서 액세스JWT 이름의 claim에서 value를 찾아온다

- 찾아온 액세스JWT를 분해한다 ("Bearer  " 제거)

- 분해한 JWT를 검증한다

2. 검증 메서드 작성

- 검증 내용 : 

3. 검증된 액세스 JWT