文章目录
1. 无状态服务与有状态服务
无状态:每次请求毫无关联。
有状态:比如cookie、session,可作为类似http等无状态服务的传输介质。
2. 单点登录时序图
分三个部分:
用户、系统、认证中心。
以上时序图解释:
- 用户访问系统1,未登录,转发请求到认证中心验证;
- 认证中心验证该用户未登录,引导用户访问未登录页面;
- 用户输入账号密码,携带地址1地址参数发给认证中心;
- 认证中心验证通过后创建与用户的全局会话,并创建授权令牌发送给系统1;
- 系统1返回授权令牌给认证中心作二次验证;
- 认证中心验证通过,注册系统1,返回有效;
- 系统1创建与用户的局部会话;
- 用户访问系统2,未登录,转发请求到认证中心;
- 认证中心通过全局会话发现用户已登录,返回授权令牌给系统2;
- 系统2携带自己的地址参数并返回授权令牌;
- 认证中心校验授权令牌通过,注册系统2,返回有效;
- 系统2创建与用户的局部会话。
3. 单点注销
- 用户向系统1发起注销请求,系统1通过会话ID取出令牌,发给认证中心;
- 认证中心校验令牌通过,销毁与拥护的全局会话,并向使用该授权令牌注册的系统发送注销请求;
- 系统收到注销请求,销毁与用户的局部会话。
会话间的区别 - 全局会话存在,局部会话不一定存在;
- 局部会话存在,全局会话一定存在;
- 局部不存在,全局会话不一定存在;
- 全局会话不存在,局部会话一定不存在。
4. 系统设计
系统1、系统2、认证中心。
系统:
- 拦截登录请求,重定向到认证中心验证;
- 创建与用户的局部会话。
认证中心: - 负责登录操作以及授权码的校验
- map存储系统地址,模仿系统的注册,方便统一发送注销请求;
- 创建与用户的全局会话;
- 统一注销。
核心请求拦截器的实现:
package com.example.sso.client.interceptor;
import com.example.sso.client.utils.CookieUtil;
import com.example.sso.client.utils.SSOClientHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 创建拦截器-拦截需要安全访问的请求
* 方法说明
* 1.preHandle():前置处理回调方法,返回true继续执行,返回false中断流程,不会继续调用其它拦截器
* 2.postHandle():后置处理回调方法,但在渲染视图之前
* 3.afterCompletion():全部后置处理之后,整个请求处理完毕后回调。
*
* @author yhw
*/
@Slf4j
public class WebInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
log.info("[ WebInterceptor ] >> preHandle requestUrl:{} ", request.getRequestURI());
//判断是否有局部会话
HttpSession session = request.getSession();
Object isLogin = session.getAttribute("isLogin");
if (isLogin != null && (Boolean) isLogin) {
log.debug("[ WebInterceptor ] >> 已登录,有局部会话 requestUrl:{}", request.getRequestURI());
return true;
}
//获取令牌ssoToken
String token = SSOClientHelper.getSsoToken(request);
//无令牌
if (StringUtils.isEmpty(token)) {
//认证中心验证是否已经登录(是否存在全局会话)
SSOClientHelper.checkLogin(request, response);
return true;
}
//有令牌-则请求认证中心校验令牌是否有效
Boolean checkToken = SSOClientHelper.checkToken(token, session.getId());
//令牌无效
if (!checkToken) {
log.debug("[ WebInterceptor ] >> 令牌无效,将跳转认证中心进行认证 requestUrl:{}, token:{}", request.getRequestURI(), token);
//认证中心验证是否已经登录(是否存在全局会话)
SSOClientHelper.checkLogin(request, response);
return true;
}
//token有效,创建局部会话设置登录状态,并放行
session.setAttribute("isLogin", true);
//设置session失效时间-单位秒
session.setMaxInactiveInterval(1800);
//设置本域cookie
CookieUtil.setCookie(response, SSOClientHelper.SSOProperty.TOKEN_NAME, token, 1800);
log.debug("[ WebInterceptor ] >> 令牌有效,创建局部会话成功 requestUrl:{}, token:{}", request.getRequestURI(), token);
return true;
}
}
校验令牌接口
/**
* 校验令牌是否合法
*
* @param ssoToken 令牌
* @param loginOutUrl 退出登录访问地址
* @param jsessionid
* @return 令牌是否有效
*/
@ResponseBody
@RequestMapping("/checkToken")
public String verify(String ssoToken, String loginOutUrl, String jsessionid) {
// 判断token是否存在map容器中,如果存在则代表合法
boolean isVerify = SSOConstantPool.TOKEN_POOL.contains(ssoToken);
if (!isVerify) {
log.info("[ SSO-令牌校验 ] checkToken 令牌已失效 ssoToken:{}", ssoToken);
return "false";
}
//把客户端的登出地址记录起来,后面注销的时候需要根据使用(生产环境建议存库或者redis)
List<ClientRegisterModel> clientInfoList =
SSOConstantPool.CLIENT_REGISTER_POOL.computeIfAbsent(ssoToken, k -> new ArrayList<>());
ClientRegisterModel vo = new ClientRegisterModel();
vo.setLoginOutUrl(loginOutUrl);
vo.setJsessionid(jsessionid);
clientInfoList.add(vo);
log.info("[ SSO-令牌校验 ] checkToken success ssoToken:{} , clientInfoList:{}", ssoToken, clientInfoList);
return "true";
}
5. 效果截图
单点登录
单点注销:
6.单点登录实现的三种方式
session广播(session复制)
适合在服务模块较少的情况。如果在服务过多的情况下,显得过于冗余。
cookie+redis实现
-
在项目任何一个模块进行登录,登录之后,把数据放到两个地方
(1)redis:在key:生成随机唯一值(ip、用户id等),在value:存用户数据
(2)cookie:把redis里面生成的key值放到cookie。 -
访问其他模块时,请求带着cookie进行发送
各个模块从cookie中获取key,到redis中查找值,查到证明已登录
jwt验证机制(使用token实现)
在某个模块登录后,会把用户信息以某种加密形式放到字符串中,并返回。
(1)放到cookie中
(2)拼接到地址栏后面
每次发送请求都带上这个字符串,其他模块从这个字符串中提取到用户数据,注意不能存储敏感信息,比如用户密码。