一直有人问,为什么我实现的共享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

GitHub传送门

这个呢,是我在之前共享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】,什么是【单点登录】吗?的更多相关文章

  1. linux下实现redis共享session的tomcat集群

    为了实现主域名与子域名的下不同的产品间一次登录,到处访问的效果,因此采用rediss实现tomcat的集群效果.基于redis能够异步讲缓存内容固化到磁盘上,从而当服务器意外重启后,仍然能够让sess ...

  2. 基于SpringBoot+Redis的Session共享与单点登录

    title: 基于SpringBoot+Redis的Session共享与单点登录 date: 2019-07-23 02:55:52 categories: 架构 author: mrzhou tag ...

  3. 如何通过session控制单点登录

    web服务器为每一个浏览器实例对应一个session.这个session有自己的一个独立id,这个id保存在浏览器的cookie中(这个cookie貌似随着这个浏览器实例的关闭而清除),访问web服务 ...

  4. 跨域共享cookie和跨域共享session

    转载自:http://blog.csdn.net/ahhsxy/article/details/7356128 这里所说的跨域,是指跨二级域名,而且这些域名对应的应用都在同一个app上, 比如我有以下 ...

  5. nginx+iis+redis+Task.MainForm构建分布式架构 之 (redis存储分布式共享的session及共享session运作流程)

    本次要分享的是利用windows+nginx+iis+redis+Task.MainForm组建分布式架构,上一篇分享文章制作是在windows上使用的nginx,一般正式发布的时候是在linux来配 ...

  6. 多Web服务器之间共享Session的解决方案

    一.提出问题: 为了满足足够大的应用,满足更多的客户,于是我们架设了N台Web服务器(N>=2),在多台Web服务器的情况下,我们会涉及到一个问题:用户登陆一台服务器以后,如果在跨越到另一台服务 ...

  7. 数据库实现多站点共享Session

    数据库实现多站点共享Session 多站点共享Session有很多方法,多站点共享Session常见的做法有: 使用.net自动的状态服务(Asp.net State Service); 使用.net ...

  8. [转]asp.net webform 与mvc 共享session

    公司内部系统最早是用.net webform模式开发的,现新项目用.net mvc 开发,现存在的问题就是如何保持原有.net webform的登录状态不变,而在mvc中能够验证用户的登录状态,也就是 ...

  9. nginx 负载均衡、用数据库存储Session,来实现多站点共享Session[转]

    多站点共享Session常见的作法有: 1.使用.net自动的状态服务(Asp.net State Service); 2.使用.net的Session数据库: 3.使用Memcached. 4.使用 ...

随机推荐

  1. 30行Python代码实现人脸检测

    参考OpenCV自带的例子,30行Python代码实现人脸检测,不得不说,Python这个语言的优势太明显了,几乎把所有复杂的细节都屏蔽了,虽然效率较差,不过在调用OpenCV的模块时,因为模块都是C ...

  2. 关于IWMS中遇到的问题及解决方法

    1.生成的文章上传到外网上,但是没一会儿又变成原来的样子? 解决方案:把上传页面对应的template中的.aspx页面也要上传到外网去.

  3. springboot swagger2 泛型踩坑记

    最近使用一些工具需要和swagger打通,swagger的格式总是不对. 后来查了一下:哈哈. https://blog.csdn.net/hupingjin/article/details/8299 ...

  4. SSH整合Maven教程

    http://www.cnblogs.com/xdp-gacl/p/4239501.html

  5. DotNetty 实现 Modbus TCP 系列 (一) 报文类

    本文已收录至:开源 DotNetty 实现的 Modbus TCP/IP 协议 Modbus TCP/IP 报文 报文最大长度为 260 byte (ADU = 7 byte MBAP Header ...

  6. size_t的使用

    size_t的取值range是目标平台下最大可能的数组尺寸 典型的例子:x64平台下size_t是8位,而x32平台下是4位: int在两个平台下均为4位 所以在使用的时候一定要配置好对应的平台,否则 ...

  7. WebAPI MVC Change Identity Default Table

    看过之前的文章小伙伴们应该已经明白了,当我们新建一个带有身份验证的模板时,会自带Identity Server,并且它的表名和字段名也都是默认的. 那么该如何修改它,并让EF知道呢?不废话,直接上代码 ...

  8. c++ 动态生成string类型的数组

    定义一个字符串指针,将其初始化为空 char *a=NULL 然后输入输出 cin>>a cout<<a 编译无误,但执行会遇见错误 当为*a动态分配存储空间时,程序执行正常 ...

  9. luoguP4035

    P4035 [JSOI2008]球形空间产生器 Description 有一个球形空间产生器能够在n维空间中产生一个坚硬的球体.现在,你被困在了这个n维球体中,你只知道球面上n+1个点的坐标,你需要以 ...

  10. DRF 分页组件

    Django Rest Framework 分页组件 DRF的分页 为什么要使用分页 其实这个不说大家都知道,大家写项目的时候也是一定会用的, 我们数据库有几千万条数据,这些数据需要展示,我们不可能直 ...