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即可。

你可能感兴趣的:(Keycloak登录页面自动跳转问题分析)