前几篇说的都是基于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 如果对你帮助了,给个小星星呗