【问题标题】:Does expo support firebase phone auth?世博会支持firebase电话身份验证吗?
【发布时间】:2018-12-15 12:43:34
【问题描述】:

我正在尝试为博览会实施 Firebase 电话身份验证。我在互联网上关注了许多资源,但成功了。你能告诉我这可能/可用吗?如果可以的话,请分享一些对世博有用的资源

感谢您的期待。

【问题讨论】:

  • 你有解决办法吗? @Imdad 侯赛因
  • 伙计们,只要使用 Flutter 就开心)。这真是一个很棒的工具。我用了半年左右,没有出现 Flutter 无法解决的问题。

标签: firebase react-native firebase-authentication expo


【解决方案1】:

我遇到了同样的问题,但我找到了解决方案。那么,它是如何工作的:

  1. 我们有特殊的静态“Captcha”网页,托管在域上,并在我们的 Firebase 项目上授权。它只是显示firebase.auth.RecaptchaVerifier。用户解析验证码,并从回调的响应中给出token 字符串。

  2. 在应用程序登录屏幕上,我们显示带有“Captcha”页面的WebBrowser,并通过Linking 方法监听url 更改事件。在新的 url 上,我们从中提取令牌字符串。

  3. 然后我们用token 创建假的firebase.auth.ApplicationVerifier 对象并将其传递给firebase.auth().signInWithPhoneNumber(带有电话号码)。将发送短信代码。

我在下面编写了经过测试的最简单的代码。您可以直接“复制粘贴”它。只需添加 firebase 配置(此配置必须相同)并设置正确的“Captcha”页面 url。不要忘记电话必须以国际格式输入。在这个托管在firebase主机上的代码“Captcha”页面中,它通过包含init.js自动初始化并默认授权。

“Captcha”页面(托管在 firebase 主机上):

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Entering captcha</title>
</head>
<body>
    <p style="text-align: center; font-size: 1.2em;">Please, enter captcha for continue<p/>

    <script src="/__/firebase/5.3.1/firebase-app.js"></script>
    <script src="/__/firebase/5.3.1/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script>

        function getToken(callback) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            var captcha = new firebase.auth.RecaptchaVerifier('captcha', {
                'size': 'normal',
                'callback': function(token) {
                    callback(token);
                },
                'expired-callback': function() {
                    callback('');
                }
            });
            captcha.render().then(function() {
                captcha.verify();
            });
        }

        function sendTokenToApp(token) {
            var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
            location.href = baseUri + '/?token=' + encodeURIComponent(token);
        }

        document.addEventListener('DOMContentLoaded', function() {
            getToken(sendTokenToApp);
        });

    </script>
</body>
</html>

世博项目中的身份验证屏幕

import * as React from 'react'
import {Text, View, ScrollView, TextInput, Button} from 'react-native'
import {Linking, WebBrowser} from 'expo'
import firebase from 'firebase/app'
import 'firebase/auth'

const captchaUrl = `https://my-firebase-hosting/captcha-page.html?appurl=${Linking.makeUrl('')}`

firebase.initializeApp({
    //firebase config
});

