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,后台校验登陆状态,成功
整个交互流程图如下:
②单点登录流程演示
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)//不是登陆页面 && !requestUrl.startsWith("/login")//不是去登陆 && 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)//不是登陆页面 && !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); } @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 是否有了更深刻的理解,欢迎留言。