카테고리 없음

[iOS] 네이버 SMS API 사용하기 (1/2)

이제빵 2023. 11. 28. 13:36
728x90
반응형
SMALL

네이버 SMS API 사용하기! 
>  문자인증을 통한 로그인 회원가입 기능을 구현하기위해 네이버 SMS API를 사용해서 구현하였습니다.

### Class 만들기 

``` swift
final class SMSAuthService {
enum SMSRequestType: String {
        case sms
        case lms
        case mms
        
        var name: String {
            return self.rawValue.uppercased()
        }
    }
    
    struct SMSRequest: Codable {
        let type: String // (SMS | LMS | MMS)
        /// 발신번호
        let from: String
        /// 기본 메시지 제목
        let subject: String?
        /// 기본 메시지 내용
        let content: String
        /// 메시지 정보
        let messages: [Message]
        let files: [File]
    }
    
    struct Message: Codable {
        /// 수신번호
        let to: String
    }
    
    struct File: Codable {
        let fileId: String?
    }
    
}
```


 코드 설명 
- 내부 사정이 변경되어 인증 방식이 달라질 것을 대비하여 열거형 타입으로 두어 SMSRequestType을 SMS, LMS, MMS 등 여러개의 케이스로 두었습니다.
- SMSRequest은 문자인증을 요청 할 때 네이버 SMS API가 요구하는 JSON BODY 부분에 대한 구조체입니다. 

 개인정보 보호하면서 작업하기 
> 필요한 정보들을 Key 값을 Plist 파일로 따로 뺴서 Git에 팀원의 전화번호가 올라가는 불상사를 막았습니다. 그 후 번들파일을 extension 하여 메서드를 만들어서 사용하였습니다.  

```swift
extension Bundle {
var senderPhoneNumber: String {
        guard let file = self.path(forResource: "APIKeys", ofType: "plist") else { return "" }
        
        guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
        
        guard let key = resource["SENDERPHONENUMBER"] as? String else {
            fatalError("KEY를 찾을수없음")
        }
        return key
    }
    var accessKey: String {
        guard let file = self.path(forResource: "APIKeys", ofType: "plist") else { return "" }
        
        guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
        
        guard let key = resource["ACCESS_KEY"] as? String else {
            fatalError("KEY를 찾을수없음")
        }
        return key
    }
    
    var secretKey: String {
        guard let file = self.path(forResource: "APIKeys", ofType: "plist") else { return "" }
        
        guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
        
        guard let key = resource["SECRET_KEY"] as? String else {
            fatalError("KEY를 찾을수없음")
        }
        return key
    }
    
    var serviceId: String {
        guard let file = self.path(forResource: "APIKeys", ofType: "plist") else { return "" }
        
        guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
        
        guard let key = resource["SERVICE_ID"] as? String else {
            fatalError("KEY를 찾을수없음")
        }
        return key
    }
 }
```


 코드 설명 
- guard let file = self.path(forResource: "APIKeys", ofType: "plist") else { return "" }
- APIKeys.plist 파일의 경로를 가져오는 코드입니다. 없으면 공백을 반환하도록 했습니다. 
- guard let resource = NSDictionary(contentsOfFile: file) else { return "" }
- 파일의 데이터를 읽어오는 부분입니다. 
- guard let key = resource["넣을 키"] as? String else {
    fatalError("KEY를 찾을수없음")
}


- Root 중 하나를 "넣을 키" 부분에 삽입하면 됩니다.
- return 타입은 String으로 반환하였습니다. 


 Header 
> 우선 밑의 사진처럼 API Header에서 요구하는것들을 전부 만들겁니다. 


Header 1
- API Header중 x-ncp-apigw-timestamp는 협정세계시부터의 경과시간을 요청합니다. 
- Solution
- UTC부터 현재 시간까지의 경과시간을 String으로 변환하여 해결했습니다.
 

```
private let timestamp = String(Int(Date().timeIntervalSince1970 * 1000)) 
```


코드설명 
- 현재 날짜와 시간에서 1970년 1월 1일까지 의 경과시간을 나타내어 Double로 반환했습니다.
- 그 반환된값을 * 1000을 곱함으로써 초를 밀리초로 변환했습니다.
- 이제 변환한 값을 String으로 변환하여 밀리초로 표현하는 Int값을 String 값으로 받았습니다. 
 

 Header 2
