[Spring] 휴대폰 인증 - 네이버 sens 사용

2023. 8. 26. 15:37Web/Spring

[Spring] 휴대폰 인증 - 네이버 sens 사용

 

지난번에 이메일 인증에 이어서 휴대폰 인증을 적용해보려고 한다. 적용을 하려고 한 범위는 휴대폰 인증을 통해 한 사람당 하나의 계정만 가지게 하려는 목적으로 알아보게 되었다. 구현을 하면서 과정을 정리하려고 한다.

이전에 CoolSMS등의 인증을 거치는 서비스도 고려를 해봤으나 네이버 

 

네이버 클라우드 플랫폼 Sens

 

사용한 api는 네이버 클라우드 플랫폼 sens이다. 네이버 클라우드 플랫폼에서는 다양한 서비스를 제공하는데 휴대폰 문자인증을 위해 사용할 서비스는 Simple & Easy Notification Service로 한번 구현을 해볼 예정이다.

해당 서비스를 사용하면 문자메시지뿐만 아니라 스마트폰 앱 Push 알람 전송 기능도 구현이 가능하다. 또한 메시지 전송 현황들을 실시간으로 확인하고, 전송 이력을 조회할 수 있다

 

NAVER CLOUD PLATFORM 네이버 클라우드 플랫폼 (ncloud.com)

 

NAVER CLOUD PLATFORM

cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification

www.ncloud.com

먼저 네이버 클라우드 플랫폼에 가입 후 콘솔로 들어오면 된다. 콘솔로 들어와서 service 검색창에 Simple & Easy Notification Service를 검색해서 이동하면 다음과 같은 페이지가 등장한다. 

sens

그리고 프로젝트 생성하기 버튼을 눌러서 다음과 같이 입력해 주었다

프로젝트 생성
생성 후 화면

생성을 하고 문자를 보내기 위해서는 먼저 발신번호 등록을 해줘야 한다. 

발송하기 -> 발송번호 등록하기를 통해 먼저 문자를 보낼 발신번호를 등록해줘야 한다. 

개인적인 학습 프로젝트에 적용하는 것이니 핸드폰 인증으로 등록을 진행하였다.

 

해당 기능을 구현하기 위해서는 다음과 같은 항목들이 필요하다.

  • 네이버 sens 서비스 ID
  • 네이버 클라우드 플랫폼 Access Key ID
  • 네이버 클라우드 플랫폼 Secret Key
  • 네이버 sens에 발신 등록을 한 휴대폰 번호

이때 클라우드 플랫폼 계정관리 - 인증키 관리에 들어가면 내게 부여된 API인증키를 발급 혹은 조회할 수 있다. 부여된 access와 secret 키를 가지고 오면 된다. 해당 항목들을 application.yml에 환경변수로 넣어주었다.

 

naver-cloud-sms:
    accessKey : ENC(TAB/kxScJwa0mF/LbXIZ2ak9iAaSbMJOjUJKnQNrWkY=)
    secretKey : ENC(geSn12XqZFmmSxYr0FlgxISkHz+ZX+E20D5SB55PCR0oawTthPZYroMQGm3fJgqaQdi3nuV6jQw=)
    serviceId : ENC(EVyAot+A6ow1ZtJG/dXT/rMxE6i8bJhiBKn6Vllt5ov928/a/zqmbrpIgmIPbTJp)
    senderPhone : "01000000000"

 

[Etc] application.yml 변경 및 Jasypt (tistory.com)

 

[Etc] application.yml 변경 및 Jasypt

application.yml과 암호화 Applcation.properties vs yml 사용 이유 데이터를 저장하고 읽어오는데 사용하는 포멧 yaml 여러 언어에 쓰이고 같은 configuration 파일을 여러 개의 애플리케이션이 읽기 가능 계층

skyriv312079.tistory.com

 

변수를 넣어줄 때 그냥 넣어주면 git에 올라갈 때 정보가 노출이 되므로 암호화를 시켰다. 만약 암호화를 안 하고 싶은 경우에는. gitignore 처리를 해주면 된다. 휴대폰 번호의 경우 암호화를 진행할 때 하이픈(-)이 들어가면서 프로젝트에서 인식하지를 못하였다. 그렇기에 테스트를 할 때는 기존의 사용번호를 넣고 git에 올릴 때는 01000000000 등의 거짓된 번호를 입력하였다.

 

code

 

먼저 build.gradle에 입력해야 하는 항목이다. 

// sms 인증
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.13'
implementation 'org.apache.httpcomponents.client5:httpclient5'

요청을 받기 위한 Controller이다.

@RestController
@RequestMapping(VERIFIED_API_URI)
@RequiredArgsConstructor
public class PhoneController {

    public static final String VERIFIED_API_URI = "/verified";

    private final PhoneVerifiedService phoneVerifiedService;

    @GetMapping("/phone")
    public SmsResponse checkPhoneVerified(
        @RequestParam("phoneNumber") String phoneNumber)
        throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {
        SmsResponse smsRespononse = phoneVerifiedService.sendNumber(phoneNumber);
        return smsRespononse ;
    }
}

 

처음 service를 진행할 때 signature 키를 받아와야 한다. 받아오는 부분은 해당 url에 소개되어 있는 것을 바탕으로 가져왔다.

