ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TIL] JWT로 인증 인가 구현하기
    TIL(Today I Learned) 2023. 1. 5. 12:11

    *20230105의 회고

     

     

       Naver API 를 이용하여 관심상품을 검색하고, 저장하고, 조회하는 서비스를 개발하던 중 인증/인가에 대한 부분을 JWT 로 해결하였다. 그 과정을 기록하려 한다.

     

    1. 문제점

    - 웹 통신을 하는데, 클라이언트의 정보를 서버가 모두 가지고 있어야 할까? 그렇다면 클라이언트의 정보가 많아지면, 서버의 부하가 많이 걸리지 않을까?

    - Http 통신은 비연결성, 무상태로 이루어 지는데, 클라이언트에 유저 정보를 저장한다고 치면, 어떤 서비스를 요청할 때 마다 서버로 id와 password 를 보내줘야 할까?

     

    2. 해결 방법과 알게된 점

    - JWT 를 이용하여 인증 인가 구현을 한다.

    - JWT 를 API 요청 시 마다 Header에 포함해서 요청한다.

    - 서버는 secret key 를 통해 JWT 위조 여부를 검증하고, 토큰을 이용하여 유저인지 아닌지 판단하여 데이터를 응답해준다.

     

    - <JwtUtil 클래스 - 토큰 생성, 검증>

    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class JwtUtil {
        public static final String AUTHORIZATION_HEADER = "Authorization"; // 헤더에 들어가는 authorization 과 bearer 부분의 키값
        public static final String AUTHORIZATION_KEY = "auth"; // 사용자 권한 값의 키, 사용자 권한도 토큰안에 값을 넣어줄 건데 그거를 가지고 올 때 사용되는 키 값
        private static final String BEARER_PREFIX = "Bearer "; // 토큰을 만들 때 같이 앞에 붙어서 들어가는 부분
        private static final long TOKEN_TIME = 60 * 60 * 1000L; // 토큰 만료 시간에 사용할 시간 -> 1시간
    
        @Value("${jwt.secret.key}") // @Value : 어노테이션이 필드나 메서드(혹은 생성자)의 파라미터 수준에서 표현식 기반으로 값을 주입해 줌
        private String secretKey;
        private Key key; // 토큰을 만들 때 넣어줄 키 값 , 여기서는 secretKey를 넣어줄거임(secretKey를 디코드해서)
        private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //HS256 이라는 알고리즘 으로 암호화 할거임
    
        @PostConstruct // 처음에 객체가 생성될 때 초기화 하는 함수, 결론적으로 secretKey 를 디코드 해서 key 에 넣어주는 함수
        public void init() {
            byte[] bytes = Base64.getDecoder().decode(secretKey); // secretKey 가 base64로 인코딩이 되어 있기 때문에, 디코드를 하고 값을 넣어줌.(반환 값은 byte 배열임)
            key = Keys.hmacShaKeyFor(bytes); // secretKey를 디코드 한 결과를 key 에 넣어줌.
        }
    
        // header 토큰을 가져오기
        public String resolveToken(HttpServletRequest request) { // HttpServletRequest 객체 안에는 우리가 가져와야 할 토큰이 Header에 들어있음.(HttpServletRequest 안에는 사용자의 요청에 대한 모든 정보가 담겨 있다)
            String bearerToken = request.getHeader(AUTHORIZATION_HEADER); // request 안에 있는 header 값을 가져옴, 파라미터로 어떤 키를 가지고 올지 넣어줌.
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { // 토큰이 존재하는지, 혹은 bearer로 시작하는지 체크한 후에
                return bearerToken.substring(7); // 'bearer' 지우고 나머지 토큰 반환해줌(토큰에 연관이 되지 않는 그냥 string 은 빼고 반환)
            }
            return null;
        }
    
        // 토큰 생성
        public String createToken(String username, UserRoleEnum role) { // JWT를 만들어 주는 메서드
            Date date = new Date();
    
            return BEARER_PREFIX +
                    Jwts.builder()
                            .setSubject(username)
                            .claim(AUTHORIZATION_KEY, role)
                            .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                            .setIssuedAt(date)
                            .signWith(key, signatureAlgorithm)
                            .compact(); // 위의 과정을 통해 username, AUTHORIZATION_KEY, role, 현재시간, TOKEN_TIME, key, signatureAlgorithm 정보를 이용한 String 형식의 JWT 토큰이 만들어져 반환됨
        }
    
        // 토큰 검증
        public boolean validateToken(String token) {
            try {
                Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); // 검증할 토큰을 받아서 넣어주고, 우리가 만든 key 값을 넣어서 검증을 해주는 메서드
                return true;
            } catch (SecurityException | MalformedJwtException e) {
                log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
            } catch (ExpiredJwtException e) {
                log.info("Expired JWT token, 만료된 JWT token 입니다.");
            } catch (UnsupportedJwtException e) {
                log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
            } catch (IllegalArgumentException e) {
                log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
            }
            return false;
        }
    
        // 토큰에서 사용자 정보 가져오기
        public Claims getUserInfoFromToken(String token) {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); //validateToken 부분에서 getBody 를 추가해서 정보를 가져오는 메서드, 검증이 되었다고 가정해서 try,catch 문 없음
        }
    
    }

     

    - <ProductController>

    @RestController
    @RequestMapping("/api")
    @RequiredArgsConstructor
    public class ProductController {
    
        private final ProductService productService;
    
        // 관심 상품 등록하기
        @PostMapping("/products")
        public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, HttpServletRequest request) {
            // 응답 보내기
            return productService.createProduct(requestDto, request);
        }
    
        // 관심 상품 조회하기
        @GetMapping("/products")
        public List<ProductResponseDto> getProducts(HttpServletRequest request) {
            return productService.getProducts(request);
        }
    
        // 관심 상품 최저가 등록하기
        @PutMapping("/products/{id}")
        public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto, HttpServletRequest request) {
            // 응답 보내기 (업데이트된 상품 id)
            return productService.updateProduct(id, requestDto, request);
        }
    
    } // HttpServletRequest 객체로 클라이언트 요청을 받아온다. -> request 헤더에 JWT 토큰 받아올 수 있음

    - <UserController> 의 login 부분 -> HttpServletResponse 객체에 생성된 JWT 를 넣어서 응답값으로 클라이언트에게 보내줌

    @ResponseBody
    @PostMapping("/login") // http 요청에 body 쪽에 ajax로 데이터가 들어가기 때문에 @RequestBody 붙여줘야 함 // 서버에서 클라이언트 쪽으로 데이터를 반환할 때 HttpServletResponse 객체 사용
    public String login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) { // 데이터를 반환할 response 객체에 Header 부분에 우리가 만든 Token 을 넣어 주기 위해서 http~객체 사용
       userService.login(loginRequestDto, response);
       return "success";
    }

     

    - <ProductService> 의 관심상품 등록 로직 부분, 이런식으로 JWT 를 검증해서 로직을 구현하면 된다.

    @Transactional
        public ProductResponseDto createProduct(ProductRequestDto requestDto, HttpServletRequest request) {
            // Request 의 Header 에서 Token 가져오기
            String token = jwtUtil.resolveToken(request);
            Claims claims; // JWT 안에 들어있는 담을 수 있는 객체 정도로 이해하기
    
            // 토큰이 있는 경우에만 관심상품 추가 가능
            if (token != null) {
                if (jwtUtil.validateToken(token)) {
                    // 토큰에서 사용자 정보 가져와서 claims 객체에 넣어주기
                    claims = jwtUtil.getUserInfoFromToken(token);
                } else {
                    throw new IllegalArgumentException("Token Error");
                }
    
                // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
                User user = userRepository.findByUsername(claims.getSubject()).orElseThrow( // claims.getSubject() 하면 user 의 이름을 가져올 수 있음
                        () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
                );
    
                // 요청받은 DTO 로 DB에 저장할 객체 만들기
                Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));
    
                return new ProductResponseDto(product);
            } else { // 토큰이 없으면 null 반환
                return null;
            }
        }
Designed by Tistory.