我们使用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方法如下:
-
POST /refresh
-
参数:
-
refresh token
-
client_id
-
signatrue 签名,sha256(client_id + refresh_token + client_secret)
-
返回:
-
新的 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来拦截用户的请求,然后实现用户的认证和授权,了解详情可以参考我之前的文章,当时的配置方法如下所示:
-
location / {
-
limit_req zone=mylimit burst=20 nodelay;
-
limit_conn myaddr 50;
-
#access_by_lua_file lua/auth.lua;
-
add_header Allow "GET" always;
-
proxy_set_header Host $host;
-
proxy_set_header X-Forwarded-Server $host;
-
proxy_set_header X-Real-IP $remote_addr;
-
proxy_pass http://backend-service/;
-
proxy_set_header Cookie "";
-
if ( $request_method !~ ^(GET)$ ) {
-
return 405;
-
}
-
-
}
这样实现会将匹配/这个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的脚本改成如下配置:
-
location / {
-
limit_req zone=mylimit burst=20 nodelay;
-
limit_conn myaddr 50;
-
if ($request_uri !~* \.(js|css|jpg|jpeg|gif|ico|svg|txt|woff2|otf|png)${
-
access_by_lua_file lua/auth.lua;
-
}
- add_header Allow "GET" always;
-
-
proxy_set_header Host $host;
-
proxy_set_header X-Forwarded-Server $host;
-
proxy_set_header X-Real-IP $remote_addr;
-
proxy_pass http://backend-service/;
-
proxy_set_header Cookie "";
-
if ( $request_method !~ ^(GET)$ ) {
-
return 405;
-
}
-
-
}
问题三,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的拦截处加入如下代码:
-
instance.interceptors.response.use(function (response) {
-
return response;
-
}, function (error) {
-
if (error.response && error.response.status === 302) {
-
window.location = login web url;
-
}
-
return Promise.reject(error);
-
});
发现当发生跳转时没有进行到代码捕获处,发现浏览器302是浏览器的行为,不会被前端代码捕获到,前端接收到的是跳转页面的html内容,状态码是200,因此,修改逻辑如下:
-
instance.interceptors.response.use(function (response) {
-
if (response.request.responseURL &&
-
response.request.responseURL.includes('登录url唯一字符串')) {
-
window.location = login web url;
-
}
-
return response;
-
}, function (error) {
-
return Promise.reject(error);
-
});
注意log web url需要替换成你项目中的登录url,登录url唯一字符串替换成你项目中的,只要是能唯一决定这个url是redirect url即可。