公司系统太多,能不能实现账号互通?

  • 背景

  • 传统 Session 机制及身份认证方案

    • Cookie 与服务器的交互

    • 服务器端的 session 的机制

    • 基于 session 的身份认证流程

  • 集群环境下的 Session 困境及解决方案

    • Session 共享方案

  • 多服务下的登陆困境及 SSO 方案

    • SSO 的产生背景

    • SSO 的底层原理 CAS



e0d2aa2e439696498b6eec73174e3a84.png

背景

69b43d7b69a3ba86e8121ad964f3a1f3.png

最近开发新产品,然后老板说我们现在系统太多了,每次切换系统登录太麻烦了,能不能做个优化,同一账号互通掉。作为一个资深架构狮,老板的要求肯定要满足,安排!

一个公司产品矩阵比较丰富的时候,用户在不同系统之间来回切换,固然对产品用户体验上较差,并且增加用户密码管理成本。

也没有很好地利用内部流量进行用户打通,并且每个产品的独立体系会导致产品安全度下降。

因此实现集团产品的单点登录对用户使用体验以及效率提升有很大的帮助。那么如何实现统一认证呢?我们先了解一下传统的身份验证方式。


92662364f3d58a2ab407c096e9945760.png

传统 Session 机制及身份认证方案

578bb3aeb2d3df11d5384b8fd1026d20.png

Cookie 与服务器的交互

8e7fb9911cba17bf91a2632a217cf4ce.jpeg

图片

众所周知,http 是无状态的协议,因此客户每次通过浏览器访问 web。

页面,请求到服务端时,服务器都会新建线程,打开新的会话,而且服务器也不会自动维护客户的上下文信息。

比如我们现在要实现一个电商内的购物车功能,要怎么才能知道哪些购物车请求对应的是来自同一个客户的请求呢?

2832ffe249bbda6aa221de136734b45a.jpeg

图片

因此出现了 session 这个概念,session 就是一种保存上下文信息的机制,他是面向用户的,每一个 SessionID 对应着一个用户,并且保存在服务端中。

session 主要以 cookie 或 URL 重写为基础的来实现的,默认使用 cookie 来实现,系统会创造一个名为 JSESSIONID 的变量输出到 cookie 中。

JSESSIONID 是存储于浏览器内存中的,并不是写到硬盘上的,如果我们把浏览器的cookie 禁止,则 web 服务器会采用 URL 重写的方式传递 Sessionid,我们就可以在地址栏看到 sessionid=KWJHUG6JJM65HS2K6 之类的字符串。

通常 JSESSIONID 是不能跨窗口使用的,当你新开了一个浏览器窗口进入相同页面时,系统会赋予你一个新的 sessionid,这样我们信息共享的目的就达不到了。

服务器端的 session 的机制

当服务端收到客户端的请求时候,首先判断请求里是否包含了 JSESSIONID 的 sessionId,如果存在说明已经创建过了,直接从内存中拿出来使用,如果查询不到,说明是无效的。

如果客户请求不包含 sessionid,则为此客户创建一个 session 并且生成一个与此 session 相关联的 sessionid,这个 sessionid 将在本次响应中返回给客户端保存。

对每次 http 请求,都经历以下步骤处理:

  • 服务端首先查找对应的 cookie 的值(sessionid)。

  • 根据 sessionid,从服务器端 session 存储中获取对应 id 的 session 数据,进行返回。

  • 如果找不到 sessionid,服务器端就创建 session,生成 sessionid 对应的 cookie,写入到响应头中。

session 是由服务端生成的,并且以散列表的形式保存在内存中。

基于 session 的身份认证流程

基于 seesion 的身份认证主要流程如下:

451acc40f17de5134ac897e6472dbd95.jpeg

图片

因为 http 请求是无状态请求,所以在 Web 领域,大部分都是通过这种方式解决。但是这么做有什么问题呢?我们接着看。


bcf68f39ad739d707e6dfe2acd08be6d.png

集群环境下的 Session 困境及解决方案

82ab3da2c2828a8008f8f9906ecafce0.png

随着技术的发展,用户流量增大,单个服务器已经不能满足系统的需要了,分布式架构开始流行。

1fc344f1d6f58d012b4a089f230d61ab.jpeg

图片

通常都会把系统部署在多台服务器上,通过负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上。

因为 session 是保存在服务器上的,那么很有可能第一次请求访问的 A 服务器,创建了 session,但是第二次访问到了 B 服务器,这时就会出现取不到 session 的情况。

我们知道,Session 一般是用来存会话全局的用户信息(不仅仅是登陆方面的问题),用来简化/加速后续的业务请求。

