이 글과 이어진다.
org.springframework.security.core.Authentication
타입이다.org.springframework.security.core.Authentication
) 등록을 위한 기능일단 토큰 필터가 필요하고, 토큰 필터에서는 다음을 수행한다.
1. 요청 헤더에서 토큰 정보를 꺼낸다.
2. 토큰에서 적절한 claim을 뽑아낸다.
3. 얻어낸 claim을 SecurityContext에 등록한다.
정확히는 토큰 필터에서 수행할 기능을 먼저 정의해야 한다.
단 요청 헤더에서 토큰 정보를 꺼내는 건 실제로 요청을 받는 필터에서 수행해야 한다.
public interface AuthTokenProvider {
...
Authentication getAuthentication(AuthToken token);
}
@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");
}
}
UsernamePasswordAuthenticationToken
타입으로 반환한다.UserPrincipal
은 org.springframework.security.core.userdetails.UserDetails
의 구현 클래스다.authorities
에는 사용자의 역할이 들어간다.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
으로 반환하고 있다.public enum Role {
ADMIN,
USER
}
사용자 역할 Role
을 위와 같이 정의했으나 사실 관리자 기능을 구현하지 않았다...
그래서 AuthTokenProvider
의 getAuthentication()
메소드에서 다중 역할을 전제하고 있지만 실제로 반환되는 건 항상 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과 예외 처리는 다음 글에서 작성한다.
org.springframework.security.core.Authentication
타입이다.일단 다 했다.
SecurityConfig
AuthToken
, AuthTokenProvider
, AuthTokenFilter
, ...)를 정의했으므로, 이를 사용하기 위한 Spring security Configuration을 작성한다.AuthTokenProvier
, AuthTokenFilter
를 Spring bean으로 등록한다.AuthTokenFilter
라는 Custom filter를 정의했을 뿐, 실제로 요청이 이 filter를 거치는 게 아니다.SecurityConfig
에서 작성한다.AuthTokenFilter
예외 처리