You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flask-OIDC集成Keycloak:登出后自动登录无需凭证问题

Flask-OIDC集成Keycloak:登出后自动登录问题解决

问题概述

使用Flask-OIDC将Keycloak与Flask应用集成实现认证功能,目标是用户执行登出操作后,Flask和Keycloak端的会话均被完全清除,访问受保护页面时需重新输入账号密码登录。但目前登出后用户无需输入凭证即可自动登录进入受保护页面。

预期行为

  • 用户通过Keycloak输入账号密码成功登录
  • 用户执行登出操作后,所有会话被清除
  • 访问受保护页面/profile时,被重定向到Keycloak登录页面,需重新输入凭证

当前行为

  • 用户成功登录后执行登出操作,Flask会话已清除(oidc.user_loggedin变为False)
  • 首次访问/profile被重定向到主页
  • 再次访问/profile时无需跳转登录页面,直接自动完成登录进入页面

问题代码

app.config.update({
    'SECRET_KEY': '20221e754l7a45909ef4Ij66ac984bea',  # random secret key
    'OIDC_CLIENT_SECRETS': 'keycloak.json', # information about issuer, client_secret, token_uri etc
    'OIDC_SCOPES': ['openid', 'email'],
    'OIDC_REDIRECT_URI': 'http://localhost:5000/oidc/callback', # redirect link, conventionally named
    'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post', 
    'OIDC_INTROSPECTION_AUTH': True
})

oidc = OpenIDConnect(app)


@app.route('/')
def home():
    return 'Welcome to the Flask App! <a href="/login">Log in</a>'


@app.route('/login')
def login():
    print("login, oidc.user_loggedin ", oidc.user_loggedin)
    if oidc.user_loggedin:
        return redirect(url_for('profile'))
    
    # redirect the user to the Keycloak login page
    redirect_uri = url_for('oidc_callback', _external=True)
    return oidc.authorize_redirect(redirect_uri, prompt='login')

@app.route('/oidc/callback')
def oidc_callback():
    print("oidc_callback, oidc.user_loggedin ", oidc.user_loggedin)

    token = oidc.authorize_access_token() 
    if token:
        user_info = oidc.user_getinfo(['sub', 'email']) 
        session['user'] = user_info 
        return redirect(url_for('profile'))  

    return 'Login failed', 401

@app.route('/profile')
def profile():
    print("profile, oidc.user_loggedin ", oidc.user_loggedin)
    if oidc.user_loggedin:
        user_email = oidc.user_getfield('email')
        return f"Hello, {user_email}!"
    
    return redirect(url_for('login'))  

@app.route('/logout')
def logout():
    print("logout, oidc.user_loggedin ", oidc.user_loggedin)
    if oidc.user_loggedin:
        session.clear()  
        oidc.logout()
        id_token = oidc.get_id_token()
        logout_url = (oidc.client_secrets['issuer'] +
                      '/protocol/openid-connect/logout?id_token_hint=' +
                      id_token + '&post_logout_redirect_uri=' +
                      url_for('home', _external=True))
        return redirect(logout_url) 

    return redirect(url_for('home')) 


if __name__ == '__main__':
    app.run(debug=True)

日志信息