传统的 session 由服务器端生成并存储,当应用进行分布式集群部署的时候,如何保证不同服务器上 session 信息能够共享呢?

Session 共享方案

Session 共享一般有两种思路:

  • session 复制

  • session 集中存储

①session 复制

session 复制即将不同服务器上 session 数据进行复制,用户登录,修改,注销时,将 session 信息同时也复制到其他机器上面去。

e58cff24bfa2b696326e0e6fa0a3480f.png

图片

这种实现的问题就是实现成本高,维护难度大,并且会存在延迟登问题。

②session 集中存储

168351d8bfc54a346e90f527f7657603.png

图片

集中存储就是将获取 session 单独放在一个服务中进行存储,所有获取 session 的统一来这个服务中去取。

这样就避免了同步和维护多套 session 的问题。一般我们都是使用 redis 进行集中式存储 session。


a4e918745271679e0b981cacac89d792.png

多服务下的登陆困境及 SSO 方案

337a6ec5ec2f4ab587c9a759593a6884.png

SSO 的产生背景

9b5bbb92a5a7b77c06367d4b5a5771c0.png

图片

如果企业做大了之后,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务,按照传统的验证方式访问多系统,每个单独的系统都会有自己的安全体系和身份认证系统。

进入每个系统都需要进行登录,获取 session,再通过 session 访问对应系统资源。

这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好,那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢?

90b550b8613964b2ec1a957c83bb6eb6.png

图片

“单点登录”就是专为解决此类问题的。其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息。

SSO 的底层原理 CAS

①CAS 实现单点登录流程

我们知道对于完全不同域名的系统,cookie 是无法跨域名共享的,因此 sessionId 在页面端也无法共享,因此需要实现单店登录,就需要启用一个专门用来登录的域名如(ouath.com)来提供所有系统的 sessionId。

当业务系统被打开时,借助中心授权系统进行登录,整体流程如下:

  • 当 b.com 打开时,发现自己未登陆,于是跳转到 ouath.com 去登陆

  • ouath.com 登陆页面被打开,用户输入帐户/密码登陆成功

  • ouath.com 登陆成功,种 cookie 到 ouath.com 域名下

  • 把 sessionid 放入后台 redis,存放<ticket,sesssionid>数据结构,然后页面重定向到 A 系统

  • 当 b.com 重新被打开,发现仍然是未登陆,但是有了一个 ticket 值

  • 当 b.com 用 ticket 值,到 redis 里查到 sessionid,并做 session 同步,然后种 cookie 给自己,页面原地重定向

  • 当 b.com 打开自己页面,此时有了 cookie,后台校验登陆状态,成功

整个交互流程图如下:

5ab693e8e1e08fb39c5551154f32c033.png

图片
②单点登录流程演示
CAS 登录服务 demo 核心代码如下:

用户实体类:

public class UserForm implements Serializable{
private static final long serialVersionUID = 1L;

private String username;
private String password;
private String backurl;

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

public String getBackurl() {
    return backurl;
}

public void setBackurl(String backurl) {
    this.backurl = backurl;
}

}

登录控制器:

@Controller
public class IndexController {
    @Autowired
    private RedisTemplate redisTemplate;

@GetMapping("/toLogin")
public String toLogin(Model model,HttpServletRequest request) {
    Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);
    //不为空,则是已登陆状态
    if (null != userInfo){
        String ticket = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(ticket,userInfo,2, TimeUnit.SECONDS);
        return "redirect:"+request.getParameter("url")+"?ticket="+ticket;
    }
    UserForm user = new UserForm();
    user.setUsername("laowang");
    user.setPassword("laowang");
    user.setBackurl(request.getParameter("url"));
    model.addAttribute("user", user);

    return "login";
}

@PostMapping("/login")
public void login(@ModelAttribute UserForm user,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
    System.out.println("backurl:"+user.getBackurl());
    request.getSession().setAttribute(LoginFilter.USER_INFO,user);

    //登陆成功,创建用户信息票据
    String ticket = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set(ticket,user,20, TimeUnit.SECONDS);
    //重定向,回原url  ---a.com
    if (null == user.getBackurl() || user.getBackurl().length()==0){
        response.sendRedirect("/index");
    } else {
        response.sendRedirect(user.getBackurl()+"?ticket="+ticket);
    }
}

@GetMapping("/index")
public ModelAndView index(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView();
    Object user = request.getSession().getAttribute(LoginFilter.USER_INFO);
    UserForm userInfo = (UserForm) user;
    modelAndView.setViewName("index");
    modelAndView.addObject("user", userInfo);
    request.getSession().setAttribute("test","123");
    return modelAndView;
}
}

