JWT (JSON Web Token)

dbswl31024·2021년 10월 5일
0

Spring

목록 보기
1/5

1. JWT (JSON Web Token)

  • JSON 포맷을 사용 해 사용자의 속성을 저장하는 Web Token
  • JSON 객체를 사용 해 가볍고, 자가 수용적인 (self-contained)방식으로 안전하게 정보를 전달
  • 이 때 사용 되는 JSON 데이터는 URL-Safe하도록 URL에 포함 할 수 있는 문자만으로 구성 됨
  • 클라이언트 - 서버 간 정보를 주고 받을 때, HTTP Request Header에 JSON token을 넣은 후 서버로 전달. 서버는 유효한 token인지 확인 후, 통제 및 허가

자가수용적 (self-contained)방식?

  • 필요한 모든 정보를 자체적으로 지니고 있어서 자신의 객체를 사용 하는 것

2. JWT 구성 요소

  • '.'을 구분 문자로, 3가지의 문자열로 구성 됨
  • 헤더(header), 내용(payload), 서명(signature)

1) 헤더 (header)

{
	"typ" : "JWT",
	"alg" : "HS256"
}
  • typ
    • 토큰의 타입 지정 (ex : JWT)
  • alg
    • 알고리즘 방식을 지정 함
    • 서명(Signature) 및 토큰 검증에 사용 됨

2) 내용 (payload)

  • 토큰을 담을 정보가 들어 있음
  • 정보의 한 조각을 클레임(claim)이라고 부름

(1) 등록된 (registered) 클레임

  • 서비스에 필요한 정보가 아닌, token에 대한 정보를 담기 위해 이미 이름이 정해진 클레임

(2) 공개 (public) 클레임

  • 사용자 정의 클레임
  • 충돌을 방지하기 위해 URL 포맷 사용

(3) 비공개 (private) 클레임

  • 등록된 클레임도 아니고, 공개 된 클레임도 아닌 것
  • 클라이언트 - 서버 협의하에 사용되는 클레임 이름 들

3) 서명 (SIGNATURE)

  • token을 인코딩하거나, 유효성을 검증 할 때 사용하는 암호화 코드

3. JWT 통신 과정

[ 동작 과정 ]

(1) 사용자가 id, pwd를 입력하여 로그인 시도
(2) 서버는 요청을 확인하고, Access token 발급. 클라이언트에게 전달
=> 이 때, Refresh token도 같이 발급 됨
(3) 사용자가 API를 요청 할 때마다, HTTP request header에 Access token을 담아서 서버에게 보냄
(4) 서버는 JWT Signature를 체크하고, Payload로부터 사용자 정보를 확인해 데이터를 반환
⇒ 이 과정에서, 서버는 사용자에 대한 session을 유지 할 필요 없다!
⇒ 사용자가 요청했을 때, token만 확인하면 되므로 서버 자원과 비용 절감 가능
(5) 만약, Access token의 유효기간이 만료됐다면, Refresh token을 사용

Refresh token?

  • Access token보다 유효기간이 긴 토큰
  • 만약 Refresh token도 만료 됐다면, 사용자는 새로 로그인을 수행 해야 함
  • 보안 취약점과 잦은 로그인 수행 시 번거로움을 완화하기 위해 사용

4. JWT 적용

1) WebConfigurer.java

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebConfigurer extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private JwtFilter jwtFilter;

	// 비밀번호 암호화 객체 생성
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	// 비밀번호 암호화 적용
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
	}

	@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable().authorizeRequests()
			.antMatchers("/**/**").permitAll()
			.anyRequest().authenticated()
				.and().exceptionHandling().and().sessionManagement()
				.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		// CORS를 적용할 URL패턴을 정의
		registry.addMapping("/**")
				// 자원 공유를 허락할 Origin을 지정
				.allowedOrigins("*")
				.allowedMethods("*");
	}
}
  • Web에 대한 기본 설정 해 줌
  • JwtFilter를 선언하고, PasswordEncoder 객체를 통해 비밀번호 암호화가 적용되도록 함

2) JwtFilter.java

@Component
public class JwtFilter extends OncePerRequestFilter {
	@Autowired
	private JwtUtil jwtUtil;
	
	@Autowired
	private UserDetailsServiceImple userDetailsServiceImple;

	@Override
	protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
			FilterChain filterChain) throws ServletException, IOException {
		String authorizationHeader = httpServletRequest.getHeader("x-access-token");
		String token = null;
		String userName = null;
		
		if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
			token = authorizationHeader.substring(7);
			userName = jwtUtil.extractUsername(token);
		}
		
		// 유효한 토큰인지 확인
		if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
			UserDetails userDetails = userDetailsServiceImple.loadUserByUsername(userName);
			if (jwtUtil.validateToken(token, userDetails)) {
				UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = 
						new UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
				usernamePasswordAuthenticationToken
						.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
				SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
			}
		}
		filterChain.doFilter(httpServletRequest, httpServletResponse);
	}
}
  • OncePerRequestFilter 클래스 상속 받음
  • HTTP header에 담긴 token의 유효성을 검사 함

3) JwtUtil.java

@Service
// token을 생성, 정보를 추출, 유효성을 검사하는 역할
public class JwtUtil {
	private String secret = "javatechie";

	public String extractUsername(String token) {
		return extractClaim(token, Claims::getSubject);
	}

	public Date extractExpiration(String token) {
		return extractClaim(token, Claims::getExpiration);
	}

	public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
		final Claims claims = extractAllClaims(token);
		return claimsResolver.apply(claims);
	}

	private Claims extractAllClaims(String token) {
		return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
	}

	private Boolean isTokenExpired(String token) {
		return extractExpiration(token).before(new Date());
	}

	public String generateToken(String username) {
		Map<String, Object> claims = new HashMap<>();
		return createToken(claims, username);
	}

	private String createToken(Map<String, Object> claims, String subject) {
		return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 6))
				.signWith(SignatureAlgorithm.HS256, secret).compact();
	}

	public Boolean validateToken(String token, UserDetails userDetails) {
		final String username = extractUsername(token);
		return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
	}

}
  • token을 생성, 정보를 추출, 유효성을 검사

4) UserDetailsServiceImple.java

@Service
public class UserDetailsServiceImple implements UserDetailsService {
	@Autowired
	private UsersMapper usersMapper;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		TokUsers user = usersMapper.getUserByUserName(username);
		return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getUserPassword(),
				new ArrayList<>());
	}
}
  • Database에 접근해서 사용자 정보를 가져 옴
  • (user name, user password, user 권한)

0개의 댓글