본문 바로가기

백엔드 개발/WebSocket

SpringBoot WebSocket Chatting Server만들기 4단계 - SpringBoot Security 장착

0.목차

공부한 내용들의 출처: ddadyProgrammer님의 블로그

 

Spring websocket chatting server(4) - SpringSecurity + Jwt를 적용하여 보안강화하기

이번 장에서는 SpringSecurity와 Jwt를 이용하여 Web 및 Websocket의 보안을 좀 더 강화하고. 기존의 복잡한 로직을 간소화하는 작업을 진행해 보겠습니다. 크게 아래의 3가지 작업을 진행하겠습니다. Spri

www.daddyprogrammer.org

1. 개요 (새롭게 추가한 것들)
2. 전개도 
3. 코드 분석
4. 프로젝트 진행하며 만났던 오류 모음

 

1. 개요 

이번 장에서는 새롭게 Spring Security를 적용했다. 원래 프로젝트는 접근한 모든 사용자가 대화명만 입력하면 모두 사용할 수 있었다. 하지만 이번 장에서는 회원과 일반 방문자를 나누고, 회원만이 채팅 프로그램을 쓸 수 있도록 제약을 걸었다. 이렇게 유효성 검증을 통해 신뢰도가 있는 회원만 웹 서비스를 쓸 수 있도록 도와주는 Spring Library 중 하나가 Spring Security이다. 이번 장에서는  Spring  Security를 이용해 웹 서비스에 제약과 보안을 걸고, JWT를 이용해 사용자에 대한 유효성 검증을 할 것이다. JWT는 JSON WEB TOKEN의 약자이다. 클라이언트는 특정 페이지 이동이나 서비스 이용 시 발급 받은 JWT를 Header에 동봉하여 우리에게 보낸다. 우리는 해당 JWT가 유통기한이 지났는지, 유효한지 등을 체크한다. 그 후 유효성이 인정되는 사용자만 우리들의 서비스를 사용하도록 한다. 

2. 전개도 

이번 프로젝트의 전개도를 간략히 그려봤다. 

각 영역별로 더 확대해서 보여주겠다. 

바뀐 부분 중 가장 주목할만한 부분은 Redis Publisher가 사라지고, Subscriber가 간소화 된 것이다. 이전까지의 Chapter에서는 사용자가 특정 채팅방에 들어올 때마다 해당 채팅방 이름의 Topic을 새로 만들었다. 그리고 Topic을 만들때마다, 해당 Topic으로 발행된 메세지를 관리하는  SubScriber를 항상 새로 달아줘야 했다. 다음 그림과 같이 말이다. 

하지만 이번 장부터는 Topic객체를 프로그램 실행 시 딱 한번만 만든다.  그래서 subScriber도 RedisConfig에서 딱 한번만 쓰인다. 하나뿐인 TOPIC에 한번만 장착되기 때문이다. 원래 Publisher가 하던 메세지 발행 구현은 RedisTemplate를 이용한 Redis DB와 직접 CRUD를 하는 것으로 대체한다. 

3. 코드분석 

계층 구조는 다음과 같다.

 

ⓐ StompHandler 

WebSocket 연결하기 전에, 연결을 요청한 사용자가 유효한 TOKEN을 가진 사용자인지 확인하는 인터셉터 Handler이다. 자격 검증엔 JwtTokenProvider라는 우리가 만든 클래스의 객체를 이용한다. 

// WebSocket 연결 시 요청 header의 jwtToekn 유효성을 검증하는 코드
// 유효하지 않은 Jwt 토큰이 세팅될 경우 webSocket 연결을 하지 않고 예외 처리한다.
@Slf4j
@RequiredArgsConstructor // JwtTokenProvider에 대한 생성자 주입이 일어난다.
@Component
public class StompHandler implements ChannelInterceptor {
    private final JwtTokenProvider jwtTokenProvider;

    // webSocket을 통해 들어온 요청이 처리 되기 전에 실행된다.
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        // webSocket 연결 시 헤더의 jwt Token 검증
        if(StompCommand.CONNECT == accessor.getCommand()){
            jwtTokenProvider.validateToken(accessor.getFirstNativeHeader("token"));
        }

        return message;
    }

}

 

ⓑ EmbeddedRedisConfig는 저번과 같아서 생략 

ⓒ RedisConfig

Topic 생성의 단일화, 그에따라 Subscriber 등록도 딱 한번만 하게된다. 따라서 원래는 Service 단에서 구현했던 Sub로직을 RedisCOnfig에서 다룬다.  pub 로직은 사라지고, 대신 Chat Controller에서 RedisTemplate를 이용한 직접 Redis DB에 Post 작업을 한다. 

