timmy 修订了这个 Gist 9 months ago. 转到此修订
1 file changed, 48 insertions, 18 deletions
flask_google_oauth_example.py
| @@ -1,45 +1,75 @@ | |||
| 1 | - | from flask import Flask, redirect, url_for, session | |
| 1 | + | from flask import Flask, redirect, url_for, session, request | |
| 2 | 2 | from authlib.integrations.flask_client import OAuth | |
| 3 | 3 | import os | |
| 4 | 4 | from dotenv import load_dotenv | |
| 5 | 5 | ||
| 6 | + | # 載入環境變數 | |
| 6 | 7 | load_dotenv() | |
| 8 | + | ||
| 7 | 9 | app = Flask(__name__) | |
| 8 | - | app.secret_key = "your_secret_key" # 必須設置 Secret Key | |
| 10 | + | app.secret_key = os.getenv("FLASK_SECRET_KEY") # 從環境變數讀取 | |
| 9 | 11 | ||
| 10 | 12 | oauth = OAuth(app) | |
| 11 | 13 | ||
| 12 | - | # 註冊 Google OAuth2 提供者,使用 discovery endpoint 自動獲取 metadata | |
| 13 | - | google = oauth.register( | |
| 14 | - | name='google', | |
| 15 | - | client_id=os.getenv("GOOGLE_CLIENT_ID"), | |
| 16 | - | client_secret=os.getenv("GOOGLE_CLIENT_SECRET"), | |
| 17 | - | server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', | |
| 14 | + | # 建立 Keycloak 的 OIDC 設定 | |
| 15 | + | # 透過 server_metadata_url 自動取得 OIDC Discovery 設定 | |
| 16 | + | keycloak = oauth.register( | |
| 17 | + | name='keycloak', | |
| 18 | + | client_id=os.getenv("KEYCLOAK_CLIENT_ID"), | |
| 19 | + | client_secret=os.getenv("KEYCLOAK_CLIENT_SECRET"), | |
| 20 | + | server_metadata_url=f"{os.getenv('KEYCLOAK_BASE_URL')}/realms/{os.getenv('KEYCLOAK_REALM')}/.well-known/openid-configuration", | |
| 18 | 21 | client_kwargs={ | |
| 19 | - | 'scope': 'openid profile email', | |
| 22 | + | 'scope': 'openid profile email', # 根據需求調整 | |
| 20 | 23 | } | |
| 21 | 24 | ) | |
| 22 | 25 | ||
| 23 | 26 | @app.route('/') | |
| 24 | 27 | def homepage(): | |
| 25 | - | return '歡迎!<a href="/login">使用 Google 登入</a>' | |
| 28 | + | return '歡迎!<a href="/login">使用 Keycloak 登入</a>' | |
| 26 | 29 | ||
| 27 | 30 | @app.route('/login') | |
| 28 | 31 | def login(): | |
| 29 | - | # 生成一個隨機 nonce,作為一次性驗證令牌 | |
| 32 | + | # 生成一個隨機 nonce 作為一次性驗證令牌 | |
| 30 | 33 | nonce = os.urandom(16).hex() | |
| 31 | 34 | session['nonce'] = nonce | |
| 32 | - | # 將 nonce 傳遞給 authorize_redirect | |
| 33 | - | return google.authorize_redirect(url_for('auth', _external=True), nonce=nonce) | |
| 35 | + | return keycloak.authorize_redirect( | |
| 36 | + | redirect_uri=url_for('auth', _external=True), | |
| 37 | + | nonce=nonce | |
| 38 | + | ) | |
| 34 | 39 | ||
| 35 | 40 | @app.route('/auth') | |
| 36 | 41 | def auth(): | |
| 37 | - | token = google.authorize_access_token() | |
| 38 | - | # 從 session 中取得先前生成的 nonce | |
| 42 | + | # 取得 Keycloak 回傳的參數並換取 token | |
| 43 | + | token = keycloak.authorize_access_token() | |
| 39 | 44 | nonce = session.get('nonce') | |
| 40 | - | # 將 nonce 傳入以驗證 ID Token | |
| 41 | - | user_info = google.parse_id_token(token, nonce=nonce) | |
| 42 | - | return f"歡迎, {user_info['name']}!" | |
| 45 | + | user_info = keycloak.parse_id_token(token, nonce=nonce) | |
| 46 | + | # 將使用者資訊存入 session,並清除 nonce 避免重複驗證 | |
| 47 | + | session['user'] = user_info | |
| 48 | + | session.pop('nonce', None) | |
| 49 | + | # 重導到乾淨的頁面 | |
| 50 | + | return redirect(url_for('profile')) | |
| 51 | + | ||
| 52 | + | @app.route('/profile') | |
| 53 | + | def profile(): | |
| 54 | + | user_info = session.get('user') | |
| 55 | + | if user_info: | |
| 56 | + | return f"歡迎, {user_info.get('name', '使用者')}!<br><a href='/logout'>登出</a>" | |
| 57 | + | return redirect(url_for('homepage')) | |
| 58 | + | ||
| 59 | + | @app.route('/logout') | |
| 60 | + | def logout(): | |
| 61 | + | # 清除 session 中的所有資料 | |
| 62 | + | session.clear() | |
| 63 | + | ||
| 64 | + | # 組成 Keycloak 的登出 URL | |
| 65 | + | logout_url = f"{os.getenv('KEYCLOAK_BASE_URL')}/realms/{os.getenv('KEYCLOAK_REALM')}/protocol/openid-connect/logout" | |
| 66 | + | ||
| 67 | + | # 登出後重導回首頁 | |
| 68 | + | redirect_uri = url_for('homepage', _external=True) | |
| 69 | + | ||
| 70 | + | # 將重導 URI 作為參數附加到 Keycloak 登出 URL 上 | |
| 71 | + | return redirect(f"{logout_url}?redirect_uri={redirect_uri}") | |
| 43 | 72 | ||
| 44 | 73 | if __name__ == '__main__': | |
| 45 | 74 | app.run(debug=True) | |
| 75 | + | ||
timmy 修订了这个 Gist 10 months ago. 转到此修订
2 files changed, 47 insertions
.env(文件已创建)
| @@ -0,0 +1,2 @@ | |||
| 1 | + | GOOGLE_CLIENT_ID=你的 Google Client ID | |
| 2 | + | GOOGLE_CLIENT_SECRET=你的 Google Client Secret | |
flask_google_oauth_example.py(文件已创建)
| @@ -0,0 +1,45 @@ | |||
| 1 | + | from flask import Flask, redirect, url_for, session | |
| 2 | + | from authlib.integrations.flask_client import OAuth | |
| 3 | + | import os | |
| 4 | + | from dotenv import load_dotenv | |
| 5 | + | ||
| 6 | + | load_dotenv() | |
| 7 | + | app = Flask(__name__) | |
| 8 | + | app.secret_key = "your_secret_key" # 必須設置 Secret Key | |
| 9 | + | ||
| 10 | + | oauth = OAuth(app) | |
| 11 | + | ||
| 12 | + | # 註冊 Google OAuth2 提供者,使用 discovery endpoint 自動獲取 metadata | |
| 13 | + | google = oauth.register( | |
| 14 | + | name='google', | |
| 15 | + | client_id=os.getenv("GOOGLE_CLIENT_ID"), | |
| 16 | + | client_secret=os.getenv("GOOGLE_CLIENT_SECRET"), | |
| 17 | + | server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', | |
| 18 | + | client_kwargs={ | |
| 19 | + | 'scope': 'openid profile email', | |
| 20 | + | } | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | @app.route('/') | |
| 24 | + | def homepage(): | |
| 25 | + | return '歡迎!<a href="/login">使用 Google 登入</a>' | |
| 26 | + | ||
| 27 | + | @app.route('/login') | |
| 28 | + | def login(): | |
| 29 | + | # 生成一個隨機 nonce,作為一次性驗證令牌 | |
| 30 | + | nonce = os.urandom(16).hex() | |
| 31 | + | session['nonce'] = nonce | |
| 32 | + | # 將 nonce 傳遞給 authorize_redirect | |
| 33 | + | return google.authorize_redirect(url_for('auth', _external=True), nonce=nonce) | |
| 34 | + | ||
| 35 | + | @app.route('/auth') | |
| 36 | + | def auth(): | |
| 37 | + | token = google.authorize_access_token() | |
| 38 | + | # 從 session 中取得先前生成的 nonce | |
| 39 | + | nonce = session.get('nonce') | |
| 40 | + | # 將 nonce 傳入以驗證 ID Token | |
| 41 | + | user_info = google.parse_id_token(token, nonce=nonce) | |
| 42 | + | return f"歡迎, {user_info['name']}!" | |
| 43 | + | ||
| 44 | + | if __name__ == '__main__': | |
| 45 | + | app.run(debug=True) | |