본문 바로가기
Flutter

JWT 구축 하기

by hyoungbin 2023. 10. 30.

 

 

JWT란

JWT는 "JSON Web Token"의 약자로, 웹 통신에서 사용되는 인증과 관련된 개념입니다. 즉, JWT는 클라이언트와 서버 간의 정보를 안전하게 전송하기 위해 사용됩니다.

  • JWT 구성

헤더(Header)

페이로드(Payload)

서명(Signature)

각 부분은 Base64로 인코딩되어 하나의 문자열로 표현됩니다.

  • JWT의 장점

Stateless - 세션 상태 관리 없이도 사용자 상태를 유지할 수 있으므로 확장성이 용이합니다.데이터 자체 포함 - 필요한 모든 정보가 자체적으로 포함되어 있으므로 별도의 데이터베이스 조회 없이도 필요한 정보에 접근할 수 있습니다. 범용화 - 다양한 프로그래밍 언어와 프레임워크에서 지원하며, 쉽게 구현할 수 있습니다.

하지만 주의해야 할 점도 있습니다:

보안 설정 - 비밀키(secret key)를 안전하게 보관하고 관리해야 합니다. 크기 제약 - JWT에 담길 수 있는 데이터 크기에 제약이 있는 경우도 있으므로 적절히 활용해야 합니다.

 

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

dependencies {
		... 생략 
    // JWT 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
}
  • io.jsonwebtoken 패키지의 라이브러리들은 JWT 생성, 검증 및 처리를 위해 필요합니다.
  • jjwt-api는 JWT의 API 인터페이스를 제공하고, jjwt-impl은 실제 구현체를 포함합니다.
  • jjwt-jackson은 JSON 직렬화 및 역직렬화를 위한 Jackson 라이브러리와의 통합을 제공합니다.

JWT 구축하기 (필터로 구현)

작업 순서

  1. JwtUtil 클래스 생성하기
  2. 필터 구현하기 - Request
  3. 필터를 특정 URI 패턴에만 동작 처리 하기
  4. 로그인 시 JWT 토근 생성 및 응답 처리 하기
  5. JWT 테스트

 

package com.tencoding.todo.utils;

import java.util.Date;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Component // Ioc 대상 된다. 
public class JwtUtil {
	
	// 시크릿 키를 생성 - xxkdk --> 동적 생성 ()
	private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
	
	private final long EXP_TIME = 86400000L; // 유효 시간 1일  
		
	// 토큰 생성하는 메서드를 만들어야 한다.
	public String generateToken(String userEmail, Integer userId) {
		return Jwts.builder()
				.claim("userEmail", userEmail)
				.claim("userId", userId)
				.setExpiration(new Date(System.currentTimeMillis() + EXP_TIME))
				.signWith(key)
				.compact();		
	}
	
	// 토큰이 유효한지 검증 메서드를 만들어야 한다.
	public Boolean validateToken(String token) {
		try {
			Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
			return true;
		} catch (Exception e) {
			return false;
		}
	}
	
	// 유저에 정보를 뽑고 싶은 메서드를 만들어야 한다. - email 
	public String getUserEmailFromToken(String token) {
		Jws<Claims> claimsJws = Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws("userEmail");
		
		return claimsJws.getBody().get("userEmail", String.class);
	}
	
	
	// 유저 정보에서 userId를 뽑는 메서드를 만들어 볼 예정 
	public String getUserIdFromToken(String token) {
		Jws<Claims> claimsJws = Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws("userId");
		
		return claimsJws.getBody().get("userId", String.class);
	}
	
}

필터 만들어 보기 - JwtUtil 클래스를 활용

package com.tencoding.todo.filter;

import java.io.IOException;
import java.net.http.HttpResponse;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.tencoding.todo.utils.JwtUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtRequestFilter implements Filter {
	
	@Autowired
	private JwtUtil jwtUtil;

	// doFilter -> 약속 : 요청이 오면 반드시 doFilter가 호출 된다. 
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		log.info("JWT Filter 동작 확인 - 1");
		
		HttpServletRequest httpServletRequest = (HttpServletRequest)request;
		String authHader = httpServletRequest.getHeader("Authorization");
		
		if(authHader != null) {
			// 토큰 존재 여부 확인 
			if(!authHader.startsWith("Bearer ")) {
				sendError(response); 
				return; 
			}
			
			// 토큰 존재 여부 확인 
			if(!authHader.startsWith("Bearer ")) {
				sendError(response);
				return;
			}
			

			String jwtToken = authHader.substring(7);
			// 토큰 유효성 확인 
			if(!jwtUtil.validateToken(jwtToken)) {
				sendError(response); 
				return; 
			}
			
			
		}
		
		
		
		log.info("JWT Filter 동작 확인 - 2");
		chain.doFilter(request, response);
		
	}

	private void sendError(ServletResponse response) throws IOException {
		HttpServletResponse httpServletResponse = (HttpServletResponse)response;
		httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token");
	}

}

