单点登录(Single Sign On), 简称为SSO, 是目前比较流行的企业业务整合的解决方案之一.
SSO的定义:在多个应用系统中, 用户只需要登录一次就可以访问所有相互信任的应用系统, 企业间需要相互授信
众所皆知, HTTP是无状态的协议, 这意味着服务器无法确认用户的信息。 于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。
如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。
一、登录
用户登录成功后, 通过request获取Session(本质是根据Cookie中携带的JSESSIONID寻找Session)
二、记住我(关闭掉浏览器后,重新打开浏览器还能保持登录状态)
因为默认返回的JSESSIONID是会话级别的, 我们可以手动为Cookie中添加JSESSIONID信息,设置Cookie的过期时间, 此时不管你的浏览器是否关闭,Cookie中都会携带JSESSION信息
//登录成功后,手动添加cookie,保存JSESSIONID信息
Cookie cookie = new Cookie("JSESSIONID", session.getId());
//300年后过期(永久有效)
cookie.setMaxAge(60 * 60 * 24 * 30 * 12 * 300); //设置cookie 和 session生命周期同步.
response.addCookie(cookie);
三、注销(退出登录):从Session中删除用户的信息
session.removeAttribute("user");
多系统、单一位置登录, 实现多系统同时登录的一种技术
所谓的Session跨域就是摒弃了系统提供的Session. 而使用自定义的类似Session的机制来保存客户端数据的一种方案
如:通过设置cookie的domain来实现cookie的跨域传递。在cookie中传递一个自定义的session_id。这个session_id是客户端的唯一标记。将这个标记作为key,将客户端需要保存的数据作为value,在服务端进行保存(数据库保存或NoSQL保存)。这种机制就是Session的跨域解决。
一、什么是跨域
客户端请求的时候, 请求的服务器, 不是同一个ip、端口、域名、主机名的时候, 都称为跨域
二、什么是域
在应用模型中, 一个完整的有独立访问路径的功能集合称为一个域
三、Session跨域可以解决的两个问题
四、Session跨域的实现流程
代码实现:
@Controller
@RequestMapping("sso")
public class UserController {
@Autowired
private RedisTemplate redisTemplate;
private static final String COOKIE_NAME = "sso_session_id";
@GetMapping("login")
@ResponseBody
public String login(@RequestParam("un") String userName, @RequestParam("pw") String password,
HttpServletRequest request, HttpServletResponse response) {
String token = getToken(userName, password);
if (StringUtils.isNotEmpty(token)) {
// 登录成功, 将token存入Cookie
setCookie(request, response, COOKIE_NAME, token);
return "success";
}
// 没有token, 代表用户名或密码错误
return "error";
}
// 获取token(类似于JSESSIONID)
private String getToken(String userName, String password) {
if (userName.equals("wxf") && password.equals("123456")) {
// 登录成功
// 步骤一:生成token(此处的token的作用类似于JSESSIONID)
String token = UUID.randomUUID().toString();
// 步骤二:将用户信息保存到redis中
User user = new User(userName, password);
redisTemplate.opsForValue().set(token, JSON.toJSONString(user), 1800);
return token;
}
// 登录失败
return null;
}
/**
* 设置cookie
*/
private void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) {
// 创建cookie
Cookie cookie = new Cookie(cookieName, cookieValue);
if (null != request) {
// 分析解析域名
String domainName = getDomainName(request);
// 设置域名的cookie
cookie.setDomain(domainName);
}
// 从根路径下的后面任意路径地址,cookie都有效
cookie.setPath("/");
// response响应写入到客户端即可
response.addCookie(cookie);
}
/**
* 根据url来设置domain
*/
private static final String getDomainName(HttpServletRequest request) {
// 定义一个变量domainName
String domainName = null;
// 获取完整的请求URL地址。请求url,转换为字符串类型
String serverName = request.getRequestURL().toString();
// 判断如果请求url地址为空或者为null
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 不为空或者不为null,将域名转换为小写。域名不敏感的。大小写一样
serverName = serverName.toLowerCase();
// 判断开始如果以http://开头的
if (serverName.startsWith("http://")) {
// 截取前七位字符
serverName = serverName.substring(7);
} else if (serverName.startsWith("https://")) {
// 否则如果开始以https://开始的。//截取前八位字符
serverName = serverName.substring(8);
}
// 找到/开始的位置,可以判断end的值是否为-1,如果为-1的话说明没有这个值
// 如果存在这个值,找到这个值的位置,否则返回值为-1
final int end = serverName.indexOf("/");
// .test.com www.test.com.cn/sso.test.com.cn/.test.com.cn spring.io/xxxx/xxx
// 然后截取到0开始到/的位置
if (end != -1) {
// end等于-1。说明没有/。否则end不等于-1说明有这个值
serverName = serverName.substring(0, end);
// 这是将\\.是转义为.。然后进行分割操作。
final String[] domains = serverName.split("\\.");
// 获取到长度
int len = domains.length;
// 注意,如果是两截域名,一般没有二级域名。比如spring.io/xxx/xxx,都是在spring.io后面/拼接的
// 如果是三截域名,都是以第一截为核心分割的。
// 如果是两截的就保留。三截以及多截的就
// 多截进行匹配域名规则,第一个相当于写*,然后加.拼接后面的域名地址
if (len > 3) {
// 如果是大于等于3截的,去掉第一个点之前的。留下剩下的域名
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// 如果是2截和3截保留
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
}
// 如果域名不为空并且有这个:
if (domainName != null && domainName.indexOf(":") > 0) {
// 将:转义。去掉端口port号,cookie(cookie的domainName)和端口无关。只看域名的。
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
// 返回域名
System.out.println("==============================================" + domainName);
return domainName;
}
}
Spring-Session技术是Spring提供的用于处理集群会话共享的解决方案. Spring-Session技术是将用户Session数据保存到第三方存储容器中, 例如Mysql、redis等
Spring-Session技术是解决同域名下多服务器集群session共享问题的, 不能解决跨域名Session共享问题
利用spring session解决共享Session问题
SpringBoot+SpringSession
HTTP是一种没有状态的协议,也就是它并不知道是谁在访问应用,这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时,还得再校验一次。这明显是不合适的,如果客户每次访问应用都需要登录验证、登录验证…客户体验会非常差
解决方式:当客户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的ID编号发给客户端,客户端收到以后将ID编号存储在Cookie中,下次这个用户再向服务端发送请求的时候,可以携带这个Cookie,这样服务端就会验证这个Cookie里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
上面说的就是HTTP Session,我们需要在服务端存储为登录的用户生成的Session,这些Session可能会存储在内存、磁盘、或者数据库里。我们可能需要在服务端定期的去清理过期的Session
这种认证出现的问题是:
使用基于Token的身份认证方式, 在服务端不需要存储用户的登录记录, 大概的流程是这样的:
使用Token的优势:
JWT(JSON Web Token)
JWT是一种紧凑
、自包含
的, 用于在多方传递JSON对象的技术. 传递的数据可以使用数字签名增加其安全性. 可以使用HMAC加密算法或RSA公钥/私钥加密方式
JWT是一般用于处理用户身份验证
、数据信息交换
JWT的数据结构是:A.B.C
由字符点"."来分隔三部分数据
一、header
数据结构:{“alg”:“加密算法名称”, “typ”: “JWT”}
alg是加密算法定义内容, 例如HMAC、SHA256、RSA
typ是token类型, 这里固定为JWT
二、payload
在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的, 主要分为三个部分. 分别是已注册信息(registered claims).、公开数据(public claims)、私有数据()