登录过滤器:

public class LoginFilter implements Filter {
    public static final String USER_INFO = "user";
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
                     ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) servletRequest;
     HttpServletResponse response = (HttpServletResponse)servletResponse;

    Object userInfo = request.getSession().getAttribute(USER_INFO);;

    //如果未登陆,则拒绝请求,转向登陆页面
    String requestUrl = request.getServletPath();
    if (!"/toLogin".equals(requestUrl)//不是登陆页面
            &amp;&amp; !requestUrl.startsWith("/login")//不是去登陆
            &amp;&amp; null == userInfo) {//不是登陆状态

        request.getRequestDispatcher("/toLogin").forward(request,response);
        return ;
    }

    filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}
}

配置过滤器:

@Configuration
public class LoginConfig {

//配置filter生效
@Bean
public FilterRegistrationBean sessionFilterRegistration() {

    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new LoginFilter());
    registration.addUrlPatterns("/*");
    registration.addInitParameter("paramName", "paramValue");
    registration.setName("sessionFilter");
    registration.setOrder(1);
    return registration;
}
}

登录页面:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>enjoy login</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div text-align="center">
    <h1>请登陆</h1>
    <form action="#" th:action="@{/login}" th:object="${user}" method="post">
        <p>用户名: <input type="text" th:field="*{username}" /></p>
        <p>密  码: <input type="text" th:field="*{password}" /></p>
        <p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
        <input type="text" th:field="*{backurl}" hidden="hidden" />
    </form>
</div>


</body>
</html>
web 系统 demo 核心代码如下:

过滤器:

public class SSOFilter implements Filter {
    private RedisTemplate redisTemplate;

public static final String USER_INFO = "user";

public SSOFilter(RedisTemplate redisTemplate){
    this.redisTemplate = redisTemplate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
                     ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {

    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse)servletResponse;

    Object userInfo = request.getSession().getAttribute(USER_INFO);;

    //如果未登陆,则拒绝请求,转向登陆页面
    String requestUrl = request.getServletPath();
    if (!"/toLogin".equals(requestUrl)//不是登陆页面
            &amp;&amp; !requestUrl.startsWith("/login")//不是去登陆
            &amp;&amp; null == userInfo) {//不是登陆状态

        String ticket = request.getParameter("ticket");
        //有票据,则使用票据去尝试拿取用户信息
        if (null != ticket){
            userInfo = redisTemplate.opsForValue().get(ticket);
        }
        //无法得到用户信息,则去登陆页面
        if (null == userInfo){
            response.sendRedirect("http://127.0.0.1:8080/toLogin?url="+request.getRequestURL().toString());
            return ;
        }

        /**
         * 将用户信息,加载进session中
         */
        UserForm user = (UserForm) userInfo;
        request.getSession().setAttribute(SSOFilter.USER_INFO,user);
        redisTemplate.delete(ticket);
    }

    filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}
}

控制器:

@Controller
public class IndexController {
    @Autowired
    private RedisTemplate redisTemplate;

@GetMapping("/index")
public ModelAndView index(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView();
    Object userInfo = request.getSession().getAttribute(SSOFilter.USER_INFO);
    UserForm user = (UserForm) userInfo;
    modelAndView.setViewName("index");
    modelAndView.addObject("user", user);

    request.getSession().setAttribute("test","123");
    return modelAndView;
}
}

首页:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>enjoy index</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${user}">
    <h1>cas-website:欢迎你"></h1>
</div>
</body>
</html>
③CAS 的单点登录和 OAuth2 的区别

OAuth2: 三方授权协议,允许用户在不提供账号密码的情况下,通过信任的应用进行授权,使其客户端可以访问权限范围内的资源。

CAS: 中央认证服务(Central Authentication Service),一个基于 Kerberos 票据方式实现 SSO 单点登录的框架,为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。

CAS 的单点登录时保障客户端的用户资源的安全 ;OAuth2 则是保障服务端的用户资源的安全 。

CAS 客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS 客户端)的资源;OAuth2 获取的最终信息是,我(oauth2 服务提供方)的用户的资源到底能不能让你(oauth2 的客户端)访问。

因此,需要统一的账号密码进行身份认证,用 CAS;需要授权第三方服务使用我方资源,使用 OAuth2。

好了,不知道大家对 SSO 是否有了更深刻的理解,欢迎留言。

如喜欢本文,请点击右上角,把文章分享到朋友圈

作者:王老狮

来源:juejin.cn/post/7123787027652280356

版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

架构师指南

我们都是架构师!

b07a138d354064c5f0ae1e3545939b8b.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值