最近开发新产品,然后老板说我们现在系统太多了,每次切换系统登录太麻烦了,能不能做个优化,同一账号互通掉。作为一个资深架构狮,老板的要求肯定要满足,安排!
一个公司产品矩阵比较丰富的时候,用户在不同系统之间来回切换,固然对产品用户体验上较差,并且增加用户密码管理成本。也没有很好地利用内部流量进行用户打通,并且每个产品的独立体系会导致产品安全度下降。
因此实现集团产品的单点登录对用户使用体验以及效率提升有很大的帮助。那么如何实现统一认证呢?我们先了解一下传统的身份验证方式。
图片
众所周知,http是无状态的协议,因此客户每次通过浏览器访问web页面,请求到服务端时,服务器都会新建线程,打开新的会话,而且服务器也不会自动维护客户的上下文信息。比如我们现在要实现一个电商内的购物车功能,要怎么才能知道哪些购物车请求对应的是来自同一个客户的请求呢?
图片
因为 http 请求是无状态请求,所以在 Web 领域,大部分都是通过这种方式解决。但是这么做有什么问题呢?我们接着看
图片
随着技术的发展,用户流量增大,单个服务器已经不能满足系统的需要了,分布式架构开始流行。通常都会把系统部署在多台服务器上,通过负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上,因为 session 是保存在服务器上的,那么很有可能第一次请求访问的 A 服务器,创建了 session,但是第二次访问到了 B 服务器,这时就会出现取不到 session 的情况。
我们知道,Session 一般是用来存会话全局的用户信息(不仅仅是登陆方面的问题),用来简化/加速后续的业务请求。
传统的 session 由服务器端生成并存储,当应用进行分布式集群部署的时候,如何保证不同服务器上 session 信息能够共享呢?
Session共享一般有两种思路
session复制即将不同服务器上 session 数据进行复制,用户登录,修改,注销时,将session信息同时也复制到其他机器上面去
图片
这种实现的问题就是实现成本高,维护难度大,并且会存在延迟登问题。
图片
集中存储就是将获取session单独放在一个服务中进行存储,所有获取session的统一来这个服务中去取。这样就避免了同步和维护多套session的问题。一般我们都是使用redis进行集中式存储session。
图片
如果企业做大了之后,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务,按照传统的验证方式访问多系统,每个单独的系统都会有自己的安全体系和身份认证系统。进入每个系统都需要进行登录,获取session,再通过session访问对应系统资源。
这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好,那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢?
图片
“单点登录”就是专为解决此类问题的。其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息
我们知道对于完全不同域名的系统,cookie 是无法跨域名共享的,因此 sessionId 在页面端也无法共享,因此需要实现单店登录,就需要启用一个专门用来登录的域名如(ouath.com)来提供所有系统的sessionId。当业务系统被打开时,借助中心授权系统进行登录,整体流程如下:
整个交互流程图如下:
图片
3.2.2.1 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;}}
@Controllerpublic 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 {}@Overridepublic 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)//不是登陆页面 && !requestUrl.startsWith("/login")//不是去登陆 && null == userInfo) {//不是登陆状态 request.getRequestDispatcher("/toLogin").forward(request,response); return ; } filterChain.doFilter(request,servletResponse);}@Overridepublic void destroy() {}}
@Configurationpublic class LoginConfig {//配置filter生效@Beanpublic 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>
3.2.2.2 web系统demo核心代码
public class SSOFilter implements Filter { private RedisTemplate redisTemplate;public static final String USER_INFO = "user";public SSOFilter(RedisTemplate redisTemplate){ this.redisTemplate = redisTemplate;}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic 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)//不是登陆页面 && !requestUrl.startsWith("/login")//不是去登陆 && 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);}@Overridepublic void destroy() {}}
@Controllerpublic 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>
OAuth2: 三方授权协议,允许用户在不提供账号密码的情况下,通过信任的应用进行授权,使其客户端可以访问权限范围内的资源。
CAS: 中央认证服务(Central Authentication Service),一个基于Kerberos票据方式实现SSO单点登录的框架,为Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。
因此,需要统一的账号密码进行身份认证,用CAS;需要授权第三方服务使用我方资源,使用OAuth2;
本文链接:http://www.28at.com/showinfo-26-112778-0.html告别繁琐操作,实现一次登录产品互通
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 定时任务数量爆炸?Netty教你如何应对百万级挑战
下一篇: PHP异步非阻塞MySQL客户端连接池