export default class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            user: undefined,
            phone: '',
            confirmationResult: undefined,
            code: ''
        }
        firebase.auth().onAuthStateChanged(user => {
            this.setState({user})
        })
    }

    onPhoneChange = (phone) => {
        this.setState({phone})
    }
    onPhoneComplete = async () => {
        let token = null
        const listener = ({url}) => {
            WebBrowser.dismissBrowser()
            const tokenEncoded = Linking.parse(url).queryParams['token']
            if (tokenEncoded)
                token = decodeURIComponent(tokenEncoded)
        }
        Linking.addEventListener('url', listener)
        await WebBrowser.openBrowserAsync(captchaUrl)
        Linking.removeEventListener('url', listener)
        if (token) {
            const {phone} = this.state
            //fake firebase.auth.ApplicationVerifier
            const captchaVerifier = {
                type: 'recaptcha',
                verify: () => Promise.resolve(token)
            }
            try {
                const confirmationResult = await firebase.auth().signInWithPhoneNumber(phone, captchaVerifier)
                this.setState({confirmationResult})
            } catch (e) {
                console.warn(e)
            }

        }
    }
    onCodeChange = (code) => {
        this.setState({code})
    }
    onSignIn = async () => {
        const {confirmationResult, code} = this.state
        try {
            await confirmationResult.confirm(code)
        } catch (e) {
            console.warn(e)
        }
        this.reset()
    }
    onSignOut = async () => {
        try {
            await firebase.auth().signOut()
        } catch (e) {
            console.warn(e)
        }
    }
    reset = () => {
        this.setState({
            phone: '',
            phoneCompleted: false,
            confirmationResult: undefined,
            code: ''
        })
    }

    render() {
        if (this.state.user)
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <Text>You signed in</Text>
                    <Button
                        onPress={this.onSignOut}
                        title="Sign out"
                    />
                </ScrollView>
            )

        if (!this.state.confirmationResult)
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <TextInput
                        value={this.state.phone}
                        onChangeText={this.onPhoneChange}
                        keyboardType="phone-pad"
                        placeholder="Your phone"
                    />
                    <Button
                        onPress={this.onPhoneComplete}
                        title="Next"
                    />
                </ScrollView>
            )
        else
            return (
                <ScrollView style={{padding: 20, marginTop: 20}}>
                    <TextInput
                        value={this.state.code}
                        onChangeText={this.onCodeChange}
                        keyboardType="numeric"
                        placeholder="Code from SMS"
                    />
                    <Button
                        onPress={this.onSignIn}
                        title="Sign in"
                    />
                </ScrollView>
            )
    }
}

【讨论】:

  • 用户验证后验证码页面会自动关闭吗?
  • @iAviator,是的。 Captcha 调用callbackexpired-callback,然后通过设置location.href 进行页面重定向,最后通过监听函数检测到这个url 变化(Linking.addEventListener('url', listener)
  • 好的..还有如何在反应原生中实现不可见的验证码,我希望用户应该在没有任何交互的情况下在屏幕上进行验证?
  • WebBrowser 模式屏幕无论如何都需要显示,因为它提供了令牌字符串。您可以尝试使用 Recaptcha 模式firebase.google.com/docs/auth/web/…
  • 我现在面临的问题是网络浏览器屏幕有时不显示验证图像网格,只显示recaptcha 复选框。
【解决方案2】:

这是我的解决方案,基于@Rinat 的解决方案。 之前代码的主要问题是 firebase.auth().signInWithPhoneNumber 永远不会触发,因为它不在 webView 中,并且 firebase >6.3.3 需要有效的域进行身份验证。 我决定使用React Native Webview 让 WebView 和 Native 之间的通信更容易。

React-Native 方面

import React from 'react'
import { KeyboardAvoidingView  } from 'react-native';
import { TextInput, Button } from 'react-native-paper';
import { WebView } from 'react-native-webview';

import firebase from 'firebase/app';
import 'firebase/auth';

firebase.initializeApp({
    //...your firebase config
});

const captchaUrl = 'https://yourfirebasehosting/captcha.html';

export default class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            phoneNumber: '',
            phoneSubmitted: false,
            promptSmsCode: false,
            smsCode: '',
            smsCodeSubmitted: false
        }
        firebase.auth().onAuthStateChanged(this.onAuthStateChanged);
    }

    onAuthStateChanged = async user => {
        if (user) {
            const token = await firebase.auth().currentUser.getIdToken();
            if (token) {
                // User is fully logged in, with JWT in token variable
            }
        }
    }

    updatePhoneNumber = phoneNumber => this.setState({phoneNumber});
    updateSmsCode = smsCode => this.setState({smsCode});

    onSubmitPhoneNumber = () => this.setState({phoneSubmitted: true});

    onGetMessage = async event => {
        const { phoneNumber } = this.state;
        const message = event.nativeEvent.data;

        switch (message) {
            case "DOMLoaded":
                this.webviewRef.injectJavaScript(`getToken('${phoneNumber}')`);
                return;
            case "ErrorSmsCode":
                // SMS Not sent or Captcha verification failed. You can do whatever you want here
                return;
            case "":
                return;
            default: {
                this.setState({
                    promptSmsCode: true,
                    verificationId: message,
                })
            }
        }
    }

    onSignIn = async () => {
        this.setState({smsCodeSubmitted: true});
        const { smsCode, verificationId } = this.state;
        const credential = firebase.auth.PhoneAuthProvider.credential(verificationId, smsCode);
        firebase.auth().signInWithCredential(credential);
    }

    render() {

        const { phoneSubmitted, phoneNumber, promptSmsCode, smsCode, smsCodeSubmitted } = this.state;

        if (!phoneSubmitted) return (
            <KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
                <TextInput
                    label='Phone Number'
                    value={phoneNumber}
                    onChangeText={this.updatePhoneNumber}
                    mode="outlined"
                />
                <Button mode="contained" onPress={this.onSubmitPhoneNumber}>
                    Send me the code!
                </Button>
            </KeyboardAvoidingView >
        );

        if (!promptSmsCode) return (
            <WebView
                ref={r => (this.webviewRef = r)}
                source={{ uri: captchaUrl }}
                onMessage={this.onGetMessage}
            />
        )

        return (
            <KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
                <TextInput
                    label='Verification code'
                    value={smsCode}
                    onChangeText={this.updateSmsCode}
                    mode="outlined"
                    disabled={smsCodeSubmitted}
                    keyboardType='numeric'
                />
                <Button mode="contained" onPress={this.onSignIn} disabled={smsCodeSubmitted}>
                    Send
                </Button>
            </KeyboardAvoidingView >
        );
    }
}