package org.websocket.demo.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.websocket.demo.pubsub.RedisSubscriber;

@RequiredArgsConstructor
@Configuration
public class RedisConfig {

    // 1) 단일 TOPIC 사용을 위한 Bean 설정
    // ChannelTopic은 Redis에 존재하는 Channel(Topic) 과 맵핑 되는 객체
    @Bean
    public ChannelTopic channelTopic() {
        return new ChannelTopic("chatroom");
    }

    // 2) redis 내장 pub/sub 기능을 사용하기 위해 Message Listener 설정을 추가
    // ++ redis 로 발행(publish)된 메세지를 처리하기 위한 리스너 설정
    @Bean
    public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter, ChannelTopic channelTopic) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, channelTopic);
        return  container;
    }

    // 3) 실제 메세지를 처리하는 subScriber 설정 추가
    @Bean
    public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber){
        return new MessageListenerAdapter(subscriber, "sendMessage");
    }


    // 4) 우리가 만든 어플리케이션에서 사용할 RedisTemplate 설정, 직렬화 어케 할 건지, RestTemplate 연결에는 어떤 팩토리를 쓰는지
    // RedisTemplate: Redis 데이터 베이스와 상호작용하기위한 설정을 담은 클래스

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));

        return redisTemplate;
    }
}

ⓓ WebSecurityConfig

여기선 chat/ ~~ 로 시작하는 페이지에는 무조건 로그인으로 토큰을 받은 사용자만 들어가도록 제한하는 로직을 짰다. 로직은 Spirng Security를 이용했다. 더해서, 현재는 Mysql 같은 RDS를 쓰지 않음으로, 사용자 설정 하는 로직도 추가했다. 해당 로직은 만약에 DB를 달 경우 적을 필요 없는 로직이다.  또한 비밀번호 암호화를 위한 BcryptPasswordEncoder를 추가했다.

package org.websocket.demo.config;

/*
* WebSecurityConfigurerAdapter 는 deprecated 되었음. 따라서 그 대안으로
* Component-based Security Configuration을 사용하는 것이 권장됨. configure() -> filterChain
*
* */

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());

        http.csrf(AbstractHttpConfigurer::disable) // 기본적으로 ON인 csrf 취약점 보안을 해제한다.
        // .header((s) -> s.frameOptions((a) -> a.sameOrigin()))
                .headers((header) -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // SockJS는 기본적으로 HTML ifram 요소를 통한 전송을 허용하지 않도록 설정되는데, 해당 설정을 해제한다.
                .formLogin(Object::toString) // 권한 없이 페이지 접근 시 로그인 페이지로 이동 시킴
                .authorizeHttpRequests((request) -> request.requestMatchers("/chat/*").hasRole("USER") // chat으로 시작하는 리소스에 대한 접근 권한 설정
                        .anyRequest().permitAll()); // 나머지 리소스에 대한 접근 설정
        return  http.build();
    }


    /*
    *  테스트를 위해 In-Memory에 계정을 임의로 생성한다.
    *  서비스에서 사용시에는 DB 데이터를 이용하도록 수정이 필요
    * */

    @Bean
    public InMemoryUserDetailsManager userDetailService() {
        UserDetails user = User.builder()
                .username("user1")
                .password(passwordEncoder().encode("1234"))
                .roles("USER")
                .build();

        UserDetails user2 = User.builder()
                .username("user2")
                .password(passwordEncoder().encode("1234"))
                .roles("USER")
                .build();

        UserDetails guest = User.builder()
                .username("guest")
                .password(passwordEncoder().encode("1234"))
                .roles("GUEST")
                .build();

        return new InMemoryUserDetailsManager(user,user2,guest);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

ⓔ WebSockConfig

여기선 별다를 것 없고,  configureCilentInboundChannel이란 매소드에서 아까 우리가 만들었던 Interceptor Handler인 StompHandler를 장착 시키는 로직을 마지막에 짰다. 이제 WebSocket에 연결하기 전에 Token이 있는 사용자인지 확인하게 된다. 

package org.websocket.demo.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.websocket.demo.config.handler.StompHandler;

// 이제 STOMP를 이용하여 WebSocket을 구현할 것이기 때문에 따로 WebSockHandler가 필요 없다.
// 왜냐하면 STOMP가 웹 소켓에서 메세지를 다루는 방법에 대한 규약이기 때문이다.

@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker // 웹소켓에서 STOMP를 활성화 하기 위한 어노테이션
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {

    private final StompHandler stompHandler;

    // 메세지 송 수신에 대한 설정을 등록한 매소드이다.
    // 메세지 수신은 sub/room/{방 번호}로 해당 방 번호로 온 메세지만 수신하도록 설정
    // 메세지 발신의 경우에는 pub/room 으로 보내고 방 번호는 responseBody 속 메타데이터로 저장한다.
    @Override
    public void configureMessageBroker (MessageBrokerRegistry config){

        //메세지를 발행하는 요청의 접두사는 /pub가 되도록 설정
        config.setApplicationDestinationPrefixes("/pub");

        //메세지를 구독하는 요청의 접두사는 /sub가 되도록 설정
        config.enableSimpleBroker("/sub");

    }


    // Stomp 소켓을 쓸 주소를 특정한다.
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){

        // stomp websocket으로 연결하는 주소의 endpoint는 /ws-stomp로 설정
        // 따라서 전체 주소는 ws://localhost:8080/ws-stomp
        registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*")
                .withSockJS();
    }


    // 인터셉터 설정 -> 우리가 만든 stompHandler를 Websocket 연결 전 처리기로 등록
    // WebSocket 최초 연결 시 유효성 검사
    @Override
    public void configureClientInboundChannel (ChannelRegistration registration){
        registration.interceptors(stompHandler);
    }

}

