Feign Client에서 Header로 Jwt 보내기

0

트러블슈팅

목록 보기
6/9

webhook을 이용해서 Slack과 연동해 워크스페이스 채널에 메시지를 작성하는 기능을 구현해보았다.
구글링으로 친절하게 설명되어있는 작성 글을 따라하다보니 금방 할 수 있었는데 완성해놓고 생각하니 사실 내가 구현해야할 기능은 봇을 이용해 dm을 보내는 기능이었다.

여기서 팀원들과 테이블을 설계했던 의도를 다시 상기시킬 수 있었는데
회원가입 시 작성한 slackId를 이용해 bot이 dm을 보내도록 하는 것이다.


여기서부터는 본인의 고유 slackId를 확인하는 방법에 대해서이다.
진짜 쉽게 안알려주고 별에별짓을 다해봤는데 이게 가장 쉬운방법이었던거같다.

Slack 워크스페이스 OAuth & Permissions 설정에서 User Token Scopes를 users:read로 설정해두고 PostMan에서 요청을 생성한 뒤

이렇게 3가지만 설정해주고 Send를 누르면 이 워크스페이스에 들어와있는 유저들의 고유 Id를 확인할 수 있다.


이 다음으로 내가 고민해야 할 것은 Slack에 대한 api를 작성할 Slack 서비스에서 User 객체의 id값을 가지고 slackId를 가져오는 방법에 대해서인데,

사실 그냥 requestBody에 slackId를 때려박아넣어도 되긴하지만 이건 그냥 기능만 구현해둔거일 뿐 사용자를 위한게 아니기도하고 FeignClient를 제대로 사용해보기 위해 공부해보기로했다.

구글 블로그들과 특강과 Gpt의 방법이 조금씩 달랐지만 조금씩 조율해가며 작성해갔다.

package com.forj.slack_message.application.service;

import com.forj.slack_message.application.dto.request.SlackMessageRequestDto;
import com.forj.slack_message.domain.repository.SlackRepository;
import com.forj.slack_message.infrastructure.dto.UserDto;
import com.slack.api.Slack;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
public class SlackService {

    private final SlackRepository slackRepository;

    private final UserServiceClient userServiceClient;

//    @Value("${webhook.slack.url}")
//    private String SLACK_WEBHOOK_URL;

    @Value("${webhook.slack.bot.token}")
    private String SLACK_BOT_TOKEN;

    private final Slack slackClient = Slack.getInstance();

    // 슬렉 채널에 메시지 전송
//    public void sendSlackMessage(SlackMessageRequestDto requestDto) {
//        try {
//            slackClient.send(SLACK_WEBHOOK_URL, payload(p -> p
//                    .text("New message") // 메시지 제목
//                    .attachments(List.of(
//                            Attachment.builder().color("#36a64f") // 메시지 색상 (초록색 Hex 코드)
//                                    .fields( // 메시지 본문 내용
//                                            requestDto.data().keySet().stream()
//                                                    .map(key -> generateSlackField(key, requestDto.data().get(key)))
//                                                    .collect(Collectors.toList())
//                                    ).build())))
//            );
//
//            com.forj.slack_message.domain.model.Slack slackMessage
//                    = com.forj.slack_message.domain.model.Slack.createSlackMessage(getCurrentUserId(),
//                    String.join(", ", requestDto.data().values()),
//                    LocalDateTime.now());
//
//            slackRepository.save(slackMessage);
//
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//    }

