sso实际流程分析记录
- 前置知识
- sso分析记录
- 1.sso启动分析
- 2.sso流程分析
- 2.1.组装重定向sso登录页面
- 2.2.sso登录页面
- 2.3.sso重定向应用系统/login
- 答疑:
- 获取token后,用户信息在哪里保存到redis的?
- 认证通过后,如何判断请求是有效的
- 每次访问,session有效期都要刷新,在哪里刷新?
- 如何sso的,一次登录,处处访问
- cookie和session关系,以及cookie写入域名问题
- session过期后如何处理
- 集成spring-session
- token是如何刷新的
- 登出
前置知识
需要对spring-security有一定了解
sso分析记录
1.sso启动分析
引入基础框架web模块,启动类加@EnableWeb,并且属性(apollo)添加sso申请的sso.appId,sso.secret。
@EnableWeb引入EnableSSOImportSelector,引入了三个配置类SSOConfiguration、SessionConfiguration、WebConfiguration,重点是前两个配置类。
配置类SSOConfiguration上标注了@EnableWebSecurity开启了spring-security,注册bean SSOConfig,最终会注册bean FilterChainProxy。
SSOConfig实现了spring-security的WebSecurityConfigurerAdapter,因此只需要关注void configure(HttpSecurity http)方法即可,该方法配置一系列子Filter。
FilterChainProxy内部有两组filter
第一组,默认使用这组
[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@349470c1,
org.springframework.security.web.context.SecurityContextPersistenceFilter@2d62612c,
org.springframework.security.web.header.HeaderWriterFilter@1e23eb19,
org.springframework.security.web.authentication.logout.LogoutFilter@1e2e58c3,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1700da2a,
com.zzz.web.filter.LoginAndLogoutFilter@19d10355,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@428fe575,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7e95bcc1,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@75cbbd7,
org.springframework.security.web.session.SessionManagementFilter@151a4691,
org.springframework.security.web.access.ExceptionTranslationFilter@48fcb93b,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1c1bb3df]
第二组
[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4909267f,
org.springframework.security.web.context.SecurityContextPersistenceFilter@12dc1d25,
org.springframework.security.web.header.HeaderWriterFilter@4700735a,
org.springframework.security.web.csrf.CsrfFilter@77923384,
org.springframework.security.web.authentication.logout.LogoutFilter@36481c6f,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7921eced,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@22d4d919,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@3fb9fd37,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@725144c1,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@329b8371,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@76c5bcdb,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@337df06d,
org.springframework.security.web.session.SessionManagementFilter@74b41e24,
org.springframework.security.web.access.ExceptionTranslationFilter@42bc4b64,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@595c47eb]
其中重点关注SecurityContextPersistenceFilter、LoginAndLogoutFilter、ExceptionTranslationFilter、FilterSecurityInterceptor
配置类SessionConfiguration主要是生成bean SessionRepositoryImpl,用于保存session信息,通常我们使用redis保存session,主要方法是public RedisSession findById(String id)
方法,具体与redis交互类是RedisSessionStorageImpl。
2.sso流程分析
2.1.组装重定向sso登录页面
以a-system为例,前端首次访问a-system,会先请求/api/mango_privilege接口,该接口请求成功后,再访问其它接口。
比如首次访问a-system,前端请求/api/mango_privilege,由于此时请求不携带cookie或者cookie已过期等,无法从session中查询到用户信息,因此在过滤器FilterSecurityInterceptor内执行抛出AccessDeniedException: Access is denied
异常,被ExceptionTranslationFilter捕获,执行认证入口点AuthenticationEntryPoint的commence方法,即执行LoginAndLogoutFilter.commence(HttpServletRequest, HttpServletResponse, AuthenticationException)
,下面是核心代码
//com.zzz.web.filter.LoginAndLogoutFilter.processEntryPoint(HttpServletRequest, HttpServletResponse, AuthenticationException)
public void processEntryPoint(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
String state = TokenUtils.randomState();//代码@1
request.getSession().setAttribute(LOGIN_STATE, state);//代码@2
//向sso发起请求的url中传入参数为原请求的uri
String reqUrl = getIndexUrl();//代码@3
if (reqUrl == null || "null".equals(reqUrl) || "".equals(reqUrl)) {
StringBuffer url = request.getRequestURL();
reqUrl = url.delete(url.length() - request.getRequestURI().length(), url.length()).append("/").toString();
}
//组装请求sso url
String requestURI = buildRedirectFrontUrl(request, response);//代码@4
requestURI = Base64Utils.encodeToString(requestURI.getBytes());//base64编码requestURI
String loginUrl = getLoginUrl(state, requestURI, reqUrl);//代码@5
//判断是否属于ajax请求
if (RequestUtil.isRequestAjax(request) || RequestUtil.isJsonRequest(request)) {
String resultObj = JSONObject.toJSONString(Result.error("302", loginUrl));
response.getWriter().write(resultObj);//代码@6
} else {
response.setContentType("application/json;charset=utf-8");
response.sendRedirect(loginUrl);
}
}
代码@1:生成个类似uuid的随机数,作为oauth2里面的state,用作后续sso跳转应用接口时候的校验值。
代码@2:request.getSession()很重要,看servlet HttpServletRequest该接口注释,返回当前请求的session,否则如果当前请求session不存在则创建session。还有个request.getSession(false)方式只是返回当前请求session,并不会创建。request.getSession().setAttribute(LOGIN_STATE, state)意思是把state保存到session内。session是个会话,state到底保存到哪里了呢?这里是保存到了redis内,具体执行堆栈
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(boolean)// 这里创建session
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession()
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession()
StrictHttpFirewall$1(HttpServletRequestWrapper).getSession()
HttpSessionSecurityContextRepository$Servlet3SaveToSessionRequestWrapper(HttpServletRequestWrapper).getSession()
HeaderWriterFilter$HeaderWriterRequest(HttpServletRequestWrapper).getSession()
HttpSessionSecurityContextRepository$Servlet3SaveToSessionRequestWrapper(HttpServletRequestWrapper).getSession()
HttpServlet3RequestFactory$Servlet3SecurityContextHolderAwareRequestWrapper(HttpServletRequestWrapper).getSession()
LoginAndLogoutFilter.processEntryPoint(HttpServletRequest, HttpServletResponse, AuthenticationException)
创建session代码为S session = SessionRepositoryFilter.this.sessionRepository.createSession();
最终创建session操作是com.zzz.web.session.SessionRepositoryImpl.createSession()
,sessionid就是uuid,接着创建真正的servlet session对象HttpSession。
代码@3:获取配置的sso.indexUrl,比如此处是a-system域名
代码@4:构建重定向到前端的请求url,通常是request header中的referer域名
代码@5:组装跳转sso的url(到sso认证用户密码),比如https://connect.zzz.com/oauth2/authorize?appid=zzz123456&redirect_uri=http%3A%2F%2Fares.zzzsys.com%2Flogin%3FredirectFrontURI%3DaHR0cDovL2FyZXMuenRvc3lzLmNvbS8%3D&response_type=code&scope=userinfo,usercontact,userprofile,user_id&state=soqRLsIHdu631uctsfv2n2j1fbT5R3Icf7U
,通过url和base64解码结果为https://connect.zzz.com/oauth2/authorize?appid=zzz123456&redirect_uri=http://ares.zzzsys.com/login?redirectFrontURI=http://ares.zzzsys.com/&response_type=code&scope=userinfo,usercontact,userprofile,user_id&state=soqRLsIHdu631uctsfv2n2j1fbT5R3Icf7U
,oauth2认证url规范格式就是携带appid,redirect_uri,scope,state
代码@6:重定向到sso认证接口。重定向即返回前端302,浏览器再次发起重定向的地址,这个过程和前端代码并没关系,是浏览器触发,属于http规范。
由于此时对于域名ares.zzzsys.com已经生成了session,那么就要写cookie了,写cookie的动作是在web层filter SessionRepositoryFilter,具体代码是
//org.springframework.session.web.http.CookieHttpSessionIdResolver.setSessionId(HttpServletRequest, HttpServletResponse, String)
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));//使用cookieSerializer进行写cookie
}
//com.zzz.web.session.web.TitansCookieSerializer.writeCookieValue(CookieValue)
@Override
public void writeCookieValue(CookieValue cookieValue) {
springCookieSerializer.writeCookieValue(cookieValue);//内部框架实现CookieSerializer,写cookie,cookie名称SESSION,cookie值就是sessionid的base64编码
if(sameSiteNone){
writeTitansCookieValue(cookieValue);
}
}
最终使用CookieHttpSessionIdResolver调用CookieSerializer写cookie,可以通过实现CookieSerializer来改写cookie名称,domain等。
最终浏览器收到的请求header中有 Set-Cookie: SESSION=ZjY0NmZjZjUtMGI3NS00MGJkLTk0NGItNmYzOTZhMGI4Mzk5; Path=/,根据http规范,浏览器把cookie写入客户端,在后续访问该域名时候,都会在请求内上送次cookie。
2.2.sso登录页面
在该页面,输入用户密码,sso进行认证,认证通过重定向到redirect_uri地址,即/login
其中用户密码被加密后在http header的Authorization字段上送。
2.3.sso重定向应用系统/login
sso验证用户密码匹配,重定向到redirect_uri地址(携带用户认证通过后的code),即/login,此时浏览器发送的http://ares.zzzsys.com/login?code=5NNFLmoaEeuoFwBQVq0i0A&redirectFrontURI=aHR0cDovL2FyZXMuenRvc3lzLmNvbS8%3D&state=sor7Y3B7ie5Wal9w6kcJ7xcA542v2j63erM
,在filter org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain)执行SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
时候根据请求上送的cookie,从redis查询到session,具体代码如下:
//根据请求携带的cookie,从session获取安全上下文SecurityContext,即SecurityContextImpl,包含认证对象Authentication,对于内部框架来说是com.zzz.web.filter.User
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);//核心方法
SecurityContext context = readSecurityContextFromSession(httpSession);//从HttpSession属性获取key为SPRING_SECURITY_CONTEXT的值,就是SecurityContext
if (context == null) {
context = generateNewContext();//session不存在SecurityContext,生成新的SecurityContext
}
SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(
response, request, httpSession != null, context);
requestResponseHolder.setResponse(wrappedResponse);
if (isServlet3) {
requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper(
request, wrappedResponse));
}
return context;
}
HttpSession httpSession = request.getSession(false);
是核心方法,分析这个方法
request.getSession(false)获取session(如果session不存在,并不会创建session),执行堆栈如下
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getRequestedSession() //根据sessionid从redis查询获取session对象
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(boolean)
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(boolean)
StrictHttpFirewall$1(HttpServletRequestWrapper).getSession(boolean)
HttpSessionSecurityContextRepository.loadContext(HttpRequestResponseHolder)
SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain)
真正获取session对象方法在org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.getRequestedSession()
从Request请求的cookie中获取cookie值,即sessionid,然后根据sessionid从redis中获取session,返回第一个非null的session。
接着从HttpSession属性获取key为SPRING_SECURITY_CONTEXT的值,就是SecurityContext,代码是SecurityContext context = readSecurityContextFromSession(httpSession);
,那么key为SPRING_SECURITY_CONTEXT是什么时候存储到session的呢?在sso重定向到/login请求时候,在com.zzz.web.filter.LoginAndLogoutFilter.doFilter(ServletRequest, ServletResponse, FilterChain)
内,具体代码如下图
保存安全上下文SecurityContext到session的具体代码在org.springframework.security.web.context.HttpSessionSecurityContextRepository.SaveToSessionResponseWrapper.saveContext(SecurityContext)
,具体代码如下图
答疑:
获取token后,用户信息在哪里保存到redis的?
在com.zzz.web.filter.LoginAndLogoutFilter.doFilter(ServletRequest, ServletResponse, FilterChain)请求处理sso重定向来的/login请求,把认证对象保存到session,即保存到redis,key是SPRING_SECURITY_CONTEXT。具体代码就是getRepo().saveContext(SecurityContextHolder.getContext(), holder.getRequest(),holder.getResponse());
,从而在SecurityContextPersistenceFilter处理的时候,根据sessionid查询出session,即认证用户信息,存放到当前线程,这样在FilterSecurityInterceptor处理时候经过认证。
认证通过后,如何判断请求是有效的
过滤器SecurityContextPersistenceFilter会通过请求携带的cookie获取sessionid,从而获取session,从而从session获取认证对象保存到SecurityContext,这样在FilterSecurityInterceptor鉴权时候就通过。
每次访问,session有效期都要刷新,在哪里刷新?
还是在SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain)
内,根据cookie获取sessionid,根据sessionid获取到session,更新session的lastAccessedTime为当前时间戳。具体执行堆栈
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getRequestedSession()
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(boolean)
SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(boolean)
StrictHttpFirewall$1(HttpServletRequestWrapper).getSession(boolean)
HttpSessionSecurityContextRepository.loadContext(HttpRequestResponseHolder)
SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain)
具体代码
如何sso的,一次登录,处处访问
sso认证用户通过后,会向浏览器写入cookie,cookie名称是com.zzz.lid,domain是zzz.com,这样根据http规范,在访问zzz.com结尾的域名时候,http请求都会携带上此cookie,举例如下:
step1:用户第一次访问a-system,由于请求不携带cookie或者cookie失效,FilterSecurityInterceptor鉴权失败,被ExceptionTranslationFilter捕捉访问异常,执行LoginAndLogoutFilter.commence方法,创建session并缓存到redis,组装重定向到sso的登录url,此url符合oauth2规范,同时向客户端写入cookie,cookie范围是当前a-system域名,cookie名称是SESSION,即Set-Cookie: SESSION=YzA0YTliYmItNmMyOC00NTI4LTkzOTItYWNhZmYwNDQ2YWY0; Path=/
step2:重定向到sso登录页面,用户登录,sso向浏览器写cookie,即Set-Cookie: com.zzz.sessionid=0d65a791cfe4618ee6233ffef578e25dc51e3f5a; Path=/; Domain=zzz.com; Max-Age=604800; HttpOnly; Secure; SameSite=None
step3:sso重定向到a-system的/login请求,此请求是a-system域名,会携带step1写入a-system域名的cookie,经过LoginAndLogoutFilter处理,根据cookie获取session,根据code获取token,保存用户认证信息到session,即缓存到redis,key是sessionid,重定向到a-system首页。
step4:用户访问a-system其它接口,每次请求都会携带step1写入a-system域名的cookie,通过SecurityContextPersistenceFilter处理,根据cookie获取sessionid,根据sessionid获取session,即根据sessionid从redis查询出缓存的用户信息,这样再通过FilterSecurityInterceptor就鉴权通过,可以正常访问a-system了。
如果用户接着访问b-system,处理如下
step5:同step1,用户第一次访问b-system,由于请求不携带cookie或者cookie失效,FilterSecurityInterceptor鉴权失败,被ExceptionTranslationFilter捕捉访问异常,执行LoginAndLogoutFilter.commence方法,创建session并缓存到redis,组装重定向到sso的登录url,此url符合oauth2规范,同时向客户端写入cookie,cookie范围是当前b-system域名,cookie名称是SESSION,即Set-Cookie: SESSION=YzA0YTliYmItNmMyOC00NTI4LTkzOTItYWNhZmYwNDQ2YWY0; Path=/
step6:重定向到sso登录页面,由于此前访问a-system的时候,sso已经写了cookie com.zzz.sessionid,此时会携带上,sso判断com.zzz.sessionid还在有效期内,因此不再出现登录页面,直接重定向到b-system的 /login请求
step7:sso重定向到b-system的/login请求,此请求是b-system域名,会携带step5写入b-system域名的cookie,经过LoginAndLogoutFilter处理,根据cookie获取session,根据code获取token,保存用户认证信息到session,即缓存到redis,key是sessionid,重定向到b-system首页。
step8:同step4,用户访问b-system其它接口,每次请求都会携带step5写入b-system域名的cookie,通过SecurityContextPersistenceFilter处理,根据cookie获取sessionid,根据sessionid获取session,即根据sessionid从redis查询出缓存的用户信息,这样再通过FilterSecurityInterceptor就鉴权通过,可以正常访问b-system了。
同理,访问其它应用系统,也是如此,每次第一次访问第一个系统,都会跳转到sso登录页面,只是根据携带的cookie com.zzz.sessionid,sso判断是否显示登录页面,从而再跳转应用系统的/login,获取token,写入session。因此不同应用系统的session是不同的,如果想保持一个应用一直不掉线,只需要定时刷新各自应用的接口即可,这样session每次回更新lastAccessedTime,和token过期时间无关。
cookie和session关系,以及cookie写入域名问题
http是无状态,需要保持状态就发明了cookie和session,实际cookie和session是一回事,cookie保存在浏览器端,session保存在服务端,cookie的value就是sessionid的base64编码后的结果。当前域名有cookie时候,那么发送http请求时候就会自动被上送(前端有配置项可以控制是否携带cookie),那么根据cookie,就知道了sessionid,根据sessionid就获取了session对象,session内存放的就是我们需要的用户信息。session可以保存在内存,分布式中即保存在redis中。
根据http规范,Response header有Set-Cookie时候,浏览器就需要写入cookie,此动作是浏览器控制,非前端代码控制。
cookie和domain关系,cookie默认是写在当前域名下,但是,如果指定了domain,则写到指定的domain下,比如Set-Cookie: com.zzz.sessionid=0d65a791cfe4618ee6233ffef578e25dc51e3f5a; Path=/; Domain=zzz.com; Max-Age=604800; HttpOnly; Secure; SameSite=None
,说明把cookie写入到zzz.com结尾的域名下,那么在访问以zzz.com结尾的域名时候,都会携带cookie,稍微有规模企业通常都有a.zzz.com,b.zzz.com,yy.b.zzz.com这样的域名,访问都会携带cookie。写cookie是spring-session的CookieSerializer接口,默认实现是DefaultCookieSerializer,我们可以实现CookieSerializer来改写cookie名称和domain。
cookie只有写在顶级域名时候,访问子域名的时候,才会携带顶级域名cookie,只能是xxx.com这样形式,比如下面就不行:
cookie写在luban.zzz.com域名下
访问a.luban.zzz.com是不会携带cookie
访问b.luban.zzz.com是不会携带cookie
session过期后如何处理
session过期,就是session在redis失效了,根据sessionid查询不到session了,那么在经FilterSecurityInterceptor处理时候就会抛出访问异常错误,从而被ExceptionTranslationFilter捕捉异常,重新生成session,接着重定向sso进行登录。和首次访问是一样的执行流程。
集成spring-session
内部使用spring-session框架,使用redis保存session,因此引入了spring-session-data-redis,这个包又引入了spring-session-core,这两个包无自动装配,需要通过@Enablexxx进行开启。其中spring-session-core有@EnableSpringHttpSession,用于开启spring-session功能,通过@Import(SpringHttpSessionConfiguration.class),配置类SpringHttpSessionConfiguration创建filter SessionRepositoryFilter。Spring Session 通过创建一个名为 springSessionRepositoryFilter 的 SessionRepositoryFilter 替换了 web server 默认的 HttpSession 实现。从而实现了 Session 管理与特定的 web server 的解藕以及 Session 存储方式的扩展。具体就是通过装饰模式,创建了HttpServletRequest HttpServletResponse对象SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper改写了默认的HttpSession实现。SpringHttpSessionConfiguration代码解释如下:
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();
private boolean usesSpringSessionRememberMeServices;
private ServletContext servletContext;
private CookieSerializer cookieSerializer;//cookie序列化,用于读取和写cookie
private HttpSessionIdResolver httpSessionIdResolver = this.defaultHttpSessionIdResolver;//session解析器
private List httpSessionListeners = new ArrayList<>();
@PostConstruct
public void init() {//类实例化和注入执行完成后执行此方法
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer //如果IOC容器有类型是CookieSerializer的bean,则使用自定义的CookieSerializer,内部框架里面是TitansCookieSerializer
: createDefaultCookieSerializer();//否则使用spring-session默认的cookie序列化对象DefaultCookieSerializer
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);//IOC容器有HttpSessionIdResolver类型bean则使用自定义,否则使用默认的session解析器CookieHttpSessionIdResolver
}
@Bean //创建SessionRepositoryFilter,改写HttpSession默认实现,从而实现session缓存到redis
public SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository sessionRepository) {//传入的SessionRepository是内部框架自定义的SessionRepositoryImpl
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
@Autowired(required = false)
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.cookieSerializer = cookieSerializer;
}
@Autowired(required = false)
public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
this.httpSessionIdResolver = httpSessionIdResolver;
}
@Autowired(required = false)//监听器可以用于监控
public void setHttpSessionListeners(List listeners) {
this.httpSessionListeners = listeners;
}
//省略其它
}
引入SpringHttpSessionConfiguration配置类需要开启spring-session-core的@EnableSpringHttpSession,或者spring-session-data-redis的@EnableRedisHttpSession,但是内部框架并没有这两个@Enable,而是通过com.zzz.web.session.SessionConfiguration extends SpringHttpSessionConfiguration来配置spring-session的。具体的redis存储session,也没用spring-session-data-redis,而是为了cat监控,自定义了org.springframework.session.SessionRepository
,因此在SpringHttpSessionConfiguration通过@Bean创建SessionRepositoryFilter时候,注入的SessionRepository是内部框架自定义的SessionRepositoryImpl。这样在SessionRepositoryFilter 通过SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper改写了默认的HttpSession实现,即可以通过内部框架实现的SessionRepositoryImpl将session保持到redis。
token是如何刷新的
目前使用并没有刷新token,虽然sso提供了刷新token。只是获取token和用户信息后缓存到redis,此后如果该session不过期(redis存在),则一直可以使用。这个当然不严谨,如果需要刷新token(判断redis内用户信息的token是过期则需要刷新token),重新请求sso获取用户信息,如果返回403,则需要进行token刷新,请求 GET https://ssourl/oauth2/token?appid=${appid}&refresh_token=${refresh_token}&grant_type=refresh_token
。当然对于原生的spring-security-oauth2来说,请求TokenEndpoint的/oauth/token,请求参数携带refresh_token。比如响应结果
请求会返回新的token对, 响应结构为:
{
access_token: "访问token",
refresh_token: "用做刷新的token",
openid: "当前登录用户在该应用下的唯一标志键",
scope: "userinfo",
expires_in: 7200
}
登出
用户退出默认使用spring-security,由org.springframework.security.web.authentication.logout.LogoutFilter进行处理。功能就是清除session,以及清除session后做一些功能(比如重定向到登录页面)
具体代码分析如下
//org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(ServletRequest, ServletResponse, FilterChain)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (requiresLogout(request, response)) {//请求是/logout
Authentication auth = SecurityContextHolder.getContext().getAuthentication();//获取session中认证用户
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
this.handler.logout(request, response, auth);//进行清除session。调用CompositeLogoutHandler.logout()
logoutSuccessHandler.onLogoutSuccess(request, response, auth);//用户退出后的动作,可以在WebSecurityConfigurerAdapter子类的configure(HttpSecurity)进行设置登出动作,
return;
}
chain.doFilter(request, response);//非/logout请求,直接由filterchain处理
}
清除session动作
//org.springframework.security.web.authentication.logout.CompositeLogoutHandler.logout(HttpServletRequest, HttpServletResponse, Authentication)
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
for (LogoutHandler handler : this.logoutHandlers) {//this.logoutHandlers集合[SecurityContextLogoutHandler]
handler.logout(request, response, authentication);
}
}
//org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler.logout(HttpServletRequest, HttpServletResponse, Authentication)
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();//清除session,即删除redis内的session。会去调用redis进行清除session
}
}
if (clearAuthentication) {
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);//清除当前线程的用户信息
}
SecurityContextHolder.clearContext();
}
比如设置退出后动作
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout()
.logoutUrl(/logout)
.invalidateHttpSession(true)
.logoutSuccessHandler(退出动作,比如重定向到登录页面);
}
}