Yandex SmartCaptcha в приложении на iOS
Чтобы встроить SmartCaptcha в приложение на iOS:
Перед началом работы
- Разместите HTML-код для работы SmartCaptcha (или воспользуйтесь готовым
https://smartcaptcha.yandexcloud.net/webview
). - Создайте капчу по инструкции.
- Получите ключи капчи. Скопируйте значения полей Ключ клиента и Ключ сервера на вкладке Обзор созданной вами капчи. Ключ клиента понадобится для загрузки страницы с капчей, Ключ сервера — для получения результата прохождения капчи.
Настройте JS часть сайта
Если вы не используете https://smartcaptcha.yandexcloud.net/webview
, то выполните следующие действия:
-
Добавьте виджет SmartCaptcha на страницу сайта.
-
Создайте метод для взаимодействия с нативной частью приложения:
function sendIos(...args) { if (args.length == 0) { return; } const message = { method: args[0], data: args[1] !== undefined ? args[1] : "" }; // Проверка на вызов из WKWebView. if (window.webkit) { window.webkit.messageHandlers.NativeClient.postMessage(message); } }
С форматом сообщения:
{ method: "captchaDidFinish" | "challengeDidAppear" | "challengeDidDisappear" data: "tokenName" | "" }
Метод возвращает:
success
— успешная валидации пользователя.challenge-visible
— открытие всплывающего окна с заданием.challenge-hidden
— закрытие всплывающего окна с заданием.
Настройте нативную часть сайта
-
В WKUserContentController зарегистрируйте обработчик
WKScriptMessageHandler
для ключаNativeClient
. -
В обработчике реализуйте метод:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let jsData = message.body as? [String: String] else { return } guard let methodName = jsData["method"] else { return } doSomething(name: methodName, params: jsData["data"]) }
-
После получения токена из метода
captchaDidFinish
отправьте POST-запрос на сервер для проверкиhttps://smartcaptcha.yandexcloud.net/validate
, передав параметры в форматеx-www-form-urlencoded
:secret
— ключ сервера;token
— одноразовый токен, полученный после прохождения проверки;ip
— IP-адрес пользователя, с которого пришел запрос на проверку токена. Этот параметр не обязателен, однако мы просим передавать IP-адрес пользователя при запросах. Это помогает улучшить качество работы SmartCaptcha.
Метод challengeDidAppear для невидимой капчи
Капча не будет отображаться в HTML-коде страницы, если она была вызвана с параметром invisible
. WKWebView должен быть загружен, но недоступен пользователю до момента вызова метода challengeDidAppear
. Один из способов сделать это:
UIApplication.shared.windows.first?.addSubview(webControllerView)
Если в результате проверки появляется captchaDidFinish
, удалите webControllerView
из иерархии. Если в результате нет captchaDidFinish
, переместите webControllerView
в иерархию для показа пользователю.
Метод challengeDidDisappear для невидимой капчи
Если пользователь "смахнул" капчу с экрана, восстановить самостоятельно ее не получится. Вызовите перезагрузку контента в WKWebView по событию challengeDidDisappear
:
webControllerView.reload()
Пример реализации на Swift с использованием https://smartcaptcha.yandexcloud.net/webview
В этой секции описаны шаги, необходимые для создания приложения с капчей для iOS. См. пример готового приложения, содержащего все настроенные компоненты: Yandex SmartCaptcha for iOS
-
Создайте класс, который будет хранить WKWebView:
final class WebNativeBridge: NSObject { private(set) var view: WKWebView? private var userContentController = WKUserContentController() func load(_ request: URLRequest?) { guard let request = request else { return } view?.load(request) } func reload() { view?.reload() } private func close() { view?.removeFromSuperview() } private func getConfiguration() -> WKWebViewConfiguration { let configuration = WKWebViewConfiguration() configuration.userContentController = userContentController return configuration } }
-
Добавьте свойство, где будет храниться обработчик для WKUserContentController:
private var handlers = [String: WebContentHandlerBase]() func setup(handlers: [String: WebContentHandlerBase]) { handlers.forEach { userContentController.add($1, name: $0) } view = WKWebView(frame: .zero, configuration: getConfiguration()) }
-
Создайте реализацию обработчика для методов страницы SmartCaptcha:
class WebContentHandlerBase: NSObject, WKScriptMessageHandler { var handlerName: String { "" } func execMethod(name: String, params: Any?...) {} func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let jsData = message.body as? [String: String] else { return } guard let methodName = jsData["method"] else { return } execMethod(name: methodName, params: jsData["data"]) } } final class CaptchaHandler: WebContentHandlerBase { private enum Methods: String { case captchaDidFinish case challengeDidAppear case challengeDidDisappear } override var handlerName: String { "NativeClient" } weak var delegate: CaptchaHandlerDelegate? private var validator: CaptchaValidatorProtocol init(_ validator) { self.validator = validator } override func execMethod(name: String, params: Any?...) { guard let method = Methods(rawValue: name) else { return } switch method { case .captchaDidFinish: guard let token = params.first as? String else { return } onSuccess(token: token) case .challengeDidDisappear: onChallengeHide() case .challengeDidAppear: onChallengeVisible() } } private func onSuccess(token: String) { validator.validateCaptcha(token: token) { result in DispatchQueue.main.async { switch result { case .success(_): self.delegate?.onSuccess() case .failure(let err): self.delegate?.onError(err) } } } } private func onChallengeVisible() { delegate?.onShow() } private func onChallengeHide() { delegate?.onHide() } }
-
Создайте класс для валидации токена от SmartCaptcha:
final class CaptchaValidator: CaptchaValidatorProtocol { private var host: String private var secret: String private var session: URLSession init(host: String, secret: String) { self.host = host self.secret = secret session = URLSession(configuration: .default) } func validateCaptcha(token: String, callback: @escaping (Result<String, Error>) -> Void) { guard let url = URL(string: host), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } components.queryItems = [ URLQueryItem(name: "secret", value: secret), URLQueryItem(name: "token", value: token), URLQueryItem(name: "ip", value: getIPAddress()), ] let task = session.dataTask(with: URLRequest(url: components.url!)) { data, response, error in guard let code = (response as? HTTPURLResponse)?.statusCode, code == 200 else { return } guard let data = data, let result = try? JSONDecoder().decode(YACValidationResponse.self, from: data) else { return } if result.status == "ok" { callback(.success("ok")) } else { callback(.failure(NSError(domain: result.message ?? "", code: code))) } } task.resume() } private func getIPAddress() -> String { var address: String = "" var ifaddr: UnsafeMutablePointer<ifaddrs>? = nil if getifaddrs(&ifaddr) == 0 { var ptr = ifaddr while ptr != nil { defer { ptr = ptr?.pointee.ifa_next } let interface = ptr?.pointee let addrFamily = interface?.ifa_addr.pointee.sa_family if addrFamily == UInt8(AF_INET) { if String(cString: (interface?.ifa_name)!) == "en0" { var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) getnameinfo(interface?.ifa_addr, socklen_t((interface?.ifa_addr.pointee.sa_len)!), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) address = String(cString: hostname) print(address) } } } freeifaddrs(ifaddr) } return address } }