Docker ( React / Flask / Nginx) - Spotify 授权码

Posted

技术标签:

【中文标题】Docker ( React / Flask / Nginx) - Spotify 授权码【英文标题】:Docker ( React / Flask / Nginx) - Spotify Authorization Code 【发布时间】:2020-06-26 09:28:37 【问题描述】:

基于this SO answer,我正在尝试实现Spotify Authorization Code,因为我需要用户永久登录。

与隐式流程不同,在授权代码流程中,应用程序必须提供 client_secret 并获取刷新令牌以进行无限制访问,因此数据交换必须发生在服务器到服务器


nginx 代理

我的后端服务器使用Flaskhttp://localhost:5000 运行,我的前端使用Reacthttp://localhost:3000 运行。

这两个服务都在 nginx 反向代理之后,配置如下:

location / 
        proxy_pass        http://client:3000;
        proxy_redirect    default;
        proxy_set_header  Upgrade $http_upgrade;
        proxy_set_header  Connection "upgrade";
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    

location /callback 
        proxy_pass        http://web:5000;
        proxy_redirect    default;
        proxy_set_header  Upgrade $http_upgrade;
        proxy_set_header  Connection "upgrade";
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    

根据上面的答案,我正在做以下事情:

    在我的前端页面上提供一个链接到您的 https://accounts.spotify.com/authorize/... 网址。 (这不能是 AJAX 请求调用,否则会引发 CORS 问题) 用户将继续为我的应用程序提供scope 参数中指定的权限,并且 将被定向回您在 REDIRECT_URI 参数中指定的 URL。 这是您获取授权码的地方,您可以在 https://accounts.spotify.com/api/token/... 端点

反应

这里我向用户提供授权按钮:

render() 
    var state = generateRandomString(16);
    const Credentials = 
      stateKey: 'spotify_auth_state',
      client_id: 'my_id',
      redirect_uri: 'http://localhost:5000/callback',
      scope: 'playlist-modify-public playlist-modify-private'
    
    let url = 'https://accounts.spotify.com/authorize';
    url += '?response_type=token';
    url += '&client_id=' + encodeURIComponent(Credentials.client_id);
    url += '&scope=' + encodeURIComponent(Credentials.scope);
    url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
    url += '&state=' + encodeURIComponent(state);


   return (
      <div className="button_container">
      <h1 className="title is-3"><font color="#C86428">"Welcome"</font></h1>
          <div className="Line" /><br/>
            <a href=url > Login to Spotify </a>
      </div>
    )
  

烧瓶

这是我希望将应用程序重定向到的位置,以便将令牌保存到数据库,并且理想情况下之后再重定向回我的前端

# spotify auth
@spotify_auth_bp.route("/spotify_auth", methods=['GET', 'POST'])
def spotify_auth():
    #Auth Step 1: Authorization
    #  Client Keys
    CLIENT_ID =   os.environ.get('SPOTIPY_CLIENT_ID')
    CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
    # Spotify URLS
    SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
    #SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
    SPOTIFY_API_BASE_URL = "https://api.spotify.com"
    API_VERSION = "v1"
    SPOTIFY_API_URL = "/".format(SPOTIFY_API_BASE_URL, API_VERSION)

    # Server-side Parameters
    CLIENT_SIDE_URL = os.environ.get('REACT_APP_WEB_SERVICE_URL')
    REDIRECT_URI = os.environ.get('REACT_APP_WEB_SERVICE_URL')
    #PORT = 5000
    #REDIRECT_URI = ":/callback".format(CLIENT_SIDE_URL, PORT)
    SCOPE = os.environ.get('SPOTIPY_SCOPE')
    STATE = ""
    SHOW_DIALOG_bool = True
    SHOW_DIALOG_str = str(SHOW_DIALOG_bool).lower()

    auth_query_parameters = 
        "response_type": "code",
        "redirect_uri": 'http://localhost/callback',
        "scope": 'user-read-currently-playing user-read-private user-library-read user-read-email user-read-playback-state user-follow-read playlist-read-private playlist-modify-public playlist-modify-private',
        # "state": STATE,
        # "show_dialog": SHOW_DIALOG_str,
        "client_id": CLIENT_ID
    
    url_args = "&".join(["=".format(key, quote(val)) for key, val in auth_query_parameters.items()])
    auth_url = "/?".format(SPOTIFY_AUTH_URL, url_args)
    return redirect(auth_url)



@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
    # Auth Step 4: Requests refresh and access tokens
    CLIENT_ID =   'my_id'
    CLIENT_SECRET = 'my_secret'
    CLIENT_SIDE_URL = 'http://localhost'
    PORT = 5000
    REDIRECT_URI = ":/callback".format(CLIENT_SIDE_URL, PORT)

    SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"

    auth_token = request.args['code']
    code_payload = 
        "grant_type": "authorization_code",
        "code": auth_token,
        "redirect_uri": 'http://localhost/callback',
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
    

    auth_str = ':'.format(CLIENT_ID, CLIENT_SECRET) 
    b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode()

    headers = 
        "Content-Type" : 'application/x-www-form-urlencoded', 
        "Authorization" : "Basic ".format(b64_auth_str) 

    post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)

    # Auth Step 5: Tokens are Returned to Application
    response_data = json.loads(post_request.text)
    print ('RESPONSE DATA', response_data)

    access_token = response_data["access_token"]
    refresh_token = response_data["refresh_token"]
    token_type = response_data["token_type"]
    expires_in = response_data["expires_in"]

    template =  render_template("index.html")
    response_object = 
                'status': 'success',
                'message': 'success',
                'data': ['access_token': access_token,
                          'refresh_token': refresh_token,
                          'token_type': token_type,
                          'content': template]
                

    return jsonify(response_object), 200

