[UMC-3rd & Springboot] rest API 서버 구현 - FCM 문자 인증 #3

조윤희·2023년 3월 13일
0

UMC_badjang_Server

목록 보기
4/4

👌FCM

Firebase Cloud Messaging
메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징 솔루션이다.
FCM 구현에는 두가지 구성요소가 필요힌데, 첫번째는 Firebase용 Cloud Functions 또는 앱 서버와 같이 메시지를 작성, 타겟팅, 전송할 수 있는 신뢰할 수 있는 환경이다.
두번째는 해당 플랫폼별 전송 서비스를 통해 메시지를 수신하는 Apple, Android 또는 웹(자바스크립트) 클라이언트 앱이다.
메시지 전송 방식에는 Firebase Admin SDK 또는 FCM 서버 프로토콜이 있다.
이번 프로젝트에서는 FCM 서버 프로토콜을 이용하여 구현해보았다.

Fcm 작동 방식(Fcm 뿐만아니라 push 알림을 전송하는 모든 서버에 적용)
1. 클라이언트 앱을 fcm서버에 등록
2. 클라이언트 앱을 구분하는 targetToken을 fcm서버에서 발급
3. 클라이언트 앱이 이 targetToken을 앱서버로 전송
4. 앱서버는 token을 저장
5. 알림전송이 필요할때, 이 token값과 함께 fcm서버에 push요청
6. 클라이언트 앱은 push알림을 수신

FCM 서버 프로토콜을 이용한 Push 전송

서버앤드포인트와 accesstoken을 이용해서 특정기기 정보(targetToken)에 (프론트 구현에서 앱마다 토큰을 얻을수있음)메시지를 전송한다

  1. 서버앤드포인트
    HTTP프로토콜을 이용할 경우 FCM에서 제공하는 서버엔드 포인트에 프로젝트 아이디를 넣어 사용한다.
POST https://fcm.googleapis.com/v1/projects/myproject-ID/messages:send
  1. AccessToken
    Fcm으로부터 인증된 서버만이 push알림을 보낼 수 있는데, 이때 이 인증방식이 AccessToken(fcm을 이용할 수 있는 권한정도라고 생각..)이다.
    앱서버는 Firebase에서 AccessToken을 발급받으면, api를 통해 push 알림을 보낼때 http 요청 header에 포함시킨다.

build.gradle 파일에 firebase sdk 및 okhttp의존성 추가

implementation 'com.google.firebase:firebase-admin:9.1.1'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'

엑세스 토큰 발급

private static String getAccessToken() throws IOException {
  GoogleCredentials googleCredentials = GoogleCredentials
          .fromStream(new FileInputStream("service-account.json"))
          .createScoped(Arrays.asList(SCOPES));
  googleCredentials.refreshAccessToken();
  return googleCredentials.getAccessToken().getTokenValue();
}

HTTP 요청 헤더에 액세스 토큰을 추가

URL url = new URL(BASE_URL + FCM_SEND_ENDPOINT);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken());
httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8");
return httpURLConnection;

https://firebase.google.com/docs/cloud-messaging/migrate-v1?authuser=1&hl=ko#use-credentials-to-mint-access-tokens/


👌화면설계서


👌API 구현

FcmController

문자인증에 필요한 전화번호와 이메일의 형식에대한 validation

@RestController
@RequiredArgsConstructor
public class FcmController {

    private final FcmService fcmservice;

    @PostMapping("/fcm")
    public ResponseEntity pushMessage(@RequestBody RequestDTO requestDTO) throws IOException, BaseException {
        System.out.println(requestDTO.getTargetToken());
        //휴대폰, 이메일 형식적 validation
        if(!isRegexPhone(requestDTO.getUser_phone())){
            throw new BaseException(POST_USERS_INVALID_PHONE);
        }
        if(!isRegexEmail(requestDTO.getUser_email())){
            throw new BaseException(POST_USERS_INVALID_EMAIL);
        }

        fcmservice.sendMessageTo(
                requestDTO.getUser_phone(),
                requestDTO.getTargetToken()
        );
        return ResponseEntity.ok().build();
    }
}

FcmService

  • 존재하는 회원인지에 대한 의미적 validation
  • sendMessageTo()
    매개변수로 전달받은 targettoken에 해당하는 device로 push전송이 이루어지는 메소드
  • makeMessage()
    랜덤한 4자리 수의 인증번호를 담은 메시지를 생성하는 메소드
    FcmMessage에서 생성된 객체가 object Mapper에 의해 문자열로 변환된 데이터를 사용한다.
    okhttp3를 이용해 http post요청의 requestbody에 변환된 데이터를 넣어준다.
@Service
@Component
@RequiredArgsConstructor
public class FcmService {
    private final FcmDao fcmDao;
    private final ObjectMapper objectMapper;

    //클래스의 인스턴스를 만들지 않고 '정적'클래스를 사용하려고하는 초보자에게 매우 흔한 오류
	//서버 엔드포인트
    private final String API_URL = "https://fcm.googleapis.com/v1/projects/myproject-ID/messages:send";

