1. 单点登录的流程是什么样的?
总体来说,一次单点登录过程包括了 3 次重定向过程。
第一次重定向,发现 Session 里未携带用户信息,拦截该未登录请求,重定向到 CAS Server;
第二次重定向,是登录成功重定向到初始访问接口(用户被拦截的接口),这步的重定向很关键,解决了单点登录的跨域问题;
第三次重定向,Session 携带用户信息去访问初始接口;
整个流程图如下:
思考以下几个问题以加深理解:
-
用户信息是哪个角色赋值在 Session 中的?
CAS Server 将用户信息封装在 body 里,服务方读取该 body 数据并填充到 Session 中;
-
服务方是怎么验证 Ticket 的?
第一次登录后,后面登录就不需要验证了,流程图如下。
访问其他系统也不需要认证了,流程图如下。
2. 从代码层面剖析单点登录流程
-
准备工作:注册拦截资源
@Component public class AuthConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { AiforceProperties aiforceProperties = SpringContextHolder.getApplicationContext().getBean(AiforceProperties.class); registry.addInterceptor(new AuthHandlerInterceptor()) .addPathPatterns("/**") .excludePathPatterns(getNoAuthUrls());// 拦截白名单 } }
-
从前端请求后端服务
首先从前端调用后端的
http://app.example.com/auth/user
接口,这时会被拦截器拦截,下面是拦截器的实现。@Component @Slf4j public class AuthHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (isLogin(request)) { return true; } // 未登录则需要跳转到CAS Server进行登录 request.getSession().setAttribute(Constants.RETURN_URL, "http://app.example.com/auth/user"); response.sendRedirect("http://cas.example.com/cas/login?service=http://app.example.com/callback");// 【1】 return false; } public boolean isLogin(HttpServletRequest request) { // 会话中用户信息为空则未登录 if (request.getSession().getAttribute(Constants.USER_SESSION) == null) { return false; } return true; } }
-
后端转发请求到单点登录服务器
注意:这里的请求转发
response.sendRedirect
跟上面的重定向可以理解为一个意思。在请求单点登录服务器前,需要把注册一个回调地址
http://app.example.com/callback
,比如步骤1中代码【1】处。用户提交表单后,单点登录服务器会跳转至
http://cas.example.com/cas/login?service=http://app.example.com/callback
中service
配置的值即http://app.example.com/callback
,并携带ticket
数据,可以放在http body
里面。 -
/callback
接口怎么实现?@RequestMapping(value = "/callback") public Result<String> callback(HttpServletRequest request, HttpServletResponse response) { // 请求单点登录服务器,校验ticket,返回用户信息 UserInfo userInfo = iAuthService.validateTicket(request); // 将用户信息放到session里 request.getSession().setAttribute(Constants.USER_SESSION, JSON.toJSONString(userInfo)); // 转发请求至步骤1初始请求的地址 response.sendRedirect("http://app.example.com/auth/user"); return Result.ok(); }
-
重新请求请求后端服务
这时再请求
http://app.example.com/auth/user
接口,步骤1中的isLogin()
就返回true
了。这样就完成了登录。
3. 跨域问题怎么解决?
比如现在有 2 个请求。
请求1:http://app1.example.com/auth/user
请求2:http://app2.example.com/auth/user
图片来源:https://blog.csdn.net/hezheqiang/article/details/82145125
可以通过在 SSO 域名下保存 Cookie 来解决跨域问题。
4. 如何实现会话超时?
在/callback
接口里添加逻辑,将用户信息缓存到redis
中。
@RequestMapping(value = "/callback")
public Result<String> callback(HttpServletRequest request, HttpServletResponse response) {
UserInfo userInfo = iAuthService.validateTicket(request);
request.getSession().setAttribute(Constants.USER_SESSION, JSON.toJSONString(userInfo));
// 新增的逻辑
stringRedisTemplate.opsForValue().set(getTokenKey(), JSON.toJSONString(userInfo), 120, TimeUnit.SECONDS);
response.sendRedirect("http://app.example.com/auth/user");
return Result.ok();
}
接着在isLogin()
方法里添加判断:redis
中的TokenKey
是否过期。
public boolean isLogin(HttpServletRequest request) {
if (request.getSession().getAttribute(Constants.USER_SESSION) == null) {
return false;
}
// 判断redis缓存中key是否失效
if (stringRedisTemplate.opsForValue().get(getTokenKey()) == null) {
return false;
}
return true;
}