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알림을 수신
서버앤드포인트와 accesstoken을 이용해서 특정기기 정보(targetToken)에 (프론트 구현에서 앱마다 토큰을 얻을수있음)메시지를 전송한다
POST https://fcm.googleapis.com/v1/projects/myproject-ID/messages:send
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;
문자인증에 필요한 전화번호와 이메일의 형식에대한 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();
}
}
@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(); //토큰값을 최종적으로 얻어옵니다
}
}
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)으로 반환됩니다.
}
}
}
이 클래스를 통해 생성된 객체는 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;
}
}
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;
}
- 서버에서 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/