JWT를 사용한 인증

김가빈·2023년 8월 31일
0

springsecurity

목록 보기
13/23
  • 이때까지 사용한 Spring security가 로그인 시 default로 생성해주는 JESSIONID cookie는 모든 유저 아이디를 담고 있는 것이 아니기 때문에 실제 어플리케이션에서 사용하는 것은 좋지 않다.
  • 또한 유저가 브라우저를 닫지 않았을 때 쿠키가 탈취당할 위험이 있다.
  • 따라서 JWT사용이 권장된다.

TOKEN

  • 토큰은 유저가 처음 로그인을 시도할 때 생기며, UUID나 JSON Web Token(JWT) 등의 타입으로 생성된다.
  • 유저가 로그인을 성공하면, 서버는 클라이언트에게 랜덤으로 생성한 token을 발급하고, 토큰을 세션에 저장한다.
  • 클라이언트는 resource 요청 시 해당 token과 함께 요청하게 되며, 서버는 받아온 token의 유효성을 검사한다.
  • 유효성 검사에 통과하면 resource를 클라이언트에게 반환한다.

token 사용 시 이점

  • 인증정보를 모든 request가 공유해 서버의 부하를 줄일 수 있다.
  • 의심스러운 요청에 대해서 유저 정보를 서버에 유지하면서 token만 무효화할 수 있다.
  • 짧은 life time을 가진다
  • 유저 role이나 인가 등 정보를 저장할 수 있다.

JWT

  • json 형식으로 되어 있다
  • header + payload + signature(Optional)을 포함한다.
  • base64로 인코딩 되어서 전달된다.

    업로드중..

signiture

  • 다른 네트워크 간 통신할 때 signiture 생성을 해서 jwt에 포함시키는 것이 좋다.
  • signiture는 SHA-256 알고리즘 등을 이용해 작성한 랜덤 문자열로 클라이언트는 통신을 요청할 때 해당 signiture까지 동일한 jwt를 전송했을 때만 해당 유저로 인식된다.
  • 또한 signiture는 hash string으로 클라이언트에서는 해석이 불가능하고 서버에서만 해석이 가능하다.
  • 해커가 jwt를 해킹하려고 하는 경우, signiture가 변경되며, 서버에서 쉽게 감지할 수 있다.

실습

  • jwt를 사용하기 위해서는 spring security에서 제공하는 sessions id가 아니라 내가 custom할 필요가 있다.
  • 해당되는 부분을 ALWAYS를 STATELESS로 교체한다.
    업로드중..
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))	
  • Authorization으로 전송할 jwt token을 클라이언트에 노출할 수 있도록 설정한다.
    • csrf token의 경우 spring security에서 제공하는 것이므로 별도의 설정이 없어도 클라이언트에 노출되지만, jwt의 경우 내가 직접 전송하는 것이기 때문에 다음과 같은 설정이 필요하다.
    • 내가 서버에서 설정한 jwt token을 Authorization에 담아서 보내고 이것을 클라이언트의 header에서 확인하는 작업을 할 것이다.
    • 따라서 setExposedHeaders의 값은 생성하는 jwt에서 setHeader에 설정된 값과 동일해야한다.
      업로드중..
  • 토큰 생성을 위해 다음과 같이 작성한다.
package com.eazybytes.filter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.crypto.SecretKey;

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

import com.eazybytes.constants.SecurityConstants;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class JWTTokenGeneratorFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (null != authentication) {
			SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
			String jwt = Jwts.builder().setIssuer("Eazy Bank").setSubject("JWT Token")
					.claim("username", authentication.getName())
					.claim("authorities", populateAuthorities(authentication.getAuthorities()))
					.setIssuedAt(new Date())
					.setExpiration(new Date((new Date()).getTime() + 30000000))
					.signWith(key).compact();
			response.setHeader(SecurityConstants.JWT_HEADER, jwt);
		}
		
		filterChain.doFilter(request, response);
	}

	@Override
	protected boolean shouldNotFilter(HttpServletRequest request) {
		return !request.getServletPath().equals("/user");
	}
	
	private String populateAuthorities(Collection<? extends GrantedAuthority> collection) {
		Set<String> authoritiesSet = new HashSet<>();
		for(GrantedAuthority authority : collection) {
			authoritiesSet.add(authority.getAuthority());
		}
		
		return String.join(",", authoritiesSet);
		
		
	}
}
package com.eazybytes.constants;

public interface SecurityConstants {

	public static final String JWT_KEY = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
	public static final String JWT_HEADER = "Authorization";
}
  • showNotFilter메소드를 통해 엔드포인트가 "/user"인 경우에만 적용한다는 것을 확인할 수 있다.
  • authentication 객체가 있는지 확인하고 해당 객체가 있을 때에만 JWT생성 로직을 실행한다.
    • 즉 로그인을 통해 유저 정보가 인가된 경우에만 jwt를 생성한다.
  • hmacShaKeyFor을 통해서 sign에 필요한 서명을 만든다.
  • setIssuer, setSubject, claim, setIssuedAt, setExpiration, signWith: JWT의 헤더, 클레임 (claim), 발급일자, 만료일자, 서명 등을 설정하고 토큰 생성한다.

  • security config class에 해당 filter가 돌 수 있도록 다음과 같이 설정한다.
    업로드중..

  • 추가로 filter를 생성해준다.

package com.eazybytes.filter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import javax.crypto.SecretKey;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import com.eazybytes.constants.SecurityConstants;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class JWTTokenValidatorFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		String jwt = request.getHeader(SecurityConstants.JWT_HEADER);
		if(null != jwt) {
			try {
				SecretKey key = Keys.hmacShaKeyFor(
						SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
				
				Claims claims = Jwts.parserBuilder()
									.setSigningKey(key)
									.build()
									.parseClaimsJws(jwt)
									.getBody();
				
				String username = String.valueOf(claims.get("username"));
				String authorities = (String) claims.get("authorities");
				Authentication auth = new UsernamePasswordAuthenticationToken(username, null, 
							AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
				SecurityContextHolder.getContext().setAuthentication(auth);
				
			} catch(Exception e) {
				throw new BadCredentialsException("Invalid Token received!");
			}
		}
	}
	
	@Override
	protected boolean shouldNotFilter(HttpServletRequest request) {
		return request.getServletPath().equals("/user");
	}

}
  • 위의 filter는 생성된 JWT token에서 정보를 추출해 SecurityContextHolder에 authentication 객체를 만드는 역할을 한다.
    • jwt 자체는 서버에 저장되지 않으며 jwt에서 추출한 정보가 securityContextHolder에 저장된다.
    • 모든 인증정보는 jwt가 가지고 있으므로 서버는 따로 jwt를 저장하지 않는 stateless를 유지한다.
  • parseClaimsJws(jwt)을 통해서 jwt문자열을 파싱하고 시그니처의 유효성을 확인한다.
    • 그 과정에서 setSigningKey를 통해서 전달한 key를 이용한다.
    • 통과되지 않을 경우 exception이 발생한다.

  • 마찬가지로 security config class에 해당 filter가 돌 수 있도록 다음과 같이 설정한다.
    업로드중..
profile
신입 웹개발자입니다.

0개의 댓글