Ncloud API (ncloud-docs.com)

 

Ncloud API

 

api.ncloud-docs.com

 

public String getSignature(String time)

        throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        String space = " ";
        String newLine = "\n";
        String method = "POST";
        String url = "/sms/v2/services/" + serviceId + "/messages";
        String timestamp = time;

        String message = new StringBuilder()
            .append(method)
            .append(space)
            .append(url)
            .append(newLine)
            .append(timestamp)
            .append(newLine)
            .append(accessKey)
            .toString();

        SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);

        byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));

        String encodeBase64String = Base64.getEncoder().encodeToString(rawHmac);

        return encodeBase64String;
    }

 

다음은 해당 시그니쳐 키를 이용해서 문자를 보내는 코드이다. 

 @Override
    public SmsResponse sendNumber(String phoneNumber)
        throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {

        String time = Long.toString(System.currentTimeMillis()); // 시그니처 키를 가져올때 시간은 동일해야한다.

		// header 값에 무엇이 들어갈 것인지, 해당 api 문서에서 요하는 조건에 맞춰서 작성
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("x-ncp-apigw-timestamp", time);
        headers.set("x-ncp-iam-access-key", accessKey);
		
        // 사용을 위한 시그니쳐 키 set
        headers.set("x-ncp-apigw-signature-v2", getSignature(time)); // signature 서명

        ArrayList<PhoneVerifiedRequest> message = new ArrayList<>();

        String number = makeRandomNumber();
        PhoneVerifiedRequest phoneVerifiedRequest = new PhoneVerifiedRequest(phoneNumber, number);
        message.add(phoneVerifiedRequest);

        SmsRequest smsRequest = SmsRequest.builder()
            .type("SMS")
            .countryCode("82")
            .contentType("COMM")
            .from(phone)
            .content("[skillBack] 인증번호 : [ " + number + " ]입니다")
            .messages(message)
            .build();
		
        // 만들어진 문자 request를 ObjectMapper를 통해 담아주기
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        String body = objectMapper.writeValueAsString(smsRequest);
        HttpEntity<String> stringHttpEntity = new HttpEntity<>(body, headers);

		// RestTemplate를 통해 post 해당 URI에 요청보내기 
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
        SmsResponse smsResponse = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+ this.serviceId +"/messages"), stringHttpEntity, SmsResponse.class);
        return smsResponse;

    }

 

인증 번호 랜덤 4자리를 만드는 메서드이다.

public String makeRandomNumber(){
    int num = (int) (Math.random() * 8999) + 1000;
    return String.valueOf(num);
}

전체 코드

@Slf4j
@Service
@RequiredArgsConstructor
public class PhoneVerifiedServiceImpl implements PhoneVerifiedService {

    private final String smsConfirmNum = makeRandomNumber();

    @Value("${naver-cloud-sms.accessKey}")
    private String accessKey;

    @Value("${naver-cloud-sms.secretKey}")
    private String secretKey;

    @Value("${naver-cloud-sms.serviceId}")
    private String serviceId;

    @Value("${naver-cloud-sms.senderPhone}")
    private String phone;


    public String getSignature(String time)

        throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException, UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        String space = " ";
        String newLine = "\n";
        String method = "POST";
        String url = "/sms/v2/services/" + serviceId + "/messages";
        String timestamp = time;

        String message = new StringBuilder()
            .append(method)
            .append(space)
            .append(url)
            .append(newLine)
            .append(timestamp)
            .append(newLine)
            .append(accessKey)
            .toString();

        SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);

        byte[] rawHmac = mac.doFinal(message.getBytes("UTF-8"));

        String encodeBase64String = Base64.getEncoder().encodeToString(rawHmac);

        return encodeBase64String;
    }

    @Override
    public SmsResponse sendNumber(String phoneNumber)
        throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, JsonProcessingException, URISyntaxException {

        String time = Long.toString(System.currentTimeMillis());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("x-ncp-apigw-timestamp", time);
        headers.set("x-ncp-iam-access-key", accessKey);

        headers.set("x-ncp-apigw-signature-v2", getSignature(time)); // signature 서명

        ArrayList<PhoneVerifiedRequest> message = new ArrayList<>();

        String number = makeRandomNumber();
        PhoneVerifiedRequest phoneVerifiedRequest = new PhoneVerifiedRequest(phoneNumber, number);
        message.add(phoneVerifiedRequest);

        SmsRequest smsRequest = SmsRequest.builder()
            .type("SMS")
            .countryCode("82")
            .contentType("COMM")
            .from(phone)
            .content("[skillBack] 인증번호 : [ " + number + " ]입니다")
            .messages(message)
            .build();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
        String body = objectMapper.writeValueAsString(smsRequest);
        HttpEntity<String> stringHttpEntity = new HttpEntity<>(body, headers);

        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
        SmsResponse smsResponse = restTemplate.postForObject(new URI("https://sens.apigw.ntruss.com/sms/v2/services/"+ this.serviceId +"/messages"), stringHttpEntity, SmsResponse.class);
        return smsResponse;

    }
      public String makeRandomNumber(){
        int num = (int) (Math.random() * 8999) + 1000;
        return String.valueOf(num);
    }
}