    //매개변수로 전달받은 targetToken에 해당하는 device로 fcm푸시알림을 전송요청
    //targettoken은 프론트에서 
    public int checkUserPhone(String user_phone) throws BaseException {
        try{
            return fcmDao.checkUserPhone(user_phone);
        } catch (Exception exception) {
            throw new BaseException(DATABASE_ERROR);
        }
    }
    //존재하는 회원인지 의미적 VALIDATION
    public void sendMessageTo(String user_phone, String targetToken) throws IOException, BaseException {
        if(checkUserPhone(user_phone) == 0) {
            throw new BaseException(NON_EXISTENT_PHONENUMBER);
        }

        Random rand  = new Random();
        String numStr = "";
        for(int i=0; i<4; i++) {
            String ran = Integer.toString(rand.nextInt(10));
            numStr+=ran;
        }

        String title = "받장 휴대폰인증 테스트 메시지";
        String body = "인증번호는" + "[" + numStr + "]" + "입니다.";

        GoogleCredentials googleCredentials = GoogleCredentials
        String message = makeMessage(targetToken, title, body);

        OkHttpClient client = new OkHttpClient();
        RequestBody requestBody = RequestBody.create(message,
                MediaType.get("application/json; charset=utf-8"));
        Request request = new Request.Builder()
                .url(API_URL)
                .post(requestBody)
                .addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken()) //header에 accesstoken을 추가
                .addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
                .build();

        Response response = client.newCall(request).execute();

        System.out.println(response.body().string());
    }

    private String makeMessage(String targetToken, String title, String body) throws JsonParseException, JsonProcessingException {
        FcmMessage fcmMessage = FcmMessage.builder()
                .message(FcmMessage.Message.builder()
                        .token(targetToken)
                        .notification(FcmMessage.Notification.builder()
                                .title(title)
                                .body(body)
                                .image(null)
                                .build()
                        ).build()).validateOnly(false).build();

        return objectMapper.writeValueAsString(fcmMessage); //FcmMessage 생성후 objectmapper를 통해 string으로 변환하여 반환
    }

    //json파일의 사용자 인증정보를 사용하여 액세스 토큰(fcm을 이용할수 있는 권한이 부여된)을 발급받는다. 발급받은 accesstoken은 header에 포함하여, push 알림을 요청한다.
    private String getAccessToken() throws IOException {
        String firebaseConfigPath = "src/main/resources/~.json";

        GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
                .createScoped(Arrays.asList("https://www.googleapis.com/auth/cloud-platform"));
                //List.of
        googleCredentials.refreshIfExpired();
        return googleCredentials.getAccessToken().getTokenValue(); //토큰값을 최종적으로 얻어옵니다
    }
}

FcmDao

User Table에 해당 폰넘버값을 갖는 유저 정보가 존재하는지 확인

@Repository
public class FcmDao {
    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setDataSource(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int checkUserPhone(String phone) {
        String checkUserPhoneQuery = "select exists(select user_phone from User where user_phone = ?)"; // User Table에 해당 폰넘버값을 갖는 유저 정보가 존재하는가?
        String checkUserPhoneParams = phone; // 해당(확인할) 폰넘버 값
        return this.jdbcTemplate.queryForObject(checkUserPhoneQuery,
                int.class,
                checkUserPhoneParams); //쿼리문의 결과(존재하지 않음(False,0),존재함(True, 1))를 int형(0,1)으로 반환됩니다.
    }
}
    }

FcmMessage

이 클래스를 통해 생성된 객체는 makemessage()메소드에서 사용

@Builder
@AllArgsConstructor
@Getter
public class FcmMessage {
        //fcm에 push 알림을 보내기위해 준수해야하는 request body (클래스)
        private boolean validateOnly;
        private Message message;

        @Builder
        @AllArgsConstructor
        @Getter
        public static class Message {
            private Notification notification;
            private String token; //특정 device의 토큰
        }

        @Builder
        @AllArgsConstructor
        @Getter
        public static class Notification {
            private String title;
            private String body;
            private String image;
        }
}

RequestDTO

package com.example.demo.src.Fcm;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RequestDTO {
        private String user_email;
        private String user_phone;
        private String targetToken;
}

❓여기서 궁금했던 점

  1. 서버에서 devicetoken을 가지고 api를 만들어서 보내는데, 만약에 전화번호를 입력받아서 해당번호로 문자를 보내고 싶으면 어떻게 해야하는가? 전화번호를 입력하면 해당 디바이스의 토큰을 날려주는 api를 따로 만들어야 하는가?
    👉 아마 이부분은(전화번호를 입력하면 해당 디바이스토큰을 서버로 날려주는) 프론트에서 구현해야하는 부분인것같다..
    서버에서 할 수 있다면 아마 메소드에서 전화번호를 파라미터로 받아, 내부에서 다시 전화번호로 토큰을 찾도록 구현하는 방법이 있겠다.

프론트에서 firebase설정을 완료한 후 테스트가 가능하다.

참고자료
https://herojoon-dev.tistory.com/24/
https://galid1.tistory.com/740/
https://firebase.google.com/docs/cloud-messaging/migrate-v1?authuser=1&hl=ko/

0개의 댓글