[JWT] JWT +Security 권한 부여에 대한 고찰

시나브로·2021년 6월 12일
3

JWT

목록 보기
3/4
post-thumbnail

0. 들어가기에 앞서


  프로젝트에 JWT를 적용하며 고민했던 것들에 대해 정리합니다. 다양한 의견들 남겨주시면 감사하겠습니다.




1. Security + JWT


  Spring Security와 JWT를 연계할 때, 여러 글들을 확인해본 결과 Filter를 사용하여 토큰을 확인한 뒤, UsernamePasswordAuthenticationToken()를 사용하여 권한을 부여한다.
  이 방식을 사용했을 때, 장점은 Security가 제공하는 인증 Process를 그대로 사용할 수 있다는 장점이 있다. 사실상 말 그대로 연계다.
  다만 단점은 인증 및 UserDetails 객체 생성을 위해 UserName을 기반으로 DB에 접근한다는 점이다.

  여기서 드는 의문점.

매 요청마다 DB에 조회하여 Token 내부의 UserId가 존재하는지 확인해야할 필요가 있을까

한명의 사용자가 100번의 요청을 보내면, 100번의 Select문이 발생한다.
100명의 사용자가 100번의 요청을 보내면 10,000번의 Select문이 발생한다.

JWT 기반 인증을 사용하는 이유가 세션 기반을 사용할 때 발생하는 메모리 부하를 DB 부하로 Trade Off 하는 이유였던가. 메모리 부하를 DB 접근 부하로 해결하는 목적이라면 단순히 Redis를 통해 세선 스토리지를 구축하면 해결될 일이다.


JWT Document를 보면 JWT 정의에 대해 다음과 같이 설명되어있다.

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

JWT의 정의는 당사자간의 JSON을 안전하게 전송하기 위함이라 나와있다. 또 권한과 관련된 설명은 다음과 같다.

Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token.

한번 로그인하면 JWT를 통해 해당 토큰으로 허용 된 경로, 서비스 및 리소스에 액세스 할 수 있다고 설명한다.


다시 돌아가서 Filter에서 UsernamePasswordAuthenticationToken() 사용하는 것을 생각해봤을 때, 단순 권한 부여하기 위해 db 접근하는 것은 매우 비효율적이라 생각한다. 애초에 PayLoad에 권한 정보가 담겨져 있지 않는 경우가 아니라면 해당 정보를 기반으로 권한이 부여된 Authentication 객체를 임의 주입하면 해결될 일이다.
추가적인 User에 대한 데이터가 필요하다면 그 때, DB 접근을 하면 될 일이다.



구현


권한은 이미 PayLoad에 있을 것이기에 이를 기반으로 Authentication를 커스텀하여 강제 주입했다


JwtAuthentication.class

 
public class JwtAuthentication implements Authentication {
	
	private static final Logger logger = LoggerFactory.getLogger(JwtAuthentication.class);

	private final String token;
	private String key;
	private String role;
	private boolean isAuthenticated;

	public JwtAuthentication(TokenDto dto) {
		this.token = dto.getToken();
		this.key = dto.getKey();
		this.role = dto.getRole();
		this.isAuthenticated = true;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		logger.debug("Getting authorities");
		if (isAuthenticated) {
				return Collections.singletonList(new SimpleGrantedAuthority(role));
		}
		return Collections.singletonList(new SimpleGrantedAuthority(Role.ANONYMOUS.getValue()));
	}

	@Override
	public Object getCredentials() {
		return token;
	}

	@Override
	public Object getDetails() {
		return null;
	}

	@Override
	public Object getPrincipal() {
		return null;
	}

	@Override
	public boolean isAuthenticated() {
		return isAuthenticated;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		this.isAuthenticated = isAuthenticated;
	}

	@Override
	public String getName() {
		if (key != null && isAuthenticated)
			return key;
		return null;
	}
}



필요한건 권한과 차후 User 정보를 조회할 UserId이지 다른 부가적인 데이터는 필요없기 때문에 UserDetails도 구현하지 않았다.

인증이 되지 않은 사용자 접근이 있을 수도 있기에 AnonymousAuthentication도 따로 생성해두었다.


AnonymousAuthentication.class


@Component
public class AnonymousAuthentication implements Authentication {

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return Collections.singletonList(new SimpleGrantedAuthority(Role.ANONYMOUS.getValue()));
	}

	@Override
	public Object getCredentials() {
		return null;
	}

	@Override
	public Object getDetails() {
		return null;
	}

	@Override
	public Object getPrincipal() {
		return null;
	}

	@Override
	public boolean isAuthenticated() {
		return true;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {

	}

	@Override
	public String getName() {
		return null;
	}
}



Filter에서는 Token을 검증한 뒤 다음과 같이 권한을 부여한다.


JwtAuthenticationFilter.class


    private void setAuthentication(TokenDto dto) {
        if (!dto.getToken().isEmpty()) {
            JwtAuthentication authentication = new JwtAuthentication(dto);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    }

    private void setAnonymousAuthentication() {
        SecurityContextHolder.getContext().setAuthentication(anonymousAuthentication);
    }



User 정보가 필요하다면

SecurityContextHolder.getContext().getAuthentication().getName(); 을 통해 정보를 취득하면 될 것이다.

매 요청마다 User 정보를 조회했던 쿼리를 많이 감소시킬 수 있게 되었다.




구현 방안은 다르지만 이와 같은 생각을 정리한 블로그가 있어 참조를 남긴다. 같은 생각을 볼 수 있어서 반가웠다.

[서버개발캠프] Spring Security + Refresh JWT DB접근없이 인증과 파싱하기




profile
Be More!

1개의 댓글

comment-user-thumbnail
2024년 8월 20일

의문점이 하나 생기네요.
권한이 페이로드에 있다면 매 요청시마다 그 토큰에 담긴 권한 값을 신용한다는 이야기인데
만약에 이게 변조되어 넘어온다면 가지고 있지 않는 권한마저 취득 할 수 있게 되는거 아닐까요?
변조되어서 인증된다는거 자체가 시크릿키가 노출됐다는거긴 하지만 그래도 최후의 방어선으로 DB에서 교차 검증은 해야하지 않을까싶네요.

하지만 DB 조회율을 많이 줄인다는것에 대해서는 좋은 아이디어네요.

답글 달기