Keycloak登录页面自动跳转问题分析

我们使用Keycloak作为认证授权服务器,当用户Session过期时会自动跳转到登录页,这个功能看似很简单,但也需要前后端配合完成,并且在实现过程中也走了些弯路,明白了不少Nginx的配置相关的问题,总结出来为以后有类似需求的开发者。

问题一,Refresh Token过期时间问题

在OAuth2中,防止access token的泄漏,给access token限定一个较短的有效期以防止泄漏带来的风险,然而引入了有效期之后,客户端使用起来就不那么方便了。每当 access token 过期,客户端就必须重新向用户索要授权。这样用户可能每隔几天,甚至每天都需要进行授权操作。这是一件非常影响用户体验的事情。希望有一种方法,可以避免这种情况。于是 Oauth2.0 引入了 refresh token 机制。refresh token 的作用是用来刷新 access token。调用方法如下:
  • 基于安全的考虑,OAuth2.0 要求,refresh token 一定是保存在服务器上,而绝不能存放在客户端上。调用 refresh 接口的时候,一定是从服务器到服务器的访问;
  • OAuth2.0 引入了 client_id 、client_secret 机制。即每一个应用都会被分配到一个 client_id 和一个对应的 client_secret。应用必须把 client_secret 妥善保管在服务器上,决不能泄露。刷新 access token 时,需要验证这个 client_secret。
因此,获取refresh token的post方法如下:
  1. POST /refresh
  2. 参数:
  3. refresh token
  4. client_id
  5. signatrue 签名,sha256(client_id + refresh_token + client_secret)
  6. 返回:
  7. 新的 access token
明白了refresh token的作用,因此一般把access token的过期时间设置比较短,refresh token的过期时间设置比较长的时间。但发现在Keycloak的控制台里没有发现可以配置refresh token的功能,于是在网上查找了各种论坛,发现原来SSO Session Idle就是refresh token的过期时间,默认设置是1800秒。发现我项目中的配置把assess token的时间和session idle的时间都设成了2个小时,因此导致refresh token基本上没有发挥作用,因为它们同时失效。因此将SSO session idle 时间设成2小时,Access Token lifespan设成30分钟,如下图,保证refresh token过期时间长于access token的过期时间。

问题二,静态资源鉴权导致浏览器重定向登录页面不能正常显示问题

之前的文章里我介绍过如何在Keycloak里如何使用OIDC-PROXY来拦截用户的请求,然后实现用户的认证和授权,了解详情可以参考我之前的文章,当时的配置方法如下所示:
  1. location / {
  2. limit_req zone=mylimit burst=20 nodelay;
  3. limit_conn myaddr 50;
  4. #access_by_lua_file lua/auth.lua;
  5. add_header Allow "GET" always;
  6. proxy_set_header Host $host;
  7. proxy_set_header X-Forwarded-Server $host;
  8. proxy_set_header X-Real-IP $remote_addr;
  9. proxy_pass http://backend-service/;
  10. proxy_set_header Cookie "";
  11. if ( $request_method !~ ^(GET)$ ) {
  12. return 405;
  13. }
  14. }
这样实现会将匹配/这个URL下的所有请求都会被Keycloak认证,如果session过期,因此会跳转到登录页面。在测试的过程中发现,当session过期后,nginx会调用ngx.redirect方法跳转到登录页面,但登录页面不能正常显示。打开F12,发现openid-connect/auth这个登录页面返回200,但没有正常显示,分析request header,发现Accept: image/png,我理解应该是text/html才是正常的header,因此浏览器不能正确的渲染出login页面。而这个请求的发起者是CSS脚本,因为CSS脚本里引用了这个图片。这里科普一下http request和response里的Accept和Content-Type的区别,作为http请求方,Accept表示接受返回什么MIME TYPE,Content-Type表示发送给服务器是什么MIME TYPE。在Response Header里,Content-Type表示服务器返回什么MIME TYPE,这个MIME TYPE是按照content negotiation规则从请求的Accept中选取。明白了这个规则,因此这个请求Accept的是image/png,但浏览器返回的是html,因此不能正常渲染页面。

因此想到的解决办法是对于这种静态资源文件不经过keycloak进行认证,因为对这种图片,脚本,字体等静态资源文件进行鉴权也会导致性能的损失,我们项目中也没有这种需求。因此将Nginx的脚本改成如下配置:
  1. location / {
  2. limit_req zone=mylimit burst=20 nodelay;
  3. limit_conn myaddr 50;
  4. if ($request_uri !~* \.(js|css|jpg|jpeg|gif|ico|svg|txt|woff2|otf|png)${
  5. access_by_lua_file lua/auth.lua;
  6. }
  7. add_header Allow "GET" always;
  8. proxy_set_header Host $host;
  9. proxy_set_header X-Forwarded-Server $host;
  10. proxy_set_header X-Real-IP $remote_addr;
  11. proxy_pass http://backend-service/;
  12. proxy_set_header Cookie "";
  13. if ( $request_method !~ ^(GET)$ ) {
  14. return 405;
  15. }
  16. }

问题三,API鉴权导致浏览器重定向登录页面不能正常显示问题

上面的办法解决了静态资源鉴权重定向问题,但当前端程序使用axios调用后端的API时,如果用户session过期,nginx会调用ngx.redirect方法跳转到Keycloak登录页面,但同样登录页面不能正常显示,分析request header,发现Accept:application/json,因此浏览器也不能正确的渲染出login页面。但对于API我们是不能像静态资源一样跳过鉴权的。因此,首先想到的办法是想在Nginx里更改重定向login页面的Accept为text/html。这里顺带介绍一下在Nginx里的几个set header方法的作用
  • proxy_set_header,即允许重新定义或添加字段传递给代理服务器的请求头。该值可以包含文本、变量和它们的组合。在没有定义proxy_set_header时会继承之前定义的值
  • add_header:当response code等于200, 201, 204, 206, 301, 302, 303, 304, 307, 308,向响应报文头部添加自定义字段,并赋值。
  • 使用Nginx Lua来ngx.header方法来设置。
但是,这几个方法都不能修改redirect请求里的request headers里的Accept值。因为302是浏览器的行为,而在nginx里只能设置反向代理request的header和反向代理后的response的header,因此没办法满足我们的功能。基于API的访问,由于我们是前端使用axios库来调用的,因此下面尝试通过在前端调用端解决这个问题。

问题四,前端处理302状态码

基于在第三步中的尝试失败,因此决定在前端调用出来处理302状态码,这种方法应该能生效,如是在axios的拦截处加入如下代码:
  1. instance.interceptors.response.use(function (response) {
  2. return response;
  3. }, function (error) {
  4. if (error.response && error.response.status === 302) {
  5. window.location = login web url;
  6. }
  7. return Promise.reject(error);
  8. });
发现当发生跳转时没有进行到代码捕获处,发现浏览器302是浏览器的行为,不会被前端代码捕获到,前端接收到的是跳转页面的html内容,状态码是200,因此,修改逻辑如下:
  1. instance.interceptors.response.use(function (response) {
  2. if (response.request.responseURL &&
  3. response.request.responseURL.includes('登录url唯一字符串')) {
  4. window.location = login web url;
  5. }
  6. return response;
  7. }, function (error) {
  8. return Promise.reject(error);
  9. });
注意log web url需要替换成你项目中的登录url,登录url唯一字符串替换成你项目中的,只要是能唯一决定这个url是redirect url即可。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356