前几篇说的都是基于session的SSO(客户端应用的session、认证服务器的session),客户端应用拿到认证服务器返回的token后,将其存在自己的session, 用户登录状态是存在服务器端的。
本篇要说的是,要实现一个基于浏览器cookie的SSO,客户端应用获取到令牌后,不是将其存到session,而是写入浏览器cookie,这个改变会带来一些列问题,本篇将解决这些问题。
在OAuth授权回调里处理
客户端应用 客户token后的改造,在OAuth授权回调里处理,拿到token后写入cookie:
CookieTokenFilter
在客户端应用,引入zuul的依赖,写一个CookieTokenFilter,从cookie拿出token 加在请求头里。
package com.nb.security.admin; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.apache.commons.lang3.StringUtils; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 从cookie获取token,统一加到请求头中去 */ @Component public class CookieTokenFilter extends ZuulFilter { private RestTemplate restTemplate = new RestTemplate(); @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); HttpServletResponse response = requestContext.getResponse(); String accessToken = getCookie("nb_access_token"); if(StringUtils.isNotBlank(accessToken)){ //令牌放到请求头 requestContext.addZuulRequestHeader("Authorization","Bearer "+accessToken); }else { //从cookie把不到token说明token已过期,刷新令牌 String refreshToken = getCookie("nb_refresh_token"); if(StringUtils.isNotBlank(refreshToken)){ String oauthServiceUrl = "http://gateway.nb.com:9070/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json请求 //网关的appId,appSecret,需要在数据库oauth_client_details注册 headers.setBasicAuth("admin","123456"); MultiValueMap<String,String> params = new LinkedMultiValueMap<>(); params.add("refresh_token",refreshToken);//授权码 params.add("grant_type","refresh_token");//授权类型-刷新令牌 HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers); //刷新令牌的时候,可能refresh_token也过期了,这里进行处理,让用户重新走授权流程 try{ ResponseEntity<AccessToken> newToken = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, AccessToken.class); //令牌放到请求头 requestContext.addZuulRequestHeader("Authorization","Bearer "+newToken.getBody().getAccess_token()); //基于 Cookie的SSO,拿到token后写入浏览器Cookie Cookie accessTokenCookie = new Cookie("nb_access_token",newToken.getBody().getAccess_token()); accessTokenCookie.setMaxAge(newToken.getBody().getExpires_in().intValue()-3);//有效期 accessTokenCookie.setDomain("nb.com");//所有以nb.com结尾的二级域名都可以访问到cookie accessTokenCookie.setPath("/"); response.addCookie(accessTokenCookie); Cookie refreshTokenCookie = new Cookie("nb_refresh_token",newToken.getBody().getRefresh_token()); refreshTokenCookie.setMaxAge(2592000);//这里随便写一个很大的值(没用),如果是过期的token服务器将处理的。 refreshTokenCookie.setDomain("nb.com");//所有以nb.com结尾的二级域名都可以访问到cookie refreshTokenCookie.setPath("/"); response.addCookie(refreshTokenCookie); }catch (Exception e){ //有异常,重新登录 requestContext.setSendZuulResponse(false);//zuul过滤器不往下走了 requestContext.setResponseStatusCode(500);//响应状态码 requestContext.setResponseBody("{\"message\":\"refresh fail\"}"); requestContext.getResponse().setContentType("application/json"); } }else { //没用refresh——token,重新登录 requestContext.setSendZuulResponse(false);//zuul过滤器不往下走了 requestContext.setResponseStatusCode(500);//响应状态码 requestContext.setResponseBody("{\"message\":\"refresh fail\"}"); requestContext.getResponse().setContentType("application/json"); } } return null; } private String getCookie(String name) { String result = null; RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); Cookie[] cookies = request.getCookies(); for(Cookie cookie : cookies){ if(StringUtils.equals(cookie.getName(),name)){ result = cookie.getValue(); break; } } return result; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } }
客户端判断用户登录状态
在前端服务器判断用户是否登录,之前基于session的SSO的处理是,会在客户端应用admin里的session里着token,往前端服务器发了一个/me请求,session如果有东西说明用户已登录,现在客户端应用session里已经不存token了,客户端应用没办法知道你是否已经等了,所以这里需要换一下,就换成,在客户端应用的页面,往网关发一个/api/user/me请求,因为yml里已经配置了,/api/开头的请求,都会转发到网关。
客户端页面的改造:
前端服务器Controller在基于session-token方案时候判断用户登录状态,不用了:
在网关上,由于从客户端应用admin过来的请求,会在请求头里带一个token,然后经过了网关的权限过滤器后,会从token解析出用户名,放在请求头传下去:
这里加一个MeFilter,排序Order在授权过滤器之后,专门映射处理/user/me请求,它不往任何一个服务转发,只是从请求头拿username,如果拿得到,就说明用户是登录状态。
实验
1,启动4个服务
2,访问客户端应用 admin
3,点击去登录,跳转到认证服务器的登录页
3,输入用户名aaa(随便输入,认证服务器没校验)密码123456 (认证服务器写死的),点击登录,可以看到,一级域名nb.com下的cookie里已经存入了access_token、refresh_token 。
点击【获取订单信息】按钮,调用订单服务,会携带cookie里的token,然后在客户端admin上, CookieTokenFilter 会从cookie里读取到access_token和refresh_token,携带到请求头,转发给网关,网关校验token后,再将请求转发给订单服务。
到现在已经实现了基于cookie 的SSO,token信息是存在cookie里的,客户端应用的session里没有存token信息。
模拟access_token失效后,客户端应用admin 拿refresh_token 去认证服务器换取access_token。
客户端应用配置表里,access_token失效时间是20秒,refresh_token 失效时间是30秒
访问订单服务 正常是70多毫秒,大概在17秒(cookie失效时间是20-3秒)后,可以看到访问订单服务时间是200多毫秒,此时在admin上是拿refresh_token去认证服务器刷新了acces_token。
30秒后,refresh_token也失效了,调用订单服务,会返回异常,捕获这个异常,前端做判断,给用户提示,让用户退出登录。
logout
function logout() { //1浏览器cookie失效掉 $.removeCookie('nb_access_token',{domain:'nb.com',path:'/'}); $.removeCookie('nb_refresh_token',{domain:'nb.com',path:'/'}); //2,将认证服务器的session失效, /logout 是SpringSecurity OAuth默认的退出过滤器 // org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter window.location.href = "http://auth.nb.com:9090/logout?redirect_uri=http://admin.nb.com:8080/index"; }
这样就在refresh_token失效后,就完全退出登录,跳转客户端的登录页
目前的架构是这样的,token信息都存在了浏览器cookie,客户端应用并没有存token信息
基于cookie的SSO的优缺点
1,登录状态 用户的登录状态存在了浏览器的cookie,当cookie里的refresh_token失效的时候才会去认证服务器做登录 。这种方案不需要在认证服务器上设置有效期很长的session,只要一个很短的就可以了,比如30分钟,因为决定能不能访问服务的不是认证服务器的session,而是浏览器cookie里的refresh_token
2,安全性低,token存在了浏览器,有一定的风险(使用https,缩短access_token有效期)
3,可控性低,refresh_token和access_token存在了客户的浏览器里,没办法主动失效掉。
4,跨域:cookie只能放在nb.com ,只有nb.com的二级域名(admin.nb.com 、order.nb.com等)可以做SSO
好处:
复杂程度低,相对于基于session的SSO来说,只需要做access_token和refresh_token过期的处理
不占服务器的资源,适合于海量用户。
代码github : https://github.com/lhy1234/springcloud-security/tree/chapt-5-7-tokensso 如果对你帮助了,给个小星星呗
欢迎关注个人公众号一起交流学习: