单点登录【SSO】
一、应用场景
同一个公司的系统,每个系统都有一套用户名和密码,用户就会头大。所以需要一个鉴权中心,全部系统用同一套用户信息,同一个地方登录。
又比如同一套用户信息可以了,但进入每个系统都要输入一次账户密码,登录还是很麻烦。需要一次登录,处处登录。登录其中一个系统,进入其他系统的时候就不需要再次登录。
效果如下:
二、原理
用户打开站点A,发现未登录,站点A跳转SSO中心登录并且带上自己的URL,SSO中心登录成功后,跳转回站点带过来的URL并把TOKEN也带上。那么站点A登录成功。
三、实现
3.1、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,后台校验登陆状态,成功。
交互流程图如下:
3.2、单点登录流程演示
CAS登录服务demo核心代码如下:
用户实体类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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; } }
登录控制器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@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; } }
登录过滤器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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() { } }
登录页面:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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核心代码如下:
过滤器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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() { } }
控制器:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@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; } }
首页:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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。