이번 포스팅에선 Spring security와 로그인을 합치기 위해
첫번째로 필터를 개발하도록 하겠다.
필터의 역할은 클라이언트가 요청할 때마다 헤더의 담긴 jwt 토큰을 읽어 유효성 검증을 통해 유효하다면 SecurityContext에 올려주고, 유효하지않다면 인증실패처리를 해주는 기능을 가지고있다.
그 후 만든 필터를 SecurityConfig 파일을 만들어 등록해주는 것 까지 해보겠다.
우선 TokenFilter.class를 만들어주자
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
}
}
이것이 내가 만들 토큰필터의 기본 구조이다.
이제 이 필터에 매 요청시 검증하는 로직을 만들것이다.
일단 검증 단계를 설계해보자 나같은 경우는 우선
1. 헤더에 토큰 속성이 존재하는지
2. 존재한다면 Bearer 로 시작하는 정상 토큰인지
3. 만료시간이 초과된 토큰인지
이렇게 세단계를 우선 검증해볼것이다.
이 단계를 검증하려면 TokenProvider 클래스에 만료시간 체크 메서드 추가가 필요하니 그 메서드부터 만들어주자
TokenProvider.class
public static boolean isExpired(String token) {
Date expiredDate = extractClaims(token).getExpiration();
return expiredDate.before(new Date());
}
그 후 앞서 말한 세가지 검증을 Filter 단에 추가해보자
@RequiredArgsConstructor
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
//헤더에 토큰을 담는 속성이 없으면 비로그인으로 판단
if (authorizationHeader == null) {
filterChain.doFilter(request, response);
return;
}
//Bearer 로 시작하는 정상 토큰인지 유효성 검사
if (!authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request,response);
return;
}
String token = authorizationHeader.split(" ")[1];
//토큰 만료시간 검사
if (TokenProvider.isExpired(token)) {
filterChain.doFilter(request, response);
return;
}
}
}
이제 세가지 단계를 넘어간 경우 정상적으로 시스템에서 사용이 가능한 토큰임을 검증했다.
그리고 토큰에서 필요한 정보를 꺼내서 나머지 로직을 진행하면 된다.
그리고 토큰에서 필요한 정보를 꺼내쓰는 메서드를 만들고
이 때 이전에는 토큰안에 넣지않았지만 향후 권한정보가 필요할 수 있으므로
TokenProvider에서 토큰생성코드 수정과 필요한 몇가지 메서드를 만들어주자
public static String createToken(User user) {
Date expiryDate = Date.from(
Instant.now()
.plus(1, ChronoUnit.DAYS)
);
Claims claims = Jwts.claims();
claims.put("id", user.getId());
claims.put("username", user.getUsername());
//추가 된 부분
claims.put("role", user.getRole());
return Jwts.builder()
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.setClaims(claims)
.setExpiration(expiryDate)
.compact();
}
public static Long getUserIdFromToken(String token) {
return extractClaims(token).get("id", Long.class);
}
public static String getUsernameFromToken(String token) {
return extractClaims(token).get("username", String.class);
}
public static String getRoleFromToken(String token) {
return extractClaims(token).get("role", String.class);
}
그리고 사용자 정보를 인증객체로 생성하기 위해 UserDetails를 상속받는 클래스를 생성해주자
권한 부분은 일단 간단하게 만들고 추후 권한에 따른 설정같은게 늘어나면 기능확장을 하면 된다.
CustomUserDetails.class
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final String userRole;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();
if (userRole.equals("ADMIN")) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
if (userRole.equals("MEMBER")) {
authorities.add(new SimpleGrantedAuthority("ROLE_MEMBER"));
}
if (userRole.equals("GUEST")) {
authorities.add(new SimpleGrantedAuthority("ROLE_GUEST"));
}
return authorities;
}
@Override
public String getPassword() {
return "";
}
@Override
public String getUsername() {
return "";
}
}
이런 느낌으로 만들어주자
그리고 UsernamePasswordAuthenticationToken 객체를 하나 생성해서 CustomUserDetails 로 만든 인증객체에 사용자 정보도 넣어 스프링 시큐리티에서 사용할 수 있게 만들어 준다.
다음으로는 요청에 대한 인증 정보를 스프링 시큐리티 컨택스트에 넣는거로 인증 인가 부분을 마무리 할 수 있다.
전체코드
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
//헤더에 토큰을 담는 속성이 없으면 비로그인으로 판단
if (authorizationHeader == null) {
filterChain.doFilter(request, response);
return;
}
//Bearer 로 시작하는 정상 토큰인지 유효성 검사
if (!authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authorizationHeader.split(" ")[1];
//토큰 만료시간 검사
if (TokenProvider.isExpired(token)) {
filterChain.doFilter(request, response);
return;
}
Long userId = TokenProvider.getUserIdFromToken(token);
String username = TokenProvider.getUsernameFromToken(token);
String role = TokenProvider.getRoleFromToken(token);
CustomUserDetails userDetails = new CustomUserDetails(role);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
}
}
요약하자면 TokenFilter 에서는 jwt 토큰의 유효성을 검증하고
검증한 후
이러한 핵심 기능을 수행하는 단계라고 볼 수 있다.