- SHA256 알고리즘을 통해서 시그니처를 생성하고 API Header 중 x-ncp-apigw-signature-v2 에 넣어서 리퀘스트 요청을 해야한다. 
- Issue
- 시그니처를 생성하는 부분에서 암호화가 잘못되어서 응답코드가 401: Unauthorized를 반환했습니다.
- Problem
- Sha256에 Hmac을 포함시킨 HMAC-SHA-256 알고리즘으로 구현해야한다. 
- Solution
- CommonCrypto을 사용하여 HMAC-SHA-256 알고리즘을 구현했습니다. 

```swift
import CommonCrypto
private func makeSignature() -> String {
        let url = "/sms/v2/services/\(serviceId)/messages"
        let message = method + " " + url + "\n" + timestamp + "\n" + accessKey
        let keyData = secretKey.data(using: .utf8)!
        var macOut = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        keyData.withUnsafeBytes { keyBytes in
            CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes.baseAddress, keyData.count, message, message.utf8.count, &macOut)
        }

        let hmacData = Data(bytes: macOut, count: Int(CC_SHA256_DIGEST_LENGTH))
        let base64Encoded = hmacData.base64EncodedString()
        
        return base64Encoded
    }
```



코드 설명
- HMAC 및 SHA 알고리즘과 같은 암호화 함수들을 제공하는 CommonCrypto을 사용합니다.
- url은 네이버 SMS API url에 자신의 서비스 아이디를 넣어야합니다. 
- method에는 Swagger에서 제공하는는것 중 하나를 선택해야합니다.
https://sens.apigw.ntruss.com/apigw/swagger-ui?productId=plv61henn8&apiId=j5tgfxp2ba&stageId=a0y11xe7vi&region=KR
- HMAC-SHA256을 계산하기 위해 CCHmac 함수를 사용하여 HMAC-SHA256 해시를 계산하고 macOut 배열에 저장시킵니다.
- macOut 배열을 Data 객체로 변환한 다음 base64로 인코딩하여 최종 시그니처를 얻어냅니다. 

Header 3 
- x-ncp-iam-access-key 부분에는 SMS API 진행하면서 발급받은 Access Key ID를 넣으면 됩니다.

Body
> 이제 API에 requset를 하기 위한 JSON의 BODY 부분들의 데이터들을 정의합니다.

```swift
private let accessKey = Bundle.main.accessKey
    private let secretKey = Bundle.main.secretKey
    private let serviceId = Bundle.main.serviceId
    private let senderPhoneNumber = Bundle.main.senderPhoneNumber
    private let receiverPhoneNumber = " "
    private var randomNumber = ""
    private let method = "POST"
    private let timestamp = String(Int(Date().timeIntervalSince1970 * 1000))

```


#### 코드 설명
- accesskey, secretkey, serviceId, senderPhoneNumber는 이미 plist안에 넣어둔 정보를 저장했습니다.
- receiverPhoneNumber: 받는사람 번호입니다.
- timestamp는 Header부분에서 설명 
- randomNumber: 저는 이 문자인증을 인증 버튼을 누르면 이 메서드가 실행되어 검사를 받는 유저에게 랜덤인 문자 메시지를 전달하게 끔 구현했습니다.

```swift
 func configRandomCode() -> String {
        let digits = "0123456789"
        var randomCode = ""
        for _ in 0..<6 {
            let randomIndex = Int.random(in: 0..<digits.count)
            let digit = digits[digits.index(digits.startIndex, offsetBy: randomIndex)]
            randomCode.append(digit)
        }
        print("인증번호 생성: \\(randomCode)")
        randomNumber = randomCode
        return randomCode
    }

func checkRightCode(code: String) -> Bool {
        if randomNumber == code {
            return true
        } else {
            return false
        }
    }

```

#### 코드설명 안에 코드설명 

- 저희의 앱은 랜덤인 6자리 숫자를 사용자에게 전달하여 보낸 문자와 사용자가 입력하여 검사하는 방식입니다.
- 0부터9의 랜덤숫자를 리턴하는 메서드입니다.
- 만약 configRandomCode() 에서 생성된 번호를 randomNumber에 저장시키고 나중에 사용자가 입력을 비교할때 checkRightCode 메서드에서 문자열을 받아 비교하는 방식으로 구현했습니다.



참고 
[네이버](https://api.ncloud-docs.com/docs/ai-application-service-sens-smsv2)
https://losskatsu.github.io/blockchain/sha256/#4-sha-256-%EA%B3%BC%EC%A0%95

반응형
LIST