profile, oidc.user_loggedin  False
127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /profile HTTP/1.1" 302 -
profile, oidc.user_loggedin  False
127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /profile HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /login HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:32] "GET /authorize?state=UYE5nA1KHfXwYI5Dn6Id0JJzhsZOa4&session_state=5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c&iss=http://localhost:8080/realms/my-realm&code=c4976ae2-b0f8-48ae-8de0-37445b00b732.5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c.5f8c05b4-8d59-4210-a147-1a4d29ea0026 HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:33] "GET / HTTP/1.1" 200 -
profile, oidc.user_loggedin  True
127.0.0.1 - - [30/Sep/2024 09:28:36] "GET /profile HTTP/1.1" 200 -
profile, oidc.user_loggedin  True
127.0.0.1 - - [30/Sep/2024 09:28:37] "GET /profile HTTP/1.1" 200 -
127.0.0.1 - - [30/Sep/2024 09:28:40] "GET /logout HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:41] "GET / HTTP/1.1" 200 -
profile, oidc.user_loggedin  False
127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /profile HTTP/1.1" 302 -
profile, oidc.user_loggedin  False
127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /profile HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /login HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /authorize?state=Hhoq1PAusTxuX7BI9zh0xRxTpukG3V&session_state=5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c&iss=http://localhost:8080/realms/my-realm&code=0e10de2f-5f8b-4032-8741-639499f1cf5c.5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c.5f8c05b4-8d59-4210-a147-1a4d29ea0026 HTTP/1.1" 302 -
127.0.0.1 - - [30/Sep/2024 09:28:42] "GET / HTTP/1.1" 200 -
profile, oidc.user_loggedin  True
127.0.0.1 - - [30/Sep/2024 09:28:43] "GET /profile HTTP/1.1" 200 -
profile, oidc.user_loggedin  True
127.0.0.1 - - [30/Sep/2024 09:28:44] "GET /profile HTTP/1.1" 200 -

已尝试方法

  • 禁用客户端设置中的Direct Access Grants
  • 登出后清除Flask会话,oidc.user_loggedin变为False
  • 启用Front-Channel Logout
  • 在不同浏览器及隐身模式下测试
  • 在授权请求中添加prompt='login'参数
  • 使用@oidc.require_login装饰器

解决方案

1. 修复登出逻辑,确保Keycloak会话彻底销毁

当前登出代码的执行顺序和参数传递存在问题,导致Keycloak端会话未被完全销毁。修改logout路由:

@app.route('/logout')
def logout():
    print("logout, oidc.user_loggedin ", oidc.user_loggedin)
    if oidc.user_loggedin:
        # 获取id_token用于Keycloak登出验证
        id_token = oidc.get_id_token()
        # 先清除Flask-OIDC内部会话
        oidc.logout()
        # 清除Flask全局session
        session.clear()
        # 清除Flask-OIDC本地令牌存储
        oidc.store.clear()
        # 构造完整的Keycloak登出URL,添加client_id确保会话匹配
        logout_url = (
            oidc.client_secrets['issuer'] + '/protocol/openid-connect/logout'
            f'?id_token_hint={id_token}'
            f'&post_logout_redirect_uri={url_for("home", _external=True)}'
            f'&client_id={你的Keycloak客户端ID}'  # 替换为实际客户端ID
        )
        return redirect(logout_url)
    return redirect(url_for('home'))

2. 强化授权请求的强制认证参数

在login路由中添加max_age=0参数,强制Keycloak忽略现有会话,要求用户重新输入凭证:

@app.route('/login')
def login():
    print("login, oidc.user_loggedin ", oidc.user_loggedin)
    if oidc.user_loggedin:
        return redirect(url_for('profile'))
    
    redirect_uri = url_for('oidc_callback', _external=True)
    # 同时设置prompt='login'和max_age=0,强制重新认证
    return oidc.authorize_redirect(
        redirect_uri,
        prompt='login',
        max_age=0
    )

3. 调整Keycloak客户端配置

  • 将客户端Access Type设置为confidential(匹配代码中client_secret_post的认证方式)
  • 在Logout Settings中,确保Post Logout Redirect URIs包含Flask应用主页URL(如http://localhost:5000/)
  • 启用Backchannel Logout(相比Front-Channel Logout,Backchannel能更可靠地销毁服务器端会话)

问题原因分析

从日志可见,登出后访问/login时Keycloak仍返回了授权码,说明Keycloak端的用户会话未被彻底销毁。核心原因包括:

  • 登出URL未携带client_id,Keycloak无法精准匹配并销毁对应客户端的会话
  • 授权请求的强制认证参数未完全生效,Keycloak仍复用了保留的用户会话
  • Flask-OIDC本地存储的令牌未被完全清除,导致客户端仍能获取有效认证信息

内容的提问来源于stack exchange,提问作者greenblack48

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 22:24:56