使用access_token+Direct Access Grants跳过Keycloak登录页实现浏览器流无缝访问
问题根因
你当前方案失效的核心原因非常明确:通过后端REST API调用密码模式生成的access_token仅存储在服务端上下文,用户浏览器侧完全没有Keycloak域名下的有效身份会话Cookie。当你重定向到网站2时,网站2会按照标准OIDC流程发起授权请求跳转到Keycloak,Keycloak校验浏览器请求时找不到对应有效会话,自然会强制返回登录页,和你后端已经拿到的token没有任何关联。
可行落地方案
以下三种方案都可以实现无感知跳转,按推荐优先级排序:
方案1:配置网站1为Keycloak自定义身份提供商(生产环境首选,安全性最高)
这是最符合OIDC协议规范的方案,完全不需要使用密码授权模式:
- 在网站2对接的Keycloak实例中,添加一个自定义身份提供商(IdP),对接网站1现有的认证体系
- 配置该IdP的认证流程,关闭所有交互环节(包括用户信息确认、账号绑定提示等页面)
- 用户在网站1完成认证后,生成带签名的临时身份断言,直接302跳转到Keycloak的IdP回调端点,携带已完成认证的断言信息
- Keycloak收到有效断言后,会自动建立自身域下的用户会话,直接完成网站2的OIDC授权流程,重定向回网站2,全程不会展示任何Keycloak页面
方案2:注入Keycloak会话Cookie实现静默登录
如果你必须复用现有密码模式获取token的逻辑,可以通过补全浏览器侧会话实现:
- 调用token接口时额外申请
offline_accessscope,从返回结果中获取session_state字段 - 持有Keycloak管理员权限,通过Keycloak的会话管理接口查询到该
session_state对应的会话信息,生成Keycloak原生格式的两个会话Cookie:KEYCLOAK_IDENTITY、KEYCLOAK_SESSION - 跳转网站2前,通过Keycloak域名下的代理接口/同域跳转逻辑,把上述两个Cookie写入用户浏览器,确保Cookie的
domain、path、SameSite、Secure、HttpOnly属性和Keycloak原生颁发的完全一致 - 完成Cookie写入后再跳转网站2,Keycloak识别到有效会话Cookie后会直接跳过登录流程
方案3:携带id_token_hint走静默授权流程
适合没有Keycloak管理员权限、无法修改IdP配置的场景:
- 调用token接口时确保返回结果中包含
id_token字段(需要在请求scope中添加openid) - 构造网站2的OIDC授权请求URL时,额外拼接两个参数:
prompt=none、id_token_hint=<获取到的id_token值> - 提前在Keycloak对应网站2的客户端配置中,开启「允许id_token_hint静默登录」开关,配置id_token的签名信任规则
- 该方案对Keycloak版本有要求,需使用12.0.0以上版本,且id_token的受众、过期时间、签名必须符合Keycloak的校验规则
踩坑提醒
- 不要尝试直接把后端拿到的
access_token传给网站2前端,绝大多数基于标准OIDC SDK开发的应用,只会认可自身走完整授权流程存储在自有域名下的token,不会接受外部传入的token参数 - 所有涉及跨域Cookie写入的场景,必须把Cookie的
SameSite属性设置为None,同时开启Secure属性(要求所有服务使用HTTPS),否则现代浏览器会拦截Cookie写入 - 不推荐长期使用密码授权模式,即使你已知晓其安全风险,该模式在最新的OAuth2.1规范中已经被正式废弃
问题相关参考代码
当前使用的密码模式获取token实现如下:
String uri = "https://keycloak02.localhost.com/auth/realms/master/protocol/openid-connect/token"; String username = "user"; String password = "pass"; String client_id="client_id"; String grant_type="password"; String client_secret="secret"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>(); map.add("username", username); map.add("password", password); map.add("client_id", client_id); map.add("grant_type", grant_type); map.add("client_secret", client_secret); HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(map, headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Object> responseEntity= restTemplate.exchange(uri, HttpMethod.POST, requestEntity, Object.class); Object object = responseEntity.getBody(); HttpStatus statusCode = responseEntity.getStatusCode(); if(!statusCode.equals(HttpStatus.OK)) { throw new Exception("Status code:" + statusCode + " uri:" + uri); } LinkedHashMap retMap = (LinkedHashMap) object; String token = StringUtils.trimString((String) retMap.get("access_token"));
补充说明:提问者已明确知晓password grants(密码授权模式)属于安全反模式。
内容的提问来源于stack exchange,提问作者daynok
相关产品推荐
相关产品推荐