ⓕ Chat Controller

추가된 로직은 jwtTokenProvider를 이용해 토큰에 맞는 닉네임이 있는지 확인했다. 해당 과정에서 오류가 나면, 그 밑이 실행되지 않아서 오류가 난다. 토큰에 매칭되는 닉네임이 있다면 밑이 실행된다. 메세지를 Topic으로 발행하는 로직은 위에서 설명했듯이 RedisTemplate.convertAndSend로 대체되었다.

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import org.websocket.demo.model.ChatMessage;
import org.websocket.demo.repo.ChatRoomRepository;
import org.websocket.demo.service.JwtTokenProvider;

// WebSocket으로 들어오는 메세지를 처리하는 컨트롤러
// 발행과 구독은 config에서 설정했던 prefix로 구분
//      -> /pub/chat/message == pub 뒤에 주소로 메세지를 발행하는 요청
//      -> /sub/chat/message == sub 뒤에 주소로부터 발행되는 메세지를 받아오겠다는 요청

// 클라이언트가 채팅방 입장 시 채팅방(TOPIC)에서 대화가 가능하도록 리스너를 연동하는 enterChatRoom 메서드를 세팅
// 채팅방에 발행된 메세지는 서로 다른 서버에 공유하기 위해 redis의 Topic으로 다시 발행


@Slf4j
@RequiredArgsConstructor
@Controller
public class ChatController {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ChannelTopic channelTopic;
    private final JwtTokenProvider jwtTokenProvider;

    // @MessageMapping == 메세지가 WebSocket으로 발행되는 경우 밑의 매소드가 실행된다.
    @MessageMapping("/chat/message")
    public void message (ChatMessage message, @Header("token") String token) {

        //WebSocket 연결 후에 메세지 전송 시 마다 유효한 Token을 가졌는지 검사하는 코드
        // 만약 유효하지 않은 토큰을 가졌다면, if문 이하 코드가 실행 안될터이고, websocket을 통해 보낸 메세지는 무시된다.
        String nickname = jwtTokenProvider.getUserNameFromJwt(token);


        // 채팅방 입장 시에는 대화명과 메세지를 자동으로 세팅한다.
        if(ChatMessage.MessageType.ENTER.equals(message.getType())){
            message.setSender("[알림]");
            message.setMessage(nickname + "님이 입장하셨습니다.");
        }

        // 로그인 회원 정보로 대화명 설정
        message.setSender(nickname);

        //WebSocket에 발행된 메세지를 redis로 발행(publish)
        redisTemplate.convertAndSend(channelTopic.getTopic(), message);
    }
}

ⓖ ChatRoomController

중요한 게 추가되진 않았다. 다만 REST API를 통해 현재 토큰을 받고 접속한 User의 정보를 JSON 형태로 받아볼 수 있도록 하였다. 

package org.websocket.demo.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.websocket.demo.model.ChatRoom;
import org.websocket.demo.model.LoginInfo;
import org.websocket.demo.repo.ChatRoomRepository;
import org.websocket.demo.service.JwtTokenProvider;

import java.util.List;


