Spring Security, JWT, Redis (2)

Ajisai·2024년 2월 26일
0

Spring Security

목록 보기
3/7

https://velog.io/@kirisame/Spring-Security-JWT-Redis-1

이 글과 이어진다.

남은 할 일

  • Spring security 곁들이기
    • OAuth가 아닌 ID, PW로 하는 일반적인 로그인을 상정한다.
    • 요청 시 헤더에서 토큰을 끄집어내고, 토큰을 검증하는 작업이 필요하다.
      • 즉 토큰 인증을 위한 Custom filter가 필요하다.
    • 토큰 인증 후에는 인증 정보가 Spring security에 등록되어야 한다.
      • 정확히는 Security context라는 곳에 저장되어야 한다.
      • Security context는 Bean으로 치면 Spring container같은 것.
      • 인증 정보는 org.springframework.security.core.Authentication 타입이다.

Spring Security

무엇이 필요한가?

  1. 토큰 인증을 위한 custom filter
  2. 여러 설정 정보(로그인 방법 등)
    • 로그인 방식
    • 토큰 인증 필터의 위치(필터 체인 내 순서)
    • 필터 체인 정의
  3. 인증 정보 (org.springframework.security.core.Authentication) 등록을 위한 기능

무엇을 먼저 해야 하는가?

일단 토큰 필터가 필요하고, 토큰 필터에서는 다음을 수행한다.
1. 요청 헤더에서 토큰 정보를 꺼낸다.
2. 토큰에서 적절한 claim을 뽑아낸다.
3. 얻어낸 claim을 SecurityContext에 등록한다.

정확히는 토큰 필터에서 수행할 기능을 먼저 정의해야 한다.
단 요청 헤더에서 토큰 정보를 꺼내는 건 실제로 요청을 받는 필터에서 수행해야 한다.

AuthTokenProvider

public interface AuthTokenProvider {
    ...
    Authentication getAuthentication(AuthToken token);
}

AuthTokenProviderImpl

@Override
public Authentication getAuthentication(AuthToken token) {
	if (validate(token)) {
		Claims claims = token.getClaims(key);

		String role = (String)claims.get("role");
		Collection<? extends GrantedAuthority> authorities = Collections.singletonList(
			new SimpleGrantedAuthority(role));
		UserAuth userAuth = new UserAuth(
			Long.parseLong((String)claims.get("userId")),
			Role.valueOf(role)
		);

		// UsernamePasswordAuthenticationToken implements Authentication
		return new UsernamePasswordAuthenticationToken(
			UserPrincipal.create(userAuth),
			token,
			authorities
		);
	} else {
		throw new JwtException("Invalid token");
	}
}
  • 여기서 하는 작업은 토큰에서 적절한(SecurityContext에 등록할) claim을 얻어 SecurityContext에 등록할 수 있는 형태로 반환하는 것이다.
  • 일반 로그인을 사용하므로 UsernamePasswordAuthenticationToken 타입으로 반환한다.
  • UserPrincipalorg.springframework.security.core.userdetails.UserDetails의 구현 클래스다.
  • authorities에는 사용자의 역할이 들어간다.
    • 역할은 하나일 수도 있고 여러 개일 수도 있다.

UserPrincipal

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class UserPrincipal implements UserDetails {
	private final String id;
	private final Role role;
	@Getter
	private final GrantedAuthority authority;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return null;
	}

	@Override
	public String getPassword() {
		return "[PROTECTED]";
	}

	@Override
	public String getUsername() {
		return id;
	}

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

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

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

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

	public static UserPrincipal create(UserAuth user) {
		return new UserPrincipal(
			Long.toString(user.getId()),
			user.getRole(),
			new SimpleGrantedAuthority(user.getRole().name())
		);
	}
}
  • UserDetails의 구현 클래스는 Spring security에서 Username(ID), Password 기반 사용자 인증 객체다.
    • 정확히는 getUsername()getPassword() 등 메소드의 반환 값으로 UserDetails 구현체가 생성되고,
      이 구현체가 SecurityContext에 등록된다.
    • 그런데 이게 쓰이려면 SecurityConfig에서 form login을 사용하도록 설정해야 되는데, 나는 하지 않았다.
    • 그래서 사실상 쓰이지 않는 메소드들이다.
  • 여기서는 getUsername()에서 사용자 sequence number를 String으로 반환하고 있다.
    • SecurityContext에는 JWT를 등록하고, stateless이므로 이 JWT로 요청한 사람이 해당 JWT의 주인인지 체크하지 않는다.

Role

