前言

本文适合初学者,如有不足或错误之处,还请大家在下方留言指正。(文章稍长,建议点赞收藏)

一、SSO单点登录是什么?

单点登录简介

单点登录SSO (Single Sign On) 是指在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。

单系统登录

在之前我们做的单系统登录,它的核心是Cookie,Cookie携带会话id在浏览器与服务器之间维护会话状态。

Cookie 和 Session

众所周知,HTTP是无状态的协议,这意味着服务器无法确认用户的信息。于是乎,W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie

如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。

HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是乎:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户。

那它们之间有什么区别呢?

Cookie 一般用来保存用户信息(数据保存在客户端(浏览器端))
①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;
②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);
③登录一次网站后访问网站其他页面不需要重新登录。

Session 的主要作用就是通过服务端记录用户的状态(数据保存在服务器端)
典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

单系统登录流程

1.用户登录时,验证用户的账户和密码
2.生成一个Token保存在数据库中,将Token写到Cookie中
3.将用户数据保存在Session中
4.请求时都会带上Cookie,检查有没有登录,如果已经登录则放行

多系统登录

虽然单系统登录有多种完美的解决方案,但对于多系统应用群已经不再适用了,为什么呢?

主要存在以下几个问题:

  1. Session不共享问题
  2. Cookie跨域的问题

其实两个问题很类似
Session不共享:很容易理解,多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是不共享的。
Cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的Cookie,而不是所有Cookie

问题解决

Session不共享问题

  • 使用广播机制将Session复制到各个服务器
  • 把Session数据放在Redis中(使用Redis模拟Session)

Cookie跨域的问题

  • 客户端对Cookie进行解析,将Token解析出来,此后请求都把Token带上
  • 多个域名共享Cookie,在写到客户端的时候设置Cookie的domain。

登录流程

上面的问题解决,一个简单的单点登录就已经完成了,来看看它的登录流程吧

对于上图的说明:

1.用户访问系统A受保护资源,系统A发现用户并没有登录,于是重定向到sso认证中心,并将自己的地址作为参数
2.sso认证中心发现用户未登录,将用户引导至登录页面
3.用户进行输入用户名和密码进行登录,用户与认证中心建立全局会话(生成一份token,写到cookie中,保存在浏览器上)
4.sso认证中心带着token跳转回系统A
5.系统A去sso认证中心验证这个token,系统A和用户建立局部会话(创建Session)。系统A已登录
6.用户访问系统B的受保护资源,重定向到sso认证中心,并将自己的地址作为参数
7.认证中心根据带过来的Cookie发现已经与用户建立了全局会话了,认证中心重定向回系统B,并把Token携带过去给系统B
8.系统B去sso认证中心验证这个token,系统B和用户建立局部会话(创建Session)。系统B已登录

注销

有了登录,肯定就有注销登录。单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁

首先要了解,全局会话和局部会话的约束关系:

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁

注销流程:

1.用户发起注销请求
2.系统A根据用户与系统A建立的会话id拿到令牌,向sso认证中心发起注销请求
3.sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
4.sso认证中心向所有注册系统发起注销请求
5.各注册系统接收sso认证中心的注销请求,销毁局部会话
6.sso认证中心引导用户至登录页面

二、代码示例

创建项目

上章讲了动态路由zuul,我们紧接上章,通过zuul配合redis实现单机版的sso单点登录。

首先创建一个springboot模块sso-server,并在eureka上注册

引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

接口创建

这里验证token,只是简单判断了一下是否存在