// 웹 소켓 내용 아니고, 채팅 화면 View 구성을 위해 필요한 Controller
@RequiredArgsConstructor
@Controller
@RequestMapping ("/chat")
public class ChatRoomController {

    private final ChatRoomRepository chatRoomRepository;
    private final JwtTokenProvider jwtTokenProvider;

    // 1) 채팅 리스트 화면 반환
    @GetMapping("/room")
    public String rooms (Model model) {
        return "/chat/room";
    }

    // 2) 채팅방 생성 -> 하나의 Topic을 생성 (RestAPI )
    @PostMapping("/room")
    @ResponseBody
    public ChatRoom createRoom(@RequestParam String name){
        return chatRoomRepository.createChatRoom(name);
    }



    // 3) 모든 채팅방 목록 반환 (RestAPI)
    @GetMapping("/rooms")
    @ResponseBody
    public List<ChatRoom> room() {
        return chatRoomRepository.findAllRoom();
    }


    // 4) 채팅방의 입장 ->  해당 토픽을 구독한다는 뜻
    @GetMapping("/room/enter/{roomId}")
    public String roomDetail (Model model, @PathVariable String roomId) {
        model.addAttribute("roomId", roomId);
        return "/chat/roomdetail";
    }


    // 5) 특정 채팅방 조회
    @GetMapping("/room/{roomId}")
    @ResponseBody
    public ChatRoom roomInfo(@PathVariable String roomId) {
        return chatRoomRepository.findRoomById(roomId);
    }


    // 6) 로그인한 회원의 id 및 Jwt 토큰 정보를 조회할 수 있도록 하는 RESTFUL API
    @GetMapping("/user")
    @ResponseBody
    public LoginInfo getUserInfo() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName();
        return LoginInfo.builder().name(name).token(jwtTokenProvider.generateToken(name)).build();
    }
}

ⓗ ChatMessage와 ChatRoom은 바뀐 내용이 없어서 넘어간다.

ⓘ LoginInfo

현재 브라우저에 접속한 유저를 나타내는 Info 문이다. Builder를 쓸 수 있게 어노테이션 처리 했고 특별한 것은 없다.

package org.websocket.demo.model;

import lombok.Builder;
import lombok.Getter;


// id 및 jwt 토큰을 전달할 DTO
@Getter
public class LoginInfo {
    private String name;
    private String token;

    @Builder
    public LoginInfo (String name, String token){
        this.name = name;
        this.token = token;
    }
}

ⓙRedisSubscriber

Topic으로 발행된 메세지를 어떻게 처리할 것인지에 대한 로직을 담은 클래스이다. 

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber {
    private final ObjectMapper objectMapper;
    private final SimpMessageSendingOperations messagingTemplate;

    // Redis에서 메세지가 발행(publish)되면 대기하고 있던 Redis Subscriber가 해당 메세지를 받아서 처리한다.

    public void sendMessage (String publishMessage) {
        try {
            // 발행된 메세지를 chatMessage DTO에 맞게 객체 매핑
            ChatMessage chatMessage = objectMapper.readValue(publishMessage, ChatMessage.class);

            // 채팅방을 구독한 클라이언트에게 메세지 발송
            messagingTemplate.convertAndSend("/sub/chat/room/" + chatMessage.getRoomId(), chatMessage);
        }catch (Exception e){
            log.error("Exception {}", e);
        }
    }
}

 

ⓚChatRoomRepository

이름이 Repository인데 사실상 Service로직이다. 저번 포스팅 까지 여기서 Topic을 만들고 관리했었는데, 이제 그럴 필요가 없어졌다. Topic은 이제 RedisConfig에서 딱 하나 만들어서 관리하기 때문이다. 여기서는 RedisTemplate.opsForHash()라는 명령어로 DB 내의 저장소 하나를 얻고, createRoom 매소드일 시, 저장소에 새로운 채팅방 ID를 만들어서 저장, find~~ 매소드 일 시 저장소 내의 데이터를 조회하는 역할을 하고 있다. 

package org.websocket.demo.repo;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Repository;
import org.websocket.demo.model.ChatRoom;


import java.util.*;

// 채팅방을 생성하고 특정 채팅방 정보를 조회하는 Repository
// 생성된 채팅방은 초기화 되지 않도록 생성 시 Redis Hash에 저장하도록 처리
// 방 정보를 조회할 때는 Redis Hash에 저장된 데이터를 불러오도록 메서드 내용을 수정
// 채팅방 입장 시에는 채팅방 ID로 Redis Topic을 조회하여 pub/sub 메세지 리스너와 연동

