你真的知道什么是【共享Session】,什么是【单点登录】吗?
一直有人问,为什么我实现的共享session不能单点登录,今天我也抽时间准备好好说一下。
我要喷(别喷我)
首先,网上水货文章很多,CSDN居多。CSDN转载率很高,也就是说同相同文章有很多,换汤不换药的,贴上去不讲清楚的,直接复制粘贴连排版都懒得排的。有时候看到这样的文章我会觉得你只是个学生或者在公司实习的学生。只是匆忙做了个笔记而已。
比如:只贴出一个xml配置,一个java类的,就敢说自己实现了单点登录,说明文字没有,点进来无疑浪费了生命中的几分钟。https://blog.csdn.net/weixin_40750117/article/details/78684109
还有这个,想读懂需要与作者换位思考,作者确实逆天了,因为只有他自己能照这个教程搭建成功。https://my.oschina.net/u/1782542/blog/1925940

【 提供一个HTTP接口,让各个系统都放入到filter里面】由谁来提供?是独立的吗?系统A登陆完成,我要访问系统B,具体流程又是什么样?不要让读者猜你想说啥
还有这个,全文读下来,他想表达的只是一个系统的“单点”登录,我不知道那两个赞怎样拿到的。https://blog.csdn.net/luckyxl029/article/details/80625461

系统A按照他的逻辑走下来,之后另外一个系统B呢?他拿着token怎样去和 key为登录账号,value为token的数据做匹配?流程说的太简单了,只有自己能体会其中的奥妙。
等等类似的文章真的数不胜数
共享session
这个东西是在分布式集群环境下诞生的,我之前也解释过。最典型的情况就是负载均衡:
原来单体应用,部署简单,随着访问量增加,一台服务器爆炸了

好,那就做负载均衡吧

看起来没毛病,后来大家发现一个问题,用户登录成功,负载均衡到1上面,用户刷新了一下,结果负载均衡到2上面,而2上面没有用户登录的信息,要重新登录!用户可能要骂人了,什么狗逼玩意。后来想出一个方法,用户登录完成之后,在服务集群之间做session同步就好了,但是这种方式成本比较高。最后采用把session存储在redis上,统一管理,以实现“无状态”。

每次验证都去redis里面拿session信息,如果有就直接登陆。没有就要求用户去手动登录,然后把session信息同步到redis。
衍生问题【科普】
很多人也这样玩了,但是他是这样玩的

然后就问了,为啥!!为啥!!为啥!!!我系统A登陆了,切换另一个系统B又要登录!!!fuck you
好,你fuck我吧
我就问你,系统A生成的sessionId和系统B生成的sessionId能一样吗???你能拿着肯德基的会员卡去麦当劳享受优惠吗?
你这不是单点登录吗?
共享session不是单点登录好吧,应付的场景就不一样啊。单点登录能解决共享session的问题,但是共享session解决不了单点登录的问题。
网上有人实现了!Spring+Shiro+Redis
关于这部分文章我也看了,有的说的蛮有道理,有的说的天真无邪,但还有一个共同点:按照他的教程我无法实现。不过我也小小研究了一下,如果不用单独的认证中心,应该可以做到“简单”的单点登录,但是这个模型有限制,并且不知道有没有bug