通过 Spotify 列入白名单的重定向

http://localhost:5000 
http://localhost:5000/callback
http://web:5000
http://web:5000/callback 
http://localhost/callback 

但是,当我单击带有前两个重定向的按钮时,我收到了错误:

localhost refused to connect.

为什么?

如果我单击将 http://localhost/callback 作为 redirect_uri 的按钮,我会得到:

KeyError: 'access_token'

我错过了什么?

问题

我想要一个像上面那样的 Flask 端点,我可以在其中获取访问令牌(如果过期则更新)。

一种无需 javascript 代码进行身份验证的解决方案,非常完美。是否可以使用容器化服务器?

【问题讨论】:

【参考方案1】:

授权代码流程未按应有的方式实现。 这个流程的开始应该是从前端(react)到后端(flask)的请求。后端负责用正确的参数触发302 Redirect到身份提供者(Spotify)。

@spotify_auth_bp.route("/auth", methods=['GET'])
def auth():
    CODE = "code"
    CLIENT_ID =   os.environ.get('SPOTIPY_CLIENT_ID')
    SCOPE = "playlist-modify-public playlist-modify-private"
    SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
    REDIRECT_URI = "http://localhost/callback"
    return redirect("?response_type=&client_id=&scope=&redirect_uri=".format(SPOTIFY_AUTH_URL, CODE, CLIENT_ID, SCOPE, REDIRECT_URI), code=302)

前端应该完全不知道身份提供者,并且后端不应该将access_token 转发给前端,而是在用户通过身份提供者进行身份验证时生成自己的令牌(理想情况下作为 Cookie)。

你在客户端根本不使用client_secret,它不应该被客户端知道。顾名思义,它应该是秘密的,一旦你将它包含在 JavaScript 代码中,它就不再是秘密了。通过将client_secret 保留在后端,您可以完全隐藏它对最终用户(尤其是对恶意用户)。

话虽如此,您观察到此错误的原因是应该在响应中包含 access_tokenPOST 请求实际上没有。

原因是?response_type=token错了,初始请求中应该是?response_type=code

来源:https://developer.spotify.com/documentation/general/guides/authorization-guide/

这里是一个回调端点的例子:

@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
    # Auth Step 4: Requests refresh and access tokens
    SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"

    CLIENT_ID =   os.environ.get('SPOTIPY_CLIENT_ID')
    CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
    REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')

    auth_token = request.args['code']
    code_payload = 
        "grant_type": "authorization_code",
        "code": auth_token,
        "redirect_uri": 'http://localhost/callback',
     

    post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)

    # Auth Step 5: Tokens are Returned to Application
    response_data = json.loads(post_request.text)

    access_token = response_data["access_token"]
    refresh_token = response_data["refresh_token"]
    token_type = response_data["token_type"]
    expires_in = response_data["expires_in"]

    # At this point, there is to generate a custom token for the frontend
    # Either a self-contained signed JWT or a random token
    # In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
    # or in the database (in case of a stateless API)
    # In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all
    # Let's assume the resulting token is stored in a variable named "token"

    res = Response('http://localhost/about', status=302)
    res.set_cookie('auth_cookie', token)
    return res

【讨论】:

谢谢。我的秘密没有暴露。它仅用于示例目的。我使用环境变量。问题 - 前面提到的 /authorize 端点的前端按钮的实现来自在 spotify 工作的人。您能否根据上面的烧瓶代码使用整个授权流程?你的回答,代码方面,太简短了。 还有?response_type=token不对,应该是?response_type=code 此外,根据文档,cient_id 和 client_Secret 必须作为标头传递是不正确的:“发送客户端 ID 和机密的另一种方法是作为请求参数(client_id 和 client_secret)在POST 正文,而不是将它们发送到标头中的 base64 编码。”我试过用编码和标题,同样的错误。 啊,是的,问题是:response_type=code 重定向到/callback 它对你有用吗?我已经尝试在正文中使用客户端 ID 和密码,但没有成功,然后我尝试使用 Authorization 标头,它成功了

以上是关于Docker ( React / Flask / Nginx) - Spotify 授权码的主要内容,如果未能解决你的问题,请参考以下文章

Flask_restful 结合 React

了解 Docker/Docker-Compose 上的 Gunicorn 和 Flask

Flask 和 React 路由

如何在 React 和 Flask 中检测过期的 PayPal 订阅?

(sqlite,Flask + React),flask session session.get() 返回 None [重复]

使用 Flask 为使用 create-react-app 创建的前端提供服务