captcha.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>Entering captcha</title>
</head>
<body>
    <script src="/__/firebase/6.3.3/firebase-app.js"></script>
    <script src="/__/firebase/6.3.3/firebase-auth.js"></script>
    <script src="/__/firebase/init.js"></script>
    <script>
        function getToken(phoneNumber) {
            var container = document.createElement('div');
            container.id = 'captcha';
            document.body.appendChild(container);
            window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('captcha', {
                'size': 'normal',
                'callback': function(response) {
                    var appVerifier = window.recaptchaVerifier;
                    firebase.auth().signInWithPhoneNumber(phoneNumber, appVerifier)
                        .then(function (confirmationResult) {
                            window.ReactNativeWebView.postMessage(confirmationResult.verificationId);
                        }).catch(function (error) {
                            window.ReactNativeWebView.postMessage('ErrorSmsCode');
                        });
                }
            });

            window.recaptchaVerifier.render().then(function() {
                window.recaptchaVerifier.verify();
            });
        }

        document.addEventListener('DOMContentLoaded', function() {
            window.ReactNativeWebView.postMessage('DOMLoaded');
        });

    </script>
</body>
</html>

【讨论】:

    【解决方案3】:

    好的,@Rinat 的回答几乎是完美的。

    验证码页面这个功能有问题

    function sendTokenToApp(token) {
      var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
      location.href = baseUri + '/?token=' + encodeURIComponent(token);
    }
    

    它适用于 iOS (Safary),但事实证明 Chrome 不允许

    location.href
    

    自定义 URL(我们试图将用户重定向到自定义 URL,exp://192.12.12.31)

    所以这是新功能:

    function sendTokenToApp(token) {
                var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
                const finalUrl = location.href = baseUri + '/?token=' + encodeURIComponent(token);
                const continueBtn = document.querySelector('#continue-btn');
                continueBtn.onclick = (event)=>{
                    window.open(finalUrl,'_blank')
                }
                continueBtn.style.display = "block";
    }
    

    当然你必须在HTML中添加一个按钮,这样你才能点击它。

    这是完整的代码:

    <!DOCTYPE html>
    <html lang="ru">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <title>Entering captcha</title>
    </head>
    <body>
        <p style="text-align: center; font-size: 1.2em;">Please, enter captcha for continue<p/>
        <button id="continue-btn" style="display:none">Continue to app</button>
    
        <script src="/__/firebase/5.3.1/firebase-app.js"></script>
        <script src="/__/firebase/5.3.1/firebase-auth.js"></script>
        <script src="/__/firebase/init.js"></script>
        <script>
    
            function getToken(callback) {
                var container = document.createElement('div');
                container.id = 'captcha';
                document.body.appendChild(container);
                var captcha = new firebase.auth.RecaptchaVerifier('captcha', {
                    'size': 'normal',
                    'callback': function(token) {
                        callback(token);
                    },
                    'expired-callback': function() {
                        callback('');
                    }
                });
                captcha.render().then(function() {
                    captcha.verify();
                });
            }
    
            function sendTokenToApp(token) {
                var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
                const finalUrl = location.href = baseUri + '/?token=' + encodeURIComponent(token);
                const continueBtn = document.querySelector('#continue-btn');
                continueBtn.onclick = (event)=>{
                    window.open(finalUrl,'_blank')
                }
                continueBtn.style.display = "block";
            }
    
            document.addEventListener('DOMContentLoaded', function() {
                getToken(sendTokenToApp);
            });
    
        </script>
    </body>
    </html>
    

    我花了将近 7 个小时才弄明白,所以我希望它对某人有所帮助!

    制作后编辑:

    不要忘记添加 "scheme": "appName",到 expo 应用的 app.json 或浏览器由于深度链接问题而无法打开。

    阅读本文

    https://docs.expo.io/versions/latest/workflow/linking#in-a-standalone-app

    【讨论】:

      【解决方案4】:

      我使用基于 Damian Buchet 版本的状态挂钩制作了功能等效项。验证码页面是一样的。 React 原生模块:

      import React, {useState} from 'react'
      import {Text, View, TextInput, Button,StyleSheet, KeyboardAvoidingView} from 'react-native'
      import { WebView } from 'react-native-webview';
      import firebase from '../../components/utils/firebase'  /contains firebase initiation
      
      
      const captchaUrl = 'https://my-domain.web.app/captcha-page.html';
      
      const LoginScreenPhone = props => {
      const [phoneNumber, setPhoneNumber] = useState();
      const [step, setStep] = useState('initial');
      const [smsCode, setSmsCode] = useState();
      const [verificationId, setVerificationId]=useState();
      
      const onAuthStateChanged = async user => {
          if (user) {
              const token = await firebase.auth().currentUser.getIdToken();
              if (token) {
                  // User is fully logged in, with JWT in token variable
              }
          }
      }
      
      firebase.auth().onAuthStateChanged(onAuthStateChanged);
      
      
      
      const onGetMessage = async event => {
      
          const message = event.nativeEvent.data;
          console.log(message);
          switch (message) {
              case "DOMLoaded":
      
                  return;
              case "ErrorSmsCode":
                  // SMS Not sent or Captcha verification failed. You can do whatever you want here
                  return;
              case "":
                  return;
              default: {
                  setStep('promptSmsCode');
                  setVerificationId(message);
                  }
              }
      }
      
      
      const onSignIn = async () => {
          setStep('smsCodeSubmitted');
          const credential = firebase.auth.PhoneAuthProvider.credential(verificationId, smsCode);
          firebase.auth().signInWithCredential(credential);
          props.navigation.navigate('Home');
      }
      
      return (
          <View>
          {step==='initial' && (
              <KeyboardAvoidingView  behavior="padding" enabled>
                  <TextInput
                      label='Phone Number'
                      value={phoneNumber}
                      onChangeText={phone =>setPhoneNumber(phone)}
                      mode="outlined"
                  />
                  <Button mode="contained" onPress={()=>setStep('phoneSubmitted')} title=' Send me the code!'>
      
                  </Button>
              </KeyboardAvoidingView >
          )}
      
        {step==='phoneSubmitted' && (
              <View style={{flex:1, minHeight:800}}>
                  <Text>{`getToken('${phoneNumber}')`}</Text>
              <WebView
               injectedJavaScript={`getToken('${phoneNumber}')`}          
                  source={{ uri: captchaUrl }}
                  onMessage={onGetMessage}
              />
              </View>
          )}
      
         {step==='promptSmsCode' && (<KeyboardAvoidingView behavior="padding" enabled>
                  <TextInput
                      label='Verification code'
                      value={smsCode}
                      onChangeText={(sms)=>setSmsCode(sms)}
                      mode="outlined"
                      keyboardType='numeric'
                  />
                  <Button mode="contained" onPress={onSignIn} title='Send'>
      
                  </Button>
          </KeyboardAvoidingView >)}
          </View>
      );
      }
      
      
      export default LoginScreenPhone;
      

      【讨论】:

        【解决方案5】:

        嗨,感谢您的解决方案..为我工作.. 但是为了更好的方式,我们可以通过添加不可见的验证码来跳过验证码

        // React Native 端

             import * as React from 'react'
            import { View, ScrollView, TextInput, Button, StyleSheet, WebView } from 'react-native';
            import { Text } from "galio-framework";
            import { Linking } from 'expo';
            import * as firebase from 'firebase';
            import OTPInputView from '@twotalltotems/react-native-otp-input'
            import theme from '../constants/Theme';
        
            const captchaUrl = `your firebase host /index.html?appurl=${Linking.makeUrl('')}`
        
            firebase.initializeApp({
                //firebase config
        
            });
        
            export default class PhoneAUth extends React.Component {
                constructor(props) {
                    super(props)
                    this.state = {
                        user: undefined,
                        phone: '',
                        confirmationResult: undefined,
                        code: '',
                        isWebView: false
                    }
                    firebase.auth().onAuthStateChanged(user => {
                        this.setState({ user })
                    })
                }
        
            onPhoneChange = (phone) => {
                this.setState({ phone })
            }
            _onNavigationStateChange(webViewState) {
                console.log(webViewState.url)
                this.onPhoneComplete(webViewState.url)
            }
            onPhoneComplete = async (url) => {
                let token = null
                console.log("ok");
                //WebBrowser.dismissBrowser()
                const tokenEncoded = Linking.parse(url).queryParams['token']
                if (tokenEncoded)
                    token = decodeURIComponent(tokenEncoded)
        
                this.verifyCaptchaSendSms(token);
        
        
            }
            verifyCaptchaSendSms = async (token) => {
                if (token) {
                    const { phone } = this.state
                    //fake firebase.auth.ApplicationVerifier
                    const captchaVerifier = {
                        type: 'recaptcha',
                        verify: () => Promise.resolve(token)
                    }
                    try {
                        const confirmationResult = await firebase.auth().signInWithPhoneNumber(phone, captchaVerifier)
                        console.log("confirmationResult" + JSON.stringify(confirmationResult));
                        this.setState({ confirmationResult, isWebView: false })
                    } catch (e) {
                        console.warn(e)
                    }
        
                }
            }
        
            onSignIn = async (code) => {
                const { confirmationResult } = this.state
                try {
                    const result = await confirmationResult.confirm(code);
                    this.setState({ result });
        
                } catch (e) {
                    console.warn(e)
        
                }
            }
            onSignOut = async () => {
                try {
                    await firebase.auth().signOut()
                } catch (e) {
                    console.warn(e)
                }
            }
            reset = () => {
                this.setState({
                    phone: '',
                    phoneCompleted: false,
                    confirmationResult: undefined,
                    code: ''
                })
            }
        
            render() {
            if (this.state.user)
                return (
                    <ScrollView style={{padding: 20, marginTop: 20}}>
                        <Text>You signed in</Text>
                        <Button
                            onPress={this.onSignOut}
                            title="Sign out"
                        />
                    </ScrollView>
                )
                else if (this.state.isWebView)
                    return (
                        <WebView
                            ref="webview"
                            source={{ uri: captchaUrl }}
                            onNavigationStateChange={this._onNavigationStateChange.bind(this)}
                            javaScriptEnabled={true}
                            domStorageEnabled={true}
                            injectedJavaScript={this.state.cookie}
                            startInLoadingState={false}
                        />
        
                    )
                else if (!this.state.confirmationResult)
                    return (
                        <ScrollView style={{ padding: 20, marginTop: 20 }}>
                            <TextInput
                                value={this.state.phone}
                                onChangeText={this.onPhoneChange}
                                keyboardType="phone-pad"
                                placeholder="Your phone"
                            />
                            <Button
                                onPress={this.onPhoneComplete}
                                title="Next"
                            />
                        </ScrollView>
                    )
                else
                    return (
                        <ScrollView style={{padding: 20, marginTop: 20}}>
                            <TextInput
                                value={this.state.code}
                                onChangeText={this.onCodeChange}
                                keyboardType="numeric"
                                placeholder="Code from SMS"
                            />
                            <Button
                                onPress={this.onSignIn}
                                title="Sign in"
                            />
                        </ScrollView>
                    )
            }
        }
        const styles = StyleSheet.create({
            borderStyleBase: {
                width: 30,
                height: 45
            },
        
            borderStyleHighLighted: {
                borderColor: theme.COLORS.PRIMARY,
            },
        
            underlineStyleBase: {
                width: 30,
                height: 45,
                borderWidth: 0,
                borderBottomWidth: 1,
            },
        
            underlineStyleHighLighted: {
                borderColor: theme.COLORS.PRIMARY,
            },
        });
        

        // 验证码侧 .我使用 Firebase 托管来托管此文件

        <!DOCTYPE html>
        <html lang="en">
        
        <head>
            <meta charset="UTF-8">
            <title>Firebase Phone Authentication</title>
            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
        
            <script src="https://www.gstatic.com/firebasejs/4.3.1/firebase.js"></script>
            <script>
                // Your web app's Firebase configuration
                var firebaseConfig = {
            // config
                };
                // Initialize Firebase
                firebase.initializeApp(firebaseConfig);
            </script>
            <script src="https://cdn.firebase.com/libs/firebaseui/2.3.0/firebaseui.js"></script>
            <link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.3.0/firebaseui.css" />
            <link href="style.css" rel="stylesheet" type="text/css" media="screen" />
        
        </head>
        
        <body>
            <script>
        
                function getToken(callback) {
                    var container = document.createElement('div');
                    container.id = 'captcha';
                    document.body.appendChild(container);
                    var captcha = new firebase.auth.RecaptchaVerifier('captcha', {
        
                /****************
                 I N V I S I B L E  
                **********************/
                        'size': 'invisible',
                        'callback': function (token) {
                            callback(token);
                        },
                        'expired-callback': function () {
                            callback('');
                        }
                    });
                    captcha.render().then(function () {
                        captcha.verify();
                    });
                }
        
                function sendTokenToApp(token) {
                    var baseUri = decodeURIComponent(location.search.replace(/^\?appurl\=/, ''));
                    location.href = 'http://www.google.com + '/?token=' + encodeURIComponent(token);
                }
        
                document.addEventListener('DOMContentLoaded', function () {
                    getToken(sendTokenToApp);
                });
        
            </script>
            <h2>Verification Code is Sending !! </h2>
            <h3>Please Wait !!</h3>
        
        </body>
        
        </html>
        

        【讨论】:

        • 在 android 中关闭网络浏览器不支持。所以我通过使用 webview 找到了更好的方法,它在 android 和 ios 上对我有用。
        【解决方案6】:

        Doorman 为 Expo 应用程序提供 Firebase 电话身份验证支持,无需分离。它还带有用于 Expo/React Native 的可定制 UI 组件。

        链接:https://doorman.cool

        代码示例如下所示:

        export default withPhoneAuth(App, {
          doorman: {
            publicProjectId: "ID here"
          }
        })
        

        【讨论】:

          猜你喜欢
          • 2021-07-13
          • 1970-01-01
          • 2021-03-10
          • 2021-06-10
          • 2018-12-14
          • 2017-01-30
          • 2018-12-06
          相关资源
          最近更新 更多