你真的知道什么是【共享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

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,巴拉巴拉巴拉~不想说了,以后再说

转载于:https://www.cnblogs.com/LUA123/p/10126881.html

原文地址:https://www.cnblogs.com/twodog/p/12135437.html