public enum Role {
	ADMIN,
	USER
}

사용자 역할 Role을 위와 같이 정의했으나 사실 관리자 기능을 구현하지 않았다...
그래서 AuthTokenProvidergetAuthentication() 메소드에서 다중 역할을 전제하고 있지만 실제로 반환되는 건 항상 Role.USER다.

AuthTokenFilter

import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import com.example.demo.auth.token.AuthToken;
import com.example.demo.auth.token.AuthTokenProvider;

import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;


/*
 * @Component 등 Bean으로 등록하는 경우
 * security filter chain이 아닌 default filter chain에 등록된다.
 */
@RequiredArgsConstructor
// 인증은 한 번만 이루어져야 하므로 OncePerRequestFilter를 상속받는다.
public class AuthTokenFilter extends OncePerRequestFilter {
	private final AuthTokenProvider tokenProvider; 

	@Override
	protected void doFilterInternal(
		HttpServletRequest request,
		HttpServletResponse response,
		FilterChain filterChain
	) throws ServletException, IOException {
		String token = request.getHeader("Authorization"); //헤더에서 토큰 정보를 얻는다.

		try {
			// claim을 얻어냄으로써 다음을 검증한다.
			// 1. 우리가 발급한 토큰이 맞는가?
			AuthToken authToken = new AuthToken(token);
			if (tokenProvider.validate(authToken)) {
                //토큰
				Authentication authentication = tokenProvider.getAuthentication(authToken);
				SecurityContextHolder.getContext().setAuthentication(authentication);
			}

			filterChain.doFilter(request, response);
		} catch (JwtException je) {
            //예외 처리
		} 

	}

}
  • 이 필터를 거쳐 토큰 인증이 완료된다.
  • 헤더에 토큰 정보가 없는 경우 tokenProvider.validate(authToken)에 의해 JwtException이 발생한다.
  • 헤더에 유효한 토큰이 있는 경우 앞에서 추가한 기능을 사용해 인증 정보를 SecurityContext에 등록한다.
  • doFilterInternal()에는 이 필터에서 처리할 내용을 정의한다.
  • doFilter()를 호출하면 doFilterInternal()이 호출된다
    • 즉 내가 정의한 내용이 처리된다.

인증이 불필요한 요청은 어떻게 되나요

즉 헤더에 토큰이 없이 요청을 보내는 경우는 Security configuration에서 허용을 해줘야 한다.
하지만 요청을 허용하는 것과 별개로 필터를 거치지 않는 건 아니다.
허용한다 ≡ 필터를 거치되 그냥 넘어긴다이며, 필터를 거치는 과정에서 발생하는 예외는 직접 처리해줘야 한다.
여기서 '넘어간다'는 Spring Security 입장이다. 즉 넘어가지 않으면 Security에 의해 예외가 발생하는데, 이 예외에 대한 처리도 Spring Security에서 작성한다.
Security configuration과 예외 처리는 다음 글에서 작성한다.

하려고 했던 일

  • Spring security 곁들이기
    • OAuth가 아닌 ID, PW로 하는 일반적인 로그인을 상정한다.
    • 요청 시 헤더에서 토큰을 끄집어내고, 토큰을 검증하는 작업이 필요하다.
      • 즉 토큰 인증을 위한 Custom filter가 필요하다.
    • 토큰 인증 후에는 인증 정보가 Spring security에 등록되어야 한다.
      • 정확히는 Security context라는 곳에 저장되어야 한다.
      • Security context는 Bean으로 치면 Spring container같은 것.
      • 인증 정보는 org.springframework.security.core.Authentication 타입이다.

일단 다 했다.

할 일

  • SecurityConfig
    • Spring security를 적용하기 위한 구성 요소(AuthToken, AuthTokenProvider, AuthTokenFilter, ...)를 정의했으므로, 이를 사용하기 위한 Spring security Configuration을 작성한다.
    • AuthTokenProvier, AuthTokenFilter를 Spring bean으로 등록한다.
    • 사실 AuthTokenFilter라는 Custom filter를 정의했을 뿐, 실제로 요청이 이 filter를 거치는 게 아니다.
      • 요청이 filter를 거치게 하려면 FilterChain에 등록해야 하며, 이 절차 역시 SecurityConfig에서 작성한다.
  • AuthTokenFilter 예외 처리
    • 모든 요청에서 인증 토큰을 필요로 하는 것은 아니다.
      따라서 이에 대한 처리가 필요하다.
profile
Java를 하고 싶었지만 JavaScript를 하게 된 사람

0개의 댓글

관련 채용 정보