import com.local.springboot.sso.ssoserver.util.AuthUtil;
import com.local.springboot.sso.ssoserver.util.RedisUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@RestController
@RequestMapping("/sso")
@SuppressWarnings("all")
public class LoginController {@Autowiredprivate AuthUtil authUtil;@Autowiredprivate RedisUtil redisUtil;/*** 验证用户登录令牌是否有效** @param accessToken 登录令牌* @return true 有效、false 无效*/@PostMapping("/checkAccessToken/{accessToken}")public boolean checkAccessToken(@PathVariable("accessToken") String accessToken) {return authUtil.checkAccessToken(accessToken);}/*** 用户登录界面** @param url* @return*/@RequestMapping("/toLogin")public ModelAndView toLogin(String url) {ModelAndView modelAndView = new ModelAndView("login");modelAndView.addObject("url", url);return modelAndView;}/*** 用户认证登录** @param response HttpServletResponse* @param userName 用户* @param password 密码* @param url      服务器请求url* @return 认证结果、重定向请求*/@RequestMapping("/login")public String login(HttpServletResponse response, String userName, String password, String url) {// 用户认证,并生成TokenString accessToken = authUtil.checkUser(userName, password);if (StringUtils.isNotBlank(accessToken)) {try {// 与用户建立全局会话(将Token写到cookie中)Cookie cookie = new Cookie("accessToken", accessToken);cookie.setMaxAge(60 * 3);//设置访问路径cookie.setPath("/");response.addCookie(cookie);// 重定向请求response.sendRedirect(url);} catch (IOException e) {e.printStackTrace();}}return "认证失败";}
}

用户认证

import com.local.springboot.sso.ssoserver.entity.TSysUserEntity;
import com.local.springboot.sso.ssoserver.serice.TSysUserService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;/*** @author: hzl* @date: 2021-09-14 18:13* @Description: 用户认证工具类**/
@Component
@SuppressWarnings("all")
public class AuthUtil {@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate TSysUserService userService;/*** 验证用户名和密码;并返回登录令牌** @param userName 用户名* @param password 密码* @return*/public String checkUser(String userName, String password) {String accessToken = "";// 判断用户名和密码是否正确TSysUserEntity entity = userService.getUserByName(userName);if (entity != null) {String dbPwd = entity.getPassword();String pwd = Md5Util.MD5(password);if (StringUtils.equals(dbPwd, pwd)) {// 用户名+时间戳加密生成登录令牌、存放redisString md5Str = userName + System.currentTimeMillis();accessToken = Md5Util.MD5(md5Str);// 登录令牌为key、存储用户信息(过期时间3分钟)redisUtil.set(accessToken, entity.getId(), 3 * 60);}}return accessToken;}public static void main(String[] args) {System.out.println(Md5Util.MD5("123456"));}/*** 验证用户登录令牌是否有效** @param accessToken 登录令牌* @return true 有效、false 无效*/public boolean checkAccessToken(String accessToken) {return redisUtil.hasKey(accessToken);}public static String getLoginUserId() {return null;}
}

application.properties配置文件

server.port=11033
spring.application.name=sso-servereureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka/spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/local_develop?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root#关闭页面缓存
spring.thymeleaf.cache=false
#thymeleaf访问根路径
spring.thymeleaf.prefix=classpath:/thymeleaf/
spring.thymeleaf.mode=LEGACYHTML5spring.redis.host=127.0.0.1
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=1000
spring.redis.jedis.pool.max-active=200
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=0

zuul-server

访问分布式系统的任意请求,被Zuul的Filter拦截过滤,修改过滤器逻辑,用户登录验证请求sso认证中心

import com.local.springboot.zuul.zuulserver.feign.SsoFeign;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;/*** 自定义过滤器*/
public class MyAccessFilter extends ZuulFilter {@Qualifier("ssoFeignFallback")@Autowiredprivate SsoFeign feign;/*** 过滤器类型,可选值有 pre、route、post、error。** @return*/@Overridepublic String filterType() {return "pre";}/*** 通过int值来定义过滤器的执行顺序* 过滤器的执行顺序,数值越小,优先级越高。** @return*/@Overridepublic int filterOrder() {return 0;}/*** 是否执行该过滤器,true 为执行,false 为不执行* 这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器。* 配置文件中禁用过滤器:* 【zuul.过滤器的类名.过滤器类型.disable=true,如:zuul.MyAccessFilter.pre.disable=true】** @return*/@Overridepublic boolean shouldFilter() {return false;}/*** 过滤器具体逻辑** @return* @throws ZuulException*/@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();HttpServletResponse response = ctx.getResponse();// 获取cookie里面的accessToken值String accessToken = "";Cookie[] cookies = request.getCookies();if (cookies != null) {for (Cookie cookie : cookies) {if ("accessToken".equals(cookie.getName())) {accessToken = cookie.getValue();}}}// 请求url地址String url = getUrl(request);// 过滤登录接口、登录页面、若带登录令牌,则验证令牌是否有效;有效则表示为登录用户if (url.contains("/sso-server/sso/toLogin") || url.contains("/sso-server/sso/login") ||((StringUtils.isNotBlank(accessToken)) && feign.checkAccessToken(accessToken))) {// 标识为登录用户ctx.setSendZuulResponse(true);ctx.setResponseStatusCode(200);} else {// 标识为未登录用户,跳转至sso认证中心,并将自己的地址作为参数ctx.setSendZuulResponse(false);ctx.setResponseStatusCode(302);try {response.sendRedirect("http://localhost:8088/sso-server/sso/toLogin?url=" + url);} catch (IOException e) {e.printStackTrace();}}return null;}private String getUrl(HttpServletRequest request) {// 请求url初始化StringBuilder url = new StringBuilder(request.getRequestURL().toString());// 请求方式String method = request.getMethod();if ("GET".equals(method)) {// GET请求拼接参数url.append("?");// 参数集合Map<String, String[]> parameterMap = request.getParameterMap();Object[] keys = parameterMap.keySet().toArray();for (int i = 0; i < keys.length; i++) {// 参数名String key = (String) keys[i];// 参数值String value = parameterMap.get(key)[0];url.append(key).append("=").append(value).append("&");}url.delete(url.length() - 1, url.length());}return url.toString();}
}

修改路由配置,映射sso-server

zuul.routes.sso-server.path=/sso-server/**
zuul.routes.sso-server.service-id=sso-server
#Zuul丢失Cookie的解决方案:
zuul.sensitive-headers=

这里偷了个懒,并没有做各个子系统创建session的步骤…

测试

启动服务(不要忘了Redis服务)

如图:

浏览器访问 http://localhost:8088/client-customer/feign/testRibbon,被zuul-server拦截重定向到sso-server登录页面


输入用户名和密码,登录失败,返回提示;登录成功,则重定向到之前的请求

再次访问http://localhost:8088/client-provider/api/provider,成功访问

可以看到cookie及过期时间

3分钟后我们再次访问 cookie、Redis失效,需要重新登录

到此,简单的sso单点登录就已经完成。

涉及以往文章:
SpringBoot —— 简单整合Redis实例
SpringCloud入门 —— Feign服务调用
SpringCloud入门 —— Zuul路由配置

« 上一章:SpringCloud入门 —— Zuul路由配置

创作不易,关注

SpringCloud入门 —— SSO 单点登录相关推荐

  1. (十)Java B2B2C o2o多用户商城 springcloud架构- SSO单点登录之OAuth2.0登录认证(1)

    2019独角兽企业重金招聘Python工程师标准>>> 之前写了很多关于spring cloud的文章,今天我们对OAuth2.0的整合方式做一下笔记,首先我从网上找了一些关于OAu ...

  2. 写了一个 SSO 单点登录的代码示例给胖友!

    发危~ " 摘要: 原创出处 http://www.iocoder.cn/Spring-Security/OAuth2-learning-sso/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1 ...

  3. java ssm 多租户_(十一)java B2B2C 源码 多级分销springmvc mybatis多租户电子商城系统- SSO单点登录之OAuth2.0登录流程(2)...

    上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...

  4. SSO单点登录学习总结(3)—— 基于CAS实现单点登录实例

    第一: 本demo在一个机器上实现(三个虚拟主机),来看SSO单点登录实例(我们可以布到多个机器上使用都是同一个道理的),一个服务器主机,和两个客户端虚拟主机 [html] view plaincop ...

  5. 互联网分布式微服务云平台规划分析--SSO单点登录系统

    介绍 鸿鹄云架构[SSO单点登录系统]为所有微服务提供统一的用户认证服务,系统本身属于微服务模式,使用JWT+Redis分布式存储方案,确保不同微服务.系统之间的安全通讯和统一用户校验.认证.在整个服 ...

  6. 一篇了解SSO单点登录

    SSO基础 文章目录 SSO基础 1.什么是单点登录? 2.回顾普通系统登录 3.多系统登录的问题与解决? 3.1.Session不共享问题 XXL-SSO框架基础入门 1.什么是XXL-SSO 2. ...

  7. Spring Cloud云架构 - SSO单点登录之OAuth2.0登录流程(2)

    上一篇是站在巨人的肩膀上去研究OAuth2.0,也是为了快速帮助大家认识OAuth2.0,闲话少说,我根据框架中OAuth2.0的使用总结,画了一个简单的流程图(根据用户名+密码实现OAuth2.0的 ...

  8. [精华][推荐]CAS SSO单点登录服务端客户端学习

    1.了解单点登录 SSO 主要特点是: SSO 应用之间使用 Web 协议(如 HTTPS) ,并且只有一个登录入口. SSO 的体系中有下面三种角色: 1) User(多个) 2) Web 应用(多 ...

  9. 学习CAS实现SSO单点登录

    学习CAS实现SSO单点登录 网上找了几篇比较详细的教程,在这记录一下: 原理: CAS实现SSO单点登录原理 教程: 1.CAS实现单点登录(SSO)经典完整教程 2.SSO之CAS单点登录实例演示 ...

最新文章

  1. mybatis 中#与$的区别
  2. 利用fnd_conc_global.set_req_globals设置子请求的Parent Request ID
  3. Spring boot自动注册DispatcherServlet
  4. DB2 SQL性能调优秘笈pdf
  5. odbc驱动程序管理器连接未打开_Windows 10 怎么修复 Windows 中的 Wi-Fi 连接问题,我教你...
  6. JavaScript性能优化【上】-- 内存管理、垃圾回收
  7. Linux下的定时器
  8. OK335xS canutils deal with compile error
  9. opencv-api convexityDefects
  10. redis 值字符串前面部分乱码_Spring-RedisTemplate写入数据乱码问题的复现与解决
  11. 重新设置Visual Studio 环境
  12. TRNSYS与MATLAB联合仿真
  13. 阿里云大学:云端搭建Linux学习环境
  14. 腾讯程序员平均月薪7.48万,分分钟变身“柠檬精”
  15. Widows Virtual PC 修改bios安装OEM xp
  16. Java程序员必读精选书籍分享,强烈推荐
  17. 基于SSM实现医院预约挂号系统
  18. 完全依赖XP必将自食其果
  19. 自适应模糊PID在反应釜温度控制中的应用
  20. 服务器运行bat文件闪退,win10运行bat文件一闪而过怎么办_网站服务器运行维护

热门文章

  1. Vivado软件的使用
  2. GUI界面的视频帧提取
  3. python 静态成员变量
  4. Linux系统内部流量转发机制,使用TC实现基于Linux系统的流量管理
  5. 通行时间可调的两路口交通灯设计实验(基于Multisim仿真)
  6. 查看tomcat的线程数
  7. LaSO: Label-Set Operations networks for multi-label few-shot learning 论文笔记
  8. Shiro中principal和credential的区别
  9. 电脑强制删除顽固文件
  10. 逃离僵尸岛【最短路】