架构 - 单点登录 - Springboot 模拟单点登录
SSO: Single Sign On,官方的概念:web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。
简言之,系统内部通过某种技术实现用户统一登录和注销,所以单点登录技术一定要包括两部分:登录、注销。
1为什么要用单点登录?因为Cookie不能跨域。
Cookie为什么不能跨域请参考《Cookie详解与跨域问题》。
2如何实现单点登录?
建立权限认证中心来处理登录和注销的问题,真正提供服务的应用服务端通过Filter将鉴权任务重定向给认证中心。
原理图:
登录:
注销:
首先看得出分为2个角色,一个是应用服务端,也就是认证客户端;另一个是认证服务端。根据上图来分析2个角色应该具备的功能。
认证客户端应该具备的能力:
1必须以Filter或者插件等形式提供,方便系统接入SSO。
2未登陆的用户重定向到SSO认证中心
3接收SSO发来的令牌并将该令牌发回给SSO做令牌认证
4处理令牌认证结果并创建局部会话
5拦截用户注销请求并重定向到SSO
6处理SSO发来的注销会话请求
认证服务端应该具备的能力:
0独立的web服务
1提供登陆页面,和对用户的校验
2创建全局会话、提供token
3校验token有效性并维护client端地址
4处理注销请求、销毁全局会话、并通知维护的client端地址
5判断用户有没有登录
代码模拟:
总共4个项目,核心的是2个SSO-server和SSO-client,其中client不是独立的web应用而是代码插件。SSO-mock-app1和SSO-mock-app2是安装了插件的2个独立应用。
Sso-server:https://github.com/yejingtao/forblog/tree/master/sso-server
Sso-client:https://github.com/yejingtao/forblog/tree/master/sso-client
Sso-mock-app1:https://github.com/yejingtao/forblog/tree/master/sso-mock-app1
Sso-moke-app2:https://github.com/yejingtao/forblog/tree/master/sso-mock-app2
Sso-client包括3部分:filter负责判断用户是否登陆和重定向到sso-server;controller负责与sso-server传递令牌等通信;service维护局部会话状态。
Sso-server包括3部分:controller负责与sso-client传递令盘、校验用户登陆情况等通信;service维护全局用户会话状态;templates为用户登陆提供页面入口
下面我们按照图中的顺序来对照代码看一下:
1 客户端filter判断如果用户没有登录,重定向到sso-server端:
登陆过放行
- if(userName!= null
- && String.valueOf(userName).trim().length()> 0
- && userAccessService.checkUserStatus(userName.toString())) {
- chain.doFilter(req, response);
- }
没登陆过重定向:
- String originalUrl = req.getRequestURL().toString();
- httpResponse.sendRedirect(ssoServerPath+ "/index?originalUrl="+originalUrl+ "&ssoUser="+userName);
2 sso-server端收到请求也需要判断用户是否登陆,用户不存在就给到自己的登陆页面,同时返回请求过来的连接在成功登陆后做3秒自动跳转。
- <code class="language-java">@RequestMapping("/index")
- public String firstCheck(HttpServletRequest request) {
- String originalUrl = request.getParameter("originalUrl");
- String ssoUser = request.getParameter("ssoUser");
- String token = null;
- boolean loginFlag = false;
- if(ssoUser!=null && ssoUser.trim().length()>0) {
- //对用户先判断是否已经登陆过
- token = authSessionService.getUserToken(ssoUser);
- if(token!=null) {
- loginFlag = true;
- }
- }
- if(loginFlag) {
- //判断如果用户已经在SSO-Server认证过,直接发送token
- if(tokenTrans(request,originalUrl,ssoUser,token)) {
- if(originalUrl!=null) {
- if(originalUrl.contains("?")) {
- originalUrl = originalUrl + "&ssoUser="+ssoUser;
- }else {
- originalUrl = originalUrl + "?ssoUser="+ssoUser;
- }
- }
- }
- return "redirect:"+originalUrl;
- }else {
- //需要替换成专业点的路径,自己登陆下了
- return "redirect:/loginPage?originalUrl="+request.getParameter("originalUrl");
- }
- }</code>
3 用户登陆页面登录后,验证用户名、创建token
- //登陆逻辑,返回的是令牌
- @RequestMapping(value= "/doLogin",method=RequestMethod.POST)
- public String login(HttpServletRequest request, HttpServletResponse response,
- String userName, String password, String originalUrl) {
- if(authSessionService.verify(userName,password)) {
- String token = authSessionService.cacheSession(userName);
- if(tokenTrans(request,originalUrl,userName,token)) {
- //跳转到提示成功的页面
- request.setAttribute( "helloName", userName);
- if(originalUrl!= null) {
- if(originalUrl.contains( "?")) {
- originalUrl = originalUrl + "&ssoUser="+userName;
- } else {
- originalUrl = originalUrl + "?ssoUser="+userName;
- }
- request.setAttribute( "originalUrl", originalUrl);
- }
- }
- return "hello"; //TO-DO 三秒跳转
- }
- //验证不通过,重新来吧
- if(originalUrl!= null) {
- request.setAttribute( "originalUrl", originalUrl);
- }
- return "loginIndex";
- }
重点是token如何传给sso-client端并从clinet端注册上来地址的:
4 server端将token传给client端:
- private boolean tokenTrans(HttpServletRequest request, String originalUrl,String userName, String token) {
- String[] paths = originalUrl.split( "/");
- String shortAppServerUrl = paths[ 2];
- String returnUrl = "http://"+shortAppServerUrl+ "/receiveToken?ssoToken="+token+ "&userName="+userName;
- //http://peer1:8088/receiveToken?ssoToken=80414bcb-a71d-48c8-bfee-098a303324d4&userName=xixi
- return "success".equals(restTemplate.getForObject(returnUrl, String.class));
- }
- @RequestMapping( "/receiveToken")
- @ResponseBody
- public String receiveToken(HttpServletRequest request, String ssoToken,String userName) {
- if(ssoToken!= null && ssoToken.toString().trim().length()> 0) {
- String realUrl = request.getRequestURL().toString();
- String[] paths = realUrl.split( "/");
- String realUrlUrls = paths[ 2];
- String returnUrl = ssoServerPath+ "/varifyToken?address="+realUrlUrls+ "&token="+ssoToken;
- //http://peer2:8089/varifyToken?address=peer1:8088&token=c2ce29be-5adb-4aaf-82cc-2ba24330176e
- String resultStr = restTemplate.getForObject(returnUrl, String.class);
- if( "true".equals(resultStr)) {
- //创建局部会话,保存用户状态为已登陆
- userAccessService.putUserStatus(userName, resultStr);
- return "success";
- }
- }
- return "error";
- }
- //校验token并注册地址
- @RequestMapping(value= "/varifyToken",method=RequestMethod.GET)
- @ResponseBody
- public String varifyToken(String token, String address) {
- return String.valueOf(authSessionService.checkAndAddAddress(token, address));
- }
- onload = function(){
- setInterval(go,1000);
- };
- var x = 3;
- function go(){
- x--;
- if(x>0){
- document.getElementById('sp').innerHTML = x;
- }else{
- var returnUrl = [[${originalUrl}]];
- location.href = returnUrl;
- }
- }
- @RequestMapping( "/ssoLogout")
- @ResponseBody
- public String ssoLogout(String userName) {
- String userToken = userAccessService.getUserToken(userName);
- if(userToken!= null) {
- String returnUrl = ssoServerPath+ "/logoutByToken?ssoToken="+userToken;
- return restTemplate.getForObject(returnUrl, String.class);
- }
- return "None Token";
- }
- @RequestMapping(value= "/logoutByToken",method=RequestMethod.GET)
- @ResponseBody
- public String logoutByToken(String ssoToken) {
- List<String> addressList = authSessionService.logoutByToken(ssoToken);
- if(addressList!= null) {
- addressList.stream().forEach(s -> sendLogout2Client(s,ssoToken));
- }
- return "logout";
- }
- private void sendLogout2Client(String address,String ssoToken) {
- String returnUrl = "http://"+address+ "/ssoDeleteToken?ssoToken="+ssoToken;
- try {
- restTemplate.getForObject(returnUrl, String.class);
- } catch(Exception e) {
- //Log and do nothing
- }
- }
10 对应的客户端要有个删除token的接收动作
- @RequestMapping( "/ssoDeleteToken")
- @ResponseBody
- public String ssoDeleteToken(String ssoToken) {
- userAccessService.deleteToken(ssoToken);
- return "success";
- }
a 客户端filter过滤范围要控制好,不要将sso-server发来的请求进行过滤,否则会死循环
b client端和server端的Redis要维护好user和token
启动测试:
Sso-mock-app1,植入sso-client插件,域名使用peer1,服务地址peer1:9001
Sso-mock-app2,植入sso-client插件,域名使用peer2,服务地址peer2:9002
Sso-Server,独立部署,域名使用peer3,服务地址peer3:9003
都启动之后,开始测试:
步骤一:无论我浏览器访问app1被重定向到http://peer3:9003/loginPage?originalUrl=http://peer1:9001/hello地址进行登录
步骤二:在peer3,也就是SSO-Server的登陆页面上用yejingtao账户登陆,登陆成功后3秒钟后页面自动跳转到peer1的步骤一中的请求页面。
步骤三:在app2服务尝试yejingtao账号直接访问,成功。
步骤四:在SSO-Server端注销该账号
步骤五:注销后app1和app2又要重新登录了。
整个过程中可以配合redisclient观察redis数据帮助自己理解。
由于只是验证和示例代码,好多可以优化的地方,例如
1 token已经预留了失效时间,可以在逻辑中增加对token时效的判断,并定时清理
2 sso-server与sso-client的通信可以改成post请求,虽然不会经过浏览器,从使用效果和安全级别上没有差别,但是从http协议设计来考虑还是post更好
3 Redis序列化部分可以换成Jackson2JsonRedisSerializer节省内存空间
4 sso-client拦截的权限范围可以放在application.yml中让用户自己设置
转载:https://blog.csdn.net/yejingtao703/article/details/78677450
架构 - 单点登录 - Springboot 模拟单点登录相关推荐
- SpringBoot模拟单点登录
SSO: Single Sign On,官方的概念:web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户.无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是 ...
- spirngmvc如何实现直接输入网页重定向到登录_Python 模拟新浪微博登录
点击上方"小猿学 Python",选择"置顶公众号" 作者:北岛知寒 链接:https://www.cnblogs.com/crazyacking/p/5232 ...
- php 提取登录QQsid_PHP模拟QQ登录的方法
本文实例讲述了PHP模拟QQ登录的方法.分享给大家供大家参考.具体实现方法如下: 原理是用curl模拟发送post登录,cookie保存本地 这样理论上可以支持永久单挂QQ //http://blog ...
- python实现模拟浏览器登录入口_登录采集——模拟浏览器登录QQ邮箱
网络爬虫学习中,简单的静态页面数据,难以满足我们的一颗"好学"之心,且现在我们"好奇"的数据往往藏得很深,要么需要登录,要么为动态加载--今天,就来分享一下模拟 ...
- 自学项目1:登录路由器(本地登录以及模拟远程登录)
项目背景:如果你需要查看路由器的各种信息,或者需要修改路由器的现有配置,或者需要对新购的路由器进行配置,由于路由器没有键盘和显示器,那么你首先要从计算机登录到路由器. 为什么要登录?如何登录? 很多人 ...
- 【C#桌面应用】第四节:制作简单的登录注册模拟窗口-登录部分的模拟
步骤一.工具箱中选择按钮 步骤二.根据上一节所演示的按钮名称修改方式进行修改按钮的名称 步骤三.添加用户名和密码的输入框和名称. 工具箱中,选择label文本及textBox1输入框的控件.
- 【Python养成】:案例(身高体重BMI值、模拟用户登录系统、键盘录入10个学生的成绩,计算出最高分、最低分和成绩总和、词频统计)
案例题目:身高体重BMI值 计算成人身高体重指数BMI值.公式:bmi = 体重 / (身高 * 身高),体重的单位是千克,身高的单位是米 .键盘输入身高和体重值,计算bmi值,并根据结果对用户做出友 ...
- [业务流程]JWT实现单点登录(SpringBoot + Vue +axious)
JWT实现单点登录(SpringBoot + Vue +axious) 准备阶段: **登录所需对象** admin{ "username":"", //作为T ...
- SpringBoot集成单点登录-“被挤下线”
SpringBoot配置单点登录 前言 你好,未来!上个周末过的还行,逛街逛到腿发软,生活还是要有仪式感,一生要待自己待你最亲近的人.周一休息,顺便看了看04版天龙八部,塑造了三位英雄人物,共同点:热 ...
最新文章
- 【JavaScript】AJAX教程
- ORACLE 如何查询被锁定表及释放session
- Linux下使用popen()执行shell命令
- Spark RDD在Spark中的地位和作用如何?
- iOS Hacker 越狱后如何使用 root 运行应用
- WEB入门实践-张晨光-专题视频课程
- python执行shell命令行_python执行命令行:python中执行shell命令行read结果
- 两个子线程不冲突_多线程操作可见性
- java 获取物理路径_JSP---jsp页面获取物理路径
- Class.isAssignableFrom(Class clz)方法 与 instanceof 关键字的区别
- 刚有个做电商的朋友在说,他们想在网上造出品牌销量很容易
- 帝国列表页分开调取年份和月份单独调用的方法?
- 操作ADS1115进行4个通道AD值的读取
- PHP使用AES加密和解密
- 解析xml文件的几种方法和原理
- 洗车店的预约系统小程序开发步骤_分享预约系统小程序有什么作用
- 34个最新的营销失败案例分析
- 云服务器的概念(云服务器年度最低价来了,就要抓住双11)
- 初学者对测绘软件的了解
- 烽火18台系列之八——Webshell,隐藏在网站之下的潘多拉魔盒
热门文章
- Solr vs Elasticsearch vs Lucene
- 解决更新包与已安装应用的签名不一致的问题
- head first java 最新版_Head First Java.(第2版)
- Windows Tomcat服务启动 修改Xms Xmx
- SIGIR 2021 | 推荐系统相关论文分类整理
- java添加zip并下载,java - 使用java创建zip并使其可下载。 - SO中文参考 - www.soinside.com...
- ubuntu18.04 安装 roboware-studio
- mongodb-更新操作符
- Flutter ListView 局部刷新数据、ListView点赞收藏
- Linux rz命令安装失败解决方法