单点登录及实现方案原理
单点登录概念:
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。例如,网页登录了淘宝账号,天猫,钉钉等阿里系应用都不用再二次登录了。 SSO核心意义就一句话:一处登录,处处登录;一处注销,处处注销。
一、基于cookie实现单点登录
这是最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。
缺点:
- Cookie不安全
- 不能跨域实现免登
对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie
的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的;对于第二个问题,不能跨域实现免登更是硬伤。因此,有了基于Session的单点登录。
cookie实现单点登录流程分析
需要做ip和域名映射:
#主系统
127.0.0.1 www.sso.com
#登录系统
127.0.0.1 login.sso.com
#vip子系统
127.0.0.1 vip.sso.com
#购物车子系统
127.0.0.1 cart.sso.com
-
用户首次登录,携带请求页url,跳转登录系统控制层接口
@Controller @RequestMapping("view") public class ViewController { @Autowired RestTemplate restTemplate; //携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤 private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token="; @GetMapping("/index") public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){ if(null!=cookie){ String token = cookie.getValue(); if(StringUtils.isNotEmpty(token)){ Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class); session.setAttribute("loginUser",result); } }else { session.removeAttribute("loginUser"); } return "index"; }首页
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>这里是首页展示页面</h1> <span> <a th:if="${session.loginUser==null}" href="http://login.sso.com:9001/view/login?target=http://www.sso.com:9000/view/index">登录</a> <a th:unless="${session.loginUser==null}" href="http://www.sso.com:9000/view/exit">退出</a> </span> <p th:unless="${session.loginUser==null}"> <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span> 已登录 </p> </body> </html> -
判断cookie为空,直接跳转登录界面
/** * 页面跳转逻辑 */ @Controller @RequestMapping("view") public class ViewController { /** * 跳转到登录页面 * * @return */ @GetMapping("/login") public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session, @CookieValue(required = false, value = "TOKEN") Cookie cookie) { if (StringUtils.isEmpty(target)) { //未携带url设置默认页面 target = "http://www.sso.com:9000"; } if (cookie != null) { //如果是已经登录的用户再次访问登录系统时,就重定向到回页面 String token = cookie.getValue(); User user = LoginCacheUtil.loginUser.get(token); if (user != null) { return "redirect:" + target; } } //未登录,跳转登录controller session.setAttribute("target", target); return "login"; } }登录界面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>Login Module</title> </head> <body> <h1>欢迎来到登录页面</h1> <p style="color:red" th:text="${session.msg}"></p> <form action="/login" method="post"> 用户名:<input name="username" value=""/> 密码:<input name="password" value=""/> <button type="submit">登录</button> </form> </body> </html> -
登录页面表单提交,登录控制层接收用户信息并校验数据是否正确,用户存在,则将该用户信息保存(redis,或Map),创建cookie,将新建token存储在cookie中,并响应到HttpServletResponse中供其他系统访问,登录成功跳回请求系统控制层接口。
判断是否登录,已登录判断用户存在session,跳转会请求页面
@Controller @RequestMapping("view") public class ViewController { /** * 跳转到登录页面 * * @return */ @GetMapping("/login") public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session, @CookieValue(required = false, value = "TOKEN") Cookie cookie) { if (StringUtils.isEmpty(target)) { //未携带url设置默认页面 target = "http://www.sso.com:9000"; } if (cookie != null) { //如果是已经登录的用户再次访问登录系统时,就重定向到回页面 String token = cookie.getValue(); User user = LoginCacheUtil.loginUser.get(token); if (user != null) { return "redirect:" + target; } } //未登录,跳转登录controller session.setAttribute("target", target); return "login"; } }未登录请求登录接口
@Controller @RequestMapping("/login") public class LoginController { // 模拟数据库用户 private static Set<User> dbUser; static { dbUser = new HashSet<>(); dbUser.add(new User(0, "zhangsan", "1234")); dbUser.add(new User(1, "lisi", "1234")); dbUser.add(new User(2, "中国魂", "123456")); } @PostMapping public String doLogin(User user, HttpSession session, HttpServletResponse response) { String target = (String) session.getAttribute("target"); // 模拟从数据库中通过登录的用户名和密码查找用户, Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) && dbUser.getPassword().equals(user.getPassword())).findFirst(); // 判断用户是否登录d if (first.isPresent()) { // 保存用户登录信息 String token = UUID.randomUUID().toString(); LoginCacheUtil.loginUser.put(token, first.get()); session.removeAttribute("msg"); //设置cookie Cookie cookie = new Cookie("TOKEN", token); //cookie在子系统间访问域要相同 //cookie不能跨域 cookie.setDomain("sso.com"); /* cookie过期时间设置方式: cookie.setMaxAge(0);//不记录cookie cookie.setMaxAge(-1);//会话级cookie,关闭浏览器失效 cookie.setMaxAge(60*60);//过期时间为1小时 */ cookie.setMaxAge(-1); cookie.setPath("/"); //通过HttpServletResponse将cookie响应到子系统 response.addCookie(cookie); } else { //登录失败 session.setAttribute("msg", "用户名或密码错误"); return "login"; } // 重定向到target地址 return "redirect:" + target; } -
(用户回显)判断cookie不为空,从cookie中获取token,用token从redis中或者跨系统从Map中获取用户信息,并存入session,跳转请求页面显示登录用户。
-
子系统点击登录,在登录系统中判断cookie是否存在,存在则获取token,根据token获取登录用户信息,如果用户存在,则直接跳回请求的子系统页面。
子系统页面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>购物车</title> </head> <body> <h1>这里是购物车展示页面</h1> <span> <a th:if="${session.loginUser==null}" href="http://login.sso.com:9001/view/login?target=http://cart.sso.com:9003/view/index">登录</a> <a th:unless="${session.loginUser==null}" href="http://cart.sso.com:9003/view/exit">退出</a> </span> <p th:unless="${session.loginUser==null}"> <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span> 已登录 </p> </body> </html>登录逻辑
@Controller @RequestMapping("view") public class ViewController { @Autowired RestTemplate restTemplate; //携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤 private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token="; @GetMapping("/index") public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){ if(null!=cookie){ String token = cookie.getValue(); if(StringUtils.isNotEmpty(token)){ Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class); session.setAttribute("loginUser",result); } }else { session.removeAttribute("loginUser"); } return "index"; }跨服务交互接口,从map中获取用户信息
/** * 通过token获取用户登录信息 * 跨系统交互 */ @GetMapping("/info") @ResponseBody public ResponseEntity<User> getUserInfo(String token) { if (StringUtils.isNotEmpty(token)) { User user = LoginCacheUtil.loginUser.get(token); return ResponseEntity.ok(user); } return new ResponseEntity(HttpStatus.BAD_REQUEST); }public class LoginCacheUtil { public static HashMap<String, User> loginUser = new HashMap<>(); } -
退出单点登录:设置cookie过期时间为0,就ok。
//退出 @GetMapping("/exit") public String toExit(HttpServletRequest request, HttpSession session, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { for (Cookie cookie : cookies) { if ("TOKEN".equals(cookie.getName())) { cookie.setDomain("sso.com"); cookie.setPath("/"); cookie.setMaxAge(0); response.addCookie(cookie); } } } return "redirect:/view/index"; }pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.sso</groupId> <artifactId>sso-use-cookie</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/><!--指定pom.xml文件的相对路径,默认从当前pom文件的上一级目录找--> </parent> <!--所属子系统--> <modules> <module>sso-main</module><!--主系统--> <module>sso-vip</module><!--vip系统--> <module>sso-cart</module><!--购物车系统--> <module>sso-login</module><!--登录系统--> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.version>1.1</project.version> <java.version>1.8</java.version> <!-- lombok --> <lombok.version>1.18.16</lombok.version> <commons-lang3.version>3.11</commons-lang3.version> </properties> <dependencies> <!--内嵌tomcat,包含starter依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--支持单元测试依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version> </dependency> <!-- lombok简化实体代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <!--页面模板--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </pluginManagement> </build> </project>
本文介绍单点登录(SSO)的基本概念及其基于Cookie的实现方案。SSO允许用户在一个系统登录后,无需再次登录即可访问其他互信系统。文章详细解析了利用Cookie存储用户凭证并实现免登录的具体流程。
1570

被折叠的 条评论
为什么被折叠?