@RequiredArgsConstructor
@Repository
public class ChatRoomRepository {
    // 채팅방에 발행되는 메세지를 처리할 Listener
    private final RedisMessageListenerContainer redisMessageListener;

    // Redis 안의 채팅방 저장소 이름을 CHAT_ROOMS으로 하겠다는 의미
    private static final String CHAT_ROOMS = "CHAT_ROOM";

    // Redis의 ChatRoom 저장소와 CRUD를 진행하기 위함.
    private final RedisTemplate<String, Object> redisTemplate;

    // chatRoom이란 이름의 HashMap에 <K: 방 번호, V: 채팅방 객체> 형태로 저장
    private HashOperations<String, String, ChatRoom> opsHashChatRoom;


    @PostConstruct
    private void init() {
        opsHashChatRoom = redisTemplate.opsForHash();
    }

    public List<ChatRoom> findAllRoom() {
        return opsHashChatRoom.values(CHAT_ROOMS);
    }

    public ChatRoom findRoomById (String id) {
        return opsHashChatRoom.get(CHAT_ROOMS, id);
    }

    // 채팅방 생성: 서버 간 채팅방 공유를 위해 redis hash에 저장한다.

    public ChatRoom createChatRoom(String name) {
        // 채팅방을 만들고
        ChatRoom chatRoom = ChatRoom.create(name);

        //Redis Hash에 저장
        opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom);

        // 그 후 만든 ChatRoom을 반환
        return chatRoom;
    }



}

ⓛ JwtTokenProvider

해당 클래스가 이번 포스팅의 핵심이다. 

이름을 암호화하여 토큰 생성 및 암호화, 암호화된 토큰을 복호화하여 다시 이름 얻기, 토큰 유효성 검증 로직 등이 존재한다. 

package org.websocket.demo.service;


import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Slf4j
@Component
public class JwtTokenProvider {

    @Value("${spring.jwt.secret}")
    private String secretKey;

    // 토큰의 유효기간
    private long tokenValidMilisecond = 1000L*60*60;

    //1) 이름으로 JWT TOKEN을 생성한다.
    public String generateToken(String name) {
        Date now = new Date();

        return Jwts.builder()
                .setId(name)
                .setIssuedAt(now) // 발행 일자
                .setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // 토큰의 수명
                .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화값 -> secret값을 바꿔서 세팅
                .compact();
    }

    //2) Jwt Token을 복호화 하여 이름을 얻는다.
    public String getUserNameFromJwt(String jwt) {
        return getClaims(jwt).getBody().getId();
    }

    //3) Jwt Token 유효성 체크
    public boolean validateToken(String jwt) {
        return this.getClaims(jwt) != null;
    }

    //4) 유효성 검증을 하는 실질적인 로직 -> 외부 접근 못하게 private로 막음
    private Jws<Claims> getClaims(String jwt) {
        try {
            return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt);
        }catch (SignatureException ex){
            log.error("Invalid JWT signature");
            throw ex;
        }catch (MalformedJwtException ex) {
            log.error("Invalid JWT token");
            throw ex;
        }catch (ExpiredJwtException ex) {
            log.error("Expired JWT token");
            throw ex;
        }catch (UnsupportedJwtException ex) {
            log.error("Unsupported JWT token");
            throw ex;
        } catch (IllegalArgumentException ex) {
            log.error("JWT claims string is empty. ");
            throw ex;
        }
    }

}

SecretKey는 암호화된 Token을 복호화할 때 사용할 Key값이다. 

        return Jwts.builder()
                .setId(name)
                .setIssuedAt(now) // 발행 일자
                .setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // 토큰의 수명
                .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화값 -> secret값을 바꿔서 세팅
                .compact();

로직을 보면, HS256으로 암호화 시에, 그걸 풀 수 있는 키로 secretKey를 설정하고 있음을 볼 수 있다. 

Claim이란 무엇인가요? 

Claim이란 Token에 담긴 실제 내용이다 즉, Token에 담긴 엔티티와 메타 데이터에 대한 명세서 이다. 따라서 Token의 반대 개념으로 Token을 복호화할 시에, Claim이 나온다. 

return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt);

해당 부분은 jwt가 인수로 받은 토큰일 때, 이를 검증하는 로직인데, JWS와 JWT에 대해선 나중에 다시 자세히 공부해봐야겠다. 일단 해당 부분의 흐름은 이해했으니 넘어가겠다. 

 

4. 프로젝트를 진행하며 겪었던 오류들

바인딩 오류: https://dalcheonroadhead.tistory.com/376