一直有人问,为什么我实现的共享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. Java多线程5:Synchronized锁机制

    一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...

  2. 如何在MAC上运行exe程序

    1. 首先下载并运行CrossOver 运行CrossOver需要收费,试用期为14天,运行CrossOver 2. 选择exe应用程序,新建容器,安装exe程序 3.安装成功后,运行exe应用程序启 ...

  3. Delphi 工具条按钮上的下拉菜单

    制作步骤: 1.添加一个 TImageList: ImageList1, 然后载入些图标; 2.添加两个 TPopupMenu: PopupMenu1.PopupMenu2, 并分别添加些菜单项; 3 ...

  4. Map接口----Map中嵌套Map

    package cn.good.com; import java.util.HashMap; import java.util.Iterator; import java.util.Map; impo ...

  5. AgilePoint数据库模式中当前所有表的列表

    表名 描述 WF_ACTIVITY_INSTS 包含有关活动实例的信息. WF_ASSIGNED_OBJECTS 包含有关用户角色的分配对象的信息. WF_AUDIT_TRAILS 包含有关流程模板的 ...

  6. SOJ 1685:chopsticks(dp)

    题目链接 说实话挺喜欢soj的界面,简简单单,没有多余的东西hhh(但是简单到连内存限制,时间限制都看不到了. 题意是有个“奇葩”的主人公,吃饭要用三根筷子.两根短的一根长的. 现在给你n根筷子,要在 ...

  7. 一般服务器端口号的反斜杠表示访问webapp下的资源

  8. 进程间通信IPC与Binder机制原理

    1, Intent隐式意图携带数据 2, AIDL(Binder) 3, 广播BroadCast 4, 内容提供者ContentProvider 5,Messager(内部通过binder实现) 6, ...

  9. Python getattr() 函数

    Python getattr() 函数  Python 内置函数 描述 getattr() 函数用于返回一个对象属性值. 语法 getattr 语法: getattr(object, name[, d ...

  10. Maven使用(一)—— Maven的安装与全局配置

    一.Maven安装 Maven的安装步骤: 1.Maven官网(http://maven.apache.org/)下载压缩包,解压缩,当前最新版本是apache-maven-3.5.3-bin.zip ...