3. 필터를 특정 URI 패턴에만 동작 처리 하기

package com.tencoding.todo.filter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

// 어노테이션은 해당 클래스가 스프링 프레임워크의 설정 정보를 담는 클래스를 나타낸다. 
// 이 Configuration @Component을 상속 받고 있다. 
// 이 클래스에 내부에서 Bean 객체를 더 생성해야 할 때 사용
@Slf4j
@Configuration 
public class FilterConfig {
	
	// 1. 우리가 정의한 JWT 관련된 필터 동작 객체를 생성자 의존 주의 받는다. 
	@Autowired
	private JwtRequestFilter jwtRequestFilter;
	
	// 특정 메서들 만들어서 URI 패턴을 등록 처리
	@Bean
	public FilterRegistrationBean<JwtRequestFilter> loggingFilter() {
		log.error("스프링 부트 구동시 초기화 확인 - 1");
		
		FilterRegistrationBean<JwtRequestFilter> registrationBean 
					= new FilterRegistrationBean<>();
		registrationBean.setFilter(jwtRequestFilter);
		registrationBean.addUrlPatterns("/todos/*");
		return registrationBean;
	}
	
	
	
}

UserController

package com.tencoding.todo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.function.ServerRequest.Headers;

import com.tencoding.todo.dto.UserDTO;
import com.tencoding.todo.repository.entity.UserEntity;
import com.tencoding.todo.service.UserService;
import com.tencoding.todo.utils.JwtUtil;

@RestController
@RequestMapping("/user")
public class UserController {
	
	private final UserService userService;
	// 코드 추가 
	private final JwtUtil jwtUtil;
	
	// 코드 수정
	@Autowired	
	public UserController(UserService userService, JwtUtil jwtUtil) {
		this.userService = userService;
		this.jwtUtil = jwtUtil;
	}
	
	// 주소 설계 
	// 회원 가입 요청 -- form, HTTP Message body
	@PostMapping("/sign-up")
	public ResponseEntity<?> signUp(@RequestBody UserDTO userDTO) {
		// 데이터 유효성 검사- 생략  
		int result = userService.signUp(userDTO); // 201 , 200, 404  
		return ResponseEntity.status(HttpStatus.CREATED).body(result);
	}  
	
	
	// 로그인 요청 --> 보안상 이유
	@PostMapping("/sign-in")
	public ResponseEntity<?> signin(@RequestBody UserDTO userDTO) {
		// 아이디만 확인 
		// 비번까지 확인 
		UserEntity user = userService.signin(userDTO); 
		// 세션 처리 ---> JWT
		if(user != null) {
			
			String token = jwtUtil.generateToken(user.getEmail(), user.getUserId());
			// 헤더 셋팅 
			HttpHeaders headers = new HttpHeaders();
			// JWT 헤더는 약속 Bearer , 으로 반드시 시작 해야 한다. 
			headers.add("Authorization", "Bearer " + token);
			
			return ResponseEntity.ok().headers(headers).body(user);
		} else {
			return new ResponseEntity<>("로그인 실패", HttpStatus.UNAUTHORIZED);
		}
	}
	
	// TEST
	// <http://localhost:80/user/token-test>
	@GetMapping("/token-test")
	public String testToken() {
		// eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyRW1haWwiOiJ0ZXN0QG5hdmVyLmNvbSIsInVzZXJJZCI6MTAsImV4cCI6MTY5ODQ3NDAxNn0.3SGGdFMME3FCVDNrFaQLOlqzN-i28khCAnG65PJe49BLdm2NdHK5AckXj-3AqQAV-Y10idjhdRiIOz2PR-OrMg
		return jwtUtil.generateToken("test@naver.com", 10);
	}
}

'Flutter' 카테고리의 다른 글

Flutter - RiverPod 이란  (0) 2023.11.01
theme 적용하기  (0) 2023.11.01
Flutter MVVM 패턴 - 2(TodoList)  (0) 2023.10.24
Flutter MVVM 패턴 - 1  (0) 2023.10.24
Flutter - HTTP 통신 하기  (0) 2023.10.23