清晰图:https://www.processon.com/view/link/59a4ee86e4b0afafe7a8213c
这个呢,是我在之前共享Session代码上加的,但是我已经屏蔽掉共享Session的影响,即没有Session的任何关系,无论你把Session放在哪里也不会影响,因为这个是基于token的实现。下面是关键代码:
SSOFilter
import com.alibaba.fastjson.JSONObject;
import com.example.app.common.Constant;
import com.example.app.common.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit; public class SSOFilter extends AccessControlFilter { @Autowired
private StringRedisTemplate stringRedisTemplate; @Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = null;
User user = null;
for(Cookie c : request.getCookies()){
if (Constant.SSO_TOKEN.equals(c.getName())){
token = c.getValue();
break;
}
}
Subject subject = SecurityUtils.getSubject();
boolean authenticated = subject.isAuthenticated();// 是否通过身份验证
if (token != null){
// 如果是登出操作,需要清除公共token信息
if(request.getRequestURI().equals("/logout")){
stringRedisTemplate.delete(Constant.TOKEN_PRE + token);
subject.logout();
return true;
}
String s = stringRedisTemplate.boundValueOps(Constant.TOKEN_PRE + token).get();
user = JSONObject.parseObject(s, User.class);// 根据token获取用户信息
if (user != null){
// 有用户信息并且没有身份认证
if(!authenticated){
// 手动通过,因为在其它系统已经登录
subject.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
}
}else{
// 没有用户信息,说明已经超时或者退出登录,需要清除当前的认证信息
if (authenticated){
subject.logout();
}
}
}
return true;
} @Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
return false;
}
}
大家可以看到,这个过滤器无论如何都会返回true,为什么呢,因为我只需要辅助判断用户是否已经登录就可以了,其它的流程按照正常走。
下面是shiro配置
@Bean(name = "ssoFilter")
public SSOFilter ssoFilter(){
return new SSOFilter();
}
/**
* 6. 配置ShiroFilter
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 静态资源
map.put("/css/**", "anon");
map.put("/js/**", "anon"); // 公共路径
map.put("/login", "anon");
map.put("/register", "anon");
//map.put("/*", "anon"); // 登出,项目中没有/logout路径,因为shiro是过滤器,而SpringMVC是Servlet,Shiro会先执行
// map.put("/logout", "logout"); // 授权
map.put("/user/**", "authc,roles[user]");
map.put("/admin/**", "authc,roles[admin]"); // everything else requires authentication:
map.put("/**", "ssoFilter,authc"); ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 配置SecurityManager
factoryBean.setSecurityManager(securityManager());
// 配置权限路径
factoryBean.setFilterChainDefinitionMap(map);
// 配置登录url
factoryBean.setLoginUrl("/");
// 配置无权限路径
factoryBean.setUnauthorizedUrl("/unauthorized");
return factoryBean;
} /**
* 解决:org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext
* or as a vm static singleton. This is an invalid application configuration.
* SSOFilter.isAccessAllowed(SSOFilter.java:44) ~[classes/:na]
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
公共的常量
public class Constant {
public static final String TOKEN_PRE = "loginToken:"; // token前缀
public static final String SSO_TOKEN = "SSO_TOKEN"; // token的cookie名称
}
登录代码
@RequestMapping("/login")
public BaseResponse<String> login(@RequestBody User user, HttpServletResponse httpServletResponse){
BaseResponse<String> response = new BaseResponse<>(0,"登陆成功");
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUsername(), user.getPassword());
subject.login(usernamePasswordToken);
response.setData("/home");
// 登陆成功之后,将token放入cookie
String token = UUID.randomUUID().toString();
Cookie cookie = new Cookie(Constant.SSO_TOKEN, token);
cookie.setPath("/");
cookie.setMaxAge(60*30);
httpServletResponse.addCookie(cookie);
// 放入redis
userService.addTokenInfo(token, new User(user.getUsername(), user.getPassword()));
return response;
}
限制:
1. 与token绑定的User需要是公共资源,这样才能被多系统共用,因为有序列化反序列化的过程。
2. 子系统最好是同一个域下,不能跨域
测试:分别打开两个系统

....

登录其中一个系统

然后直接访问另一个系统的权限资源

为了避免是共享session导致的,我已经关闭了共享session,看:

.....

sessionId不一样,也照样能完成单点登录操作。
我们再来看redis存储的token信息,当token超时清除后

刷新一下前台页面,立即返回登录界面

单点登录
关于单点登录的说明网上有很多,关键就在于独立的认证中心和身份标识token(sessionId不安全)
认证流程不由应用本身负责,而是统一去认证中心走流程,通过后会给你一张通行证token,巴拉巴拉巴拉~不想说了,以后再说
你真的知道什么是【共享Session】,什么是【单点登录】吗?的更多相关文章
- linux下实现redis共享session的tomcat集群
为了实现主域名与子域名的下不同的产品间一次登录,到处访问的效果,因此采用rediss实现tomcat的集群效果.基于redis能够异步讲缓存内容固化到磁盘上,从而当服务器意外重启后,仍然能够让sess ...
- 基于SpringBoot+Redis的Session共享与单点登录
title: 基于SpringBoot+Redis的Session共享与单点登录 date: 2019-07-23 02:55:52 categories: 架构 author: mrzhou tag ...
- 如何通过session控制单点登录
web服务器为每一个浏览器实例对应一个session.这个session有自己的一个独立id,这个id保存在浏览器的cookie中(这个cookie貌似随着这个浏览器实例的关闭而清除),访问web服务 ...
- 跨域共享cookie和跨域共享session
转载自:http://blog.csdn.net/ahhsxy/article/details/7356128 这里所说的跨域,是指跨二级域名,而且这些域名对应的应用都在同一个app上, 比如我有以下 ...
- nginx+iis+redis+Task.MainForm构建分布式架构 之 (redis存储分布式共享的session及共享session运作流程)
本次要分享的是利用windows+nginx+iis+redis+Task.MainForm组建分布式架构,上一篇分享文章制作是在windows上使用的nginx,一般正式发布的时候是在linux来配 ...
- 多Web服务器之间共享Session的解决方案
一.提出问题: 为了满足足够大的应用,满足更多的客户,于是我们架设了N台Web服务器(N>=2),在多台Web服务器的情况下,我们会涉及到一个问题:用户登陆一台服务器以后,如果在跨越到另一台服务 ...
- 数据库实现多站点共享Session
数据库实现多站点共享Session 多站点共享Session有很多方法,多站点共享Session常见的做法有: 使用.net自动的状态服务(Asp.net State Service); 使用.net ...
- [转]asp.net webform 与mvc 共享session
公司内部系统最早是用.net webform模式开发的,现新项目用.net mvc 开发,现存在的问题就是如何保持原有.net webform的登录状态不变,而在mvc中能够验证用户的登录状态,也就是 ...
- nginx 负载均衡、用数据库存储Session,来实现多站点共享Session[转]
多站点共享Session常见的作法有: 1.使用.net自动的状态服务(Asp.net State Service); 2.使用.net的Session数据库: 3.使用Memcached. 4.使用 ...
随机推荐
- 集合转数组的toArray()和toArray(T[] a)方法
参考:集合转数组的toArray()和toArray(T[] a)方法 1.ArrayList的toArray ArrayList提供了一个将List转为数组的一个非常方便的方法toArray.toA ...
- git放弃修改,强制覆盖本地代码
$ git fetch --all $ git reset --hard origin/master $ git pull
- python----re正则模块详解
今天介绍一下Python中常用的正则表达式处理函数.Python的正则表达式主要有两种方法完成模式匹配:『搜索』和『匹配』 re.match re.match 尝试从字符串的开始全部或者部分匹配某个模 ...
- Spring 使用介绍(十三)—— Bean的生命周期
一.概述 Spring Bean的完整生命周期从创建Spring容器开始,直到最终Spring容器销毁Bean,生命周期时序图如下: 二.生命周期接口分类 Bean的生命周期经历了多个接口方法的调用, ...
- Codeforces Round #467 Div. 1
B:显然即相当于能否找一条有长度为奇数的路径使得终点出度为0.如果没有环直接dp即可.有环的话可以考虑死了的spfa,由于每个点我们至多只需要让其入队两次,复杂度变成了优秀的O(kE).事实上就是拆点 ...
- 洛谷P2678跳石头题解
题目 这个题也是一个很经典的题了.其主要思想也是二分答案,原因就是题目中只要出现最大值最小或最小值最大,这种描述十有八九就是二分答案. 这个题原题也是让我们求最短的跳跃距离的最大值. 显而易见,最大值 ...
- Matplotlib学习---用seaborn画联合分布图(joint plot)
有时我们不仅需要查看单个变量的分布,同时也需要查看变量之间的联系,这时就需要用到联合分布图. 这里利用Jake Vanderplas所著的<Python数据科学手册>一书中的数据,学习画图 ...
- 手机Web 开发中图片img 如何等比例缩放
如果图片本身没有设置 width.height属性的话,只需要修改 max-width:100%; 就可以了 如果图片本身设置了 width.height属性的话,需要同时修改width 和heigh ...
- Google Apps的单点登录-谷歌使用的单点登录
简述: Customer :客户 Google:谷歌 Identity Provider:身份提供者安全断言标记语言(英语:Security Assertion Markup Language,简称S ...
- 洛谷SP22343 NORMA2 - Norma(分治,前缀和)
洛谷题目传送门 这题推式子恶心..... 考虑分治,每次统计跨过\(mid\)的所有区间的答案和.\(i\)从\(mid-1\)到\(l\)枚举,统计以\(i\)为左端点的所有区间. 我们先维护好\( ...