    public void sendSlackMessage(SlackMessageRequestDto requestDto) {
        try {

            UserDto userDto = userServiceClient.getSlackIdByUserId(requestDto.userId()/*, token*/);
            String slackUserId = userDto.slackId();
            String messageText = requestDto.data().values().stream()
                    .reduce((msg1, msg2) -> msg1 + "\n" + msg2)
                    .orElse("No message content");

            ChatPostMessageRequest messageRequest = ChatPostMessageRequest.builder()
                    .channel(slackUserId)
                    .text(messageText)
                    .build();

            ChatPostMessageResponse response = slackClient.methods(SLACK_BOT_TOKEN).chatPostMessage(messageRequest);

            if (response.isOk()) {
                Long currentUserId = getCurrentUserId();
                com.forj.slack_message.domain.model.Slack slackMessage = com.forj.slack_message.domain.model.Slack.createSlackMessage(
                        currentUserId,
                        messageText,
                        LocalDateTime.now()
                );
                slackRepository.save(slackMessage);
            } else {
                throw new RuntimeException("Failed to send message: " + response.getError());
            }

        } catch (IOException | SlackApiException e) {
            e.printStackTrace();
        }

    }

//        private Field generateSlackField (String title, String value){
//            return Field.builder()
//                    .title(title)
//                    .value(value)
//                    .valueShortEnough(false)
//                    .build();
//        }

    private Long getCurrentUserId() {
        return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName());
    }

}

코드가 너저분해지는건 싫어하지만 주석처리된 부분은 채널에 메시지를 작성하는 코드이므로 혹시나를 위해 남겨두었다.


의존성을 추가한다.

implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

FeignClient를 사용하기 위해 빌드 클래스에 어노테이션을 추가한다.

@SpringBootApplication
@ComponentScan(basePackages = {"com.forj.common", "com.forj.slack_message"})
@EnableFeignClients
public class SlackMessageApplication {

	public static void main(String[] args) {
		SpringApplication.run(SlackMessageApplication.class, args);
	}

}

Slack 서비스에 User에 대한 정보를 가져올 interface를 만든다.

@FeignClient(name = "auth", configuration = FeignConfig.class)
public interface UserServiceClient {
    @GetMapping("/api/v1/users/{userId}") // UserController에 있는 getUser의 EndPoint
    UserDto getSlackIdByUserId(@PathVariable("userId") Long userId);
    // 내가 필요한건 slackId뿐이므로 UserDto에는 slackId만 넣어두었다.
}

어노테이션 안에 name은 yml에 설정된

spring:
  application:
    name: auth

이 이름을 말한다.(이것도 나중에 알게돼서 에러가 날때마다 이것저것 수정해봤다...)

configuration = FeignConfig.class는 jwt의 정보를 담아줄 header를 설정해줄 클래스를 지정한다.

여기서 새로 알게된게
처음에 생각으로는 getUser 메서드는 user의 모든 필드들을 반환해주는데, 그러면 UserController에 새로운 메서드를 만들어서 호출해야하나? 라고 생각했었지만
getUser의 responseDto가 모든 필드를 반환한다고 하더라도 UserDto의 필드가 slackId 뿐이라면 문제없이 slackId 하나만을 가져오게 된다.


이제 header에 유저 정보만 담아서 넘겨주면된다.

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                String userId = getCurrentUserId();
                if (userId != null) {
                    requestTemplate.header("X-User-Id", userId);
                }
                String role = getCurrentUserRole();
                if (role != null) {
                    requestTemplate.header("X-User-Role", role);
                }
            }
        };
    }

    private String getCurrentUserId() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            return request.getHeader("X-User-Id");
        }
        return null;
    }

    private String getCurrentUserRole() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            return request.getHeader("X-User-Role");
        }
        return null;
    }

}

사실 여기서 헤더에 토큰만 담아주면 Context에서 알아서 정보를 꺼내가겠지 하고 돌려보는데 아무리 어떻게 하더라도 403에러가 떠버려서 진짜 6시간동안 코드 여기저기에 로그를 달면서 원인을 찾으려고 별짓을 다했는데
알고보니 토큰말고도 UserId와 UserRole을 담아주어야 했던것이었다.(사실 토큰도 안보내줘도돼서 다시 지움)
어쩐지 에러메시지로 자꾸만 userRole을 확인할 수가 없다고하는데 토큰 까보면 정보가 전부 다있고...


아무튼아무튼 이렇게 하루동안 내가 성공한 결과물

하핳

0개의 댓글