单点登录系统设计

一、介绍

token鉴权最佳的实践场景就是在单点登录系统上。

在企业发展初期,使用的后台管理系统还比较少,一个或者两个。

以电商系统为例,在起步阶段,可能只有一个商城下单系统和一个后端管理产品和库存的系统。

随着业务量越来越大,此时的业务系统会越来越复杂,项目会划分成多个组,每个组负责各自的领域,例如:A组负责商城系统的开发,B组负责支付系统的开发,C组负责库存系统的开发,D组负责物流跟踪系统的开发,E组负责每日业绩报表统计的开发...等等。

规模变大的同时,人员也会逐渐的增多,以研发部来说,大致的人员就有这么几大类:研发人员、测试人员、运维人员、产品经理、技术支持等等。

他们会频繁的登录各自的后端业务系统,然后进行办公。

此时,可以设想一下,如果每个组都自己开发一套后端管理系统的登录,假如有10个这样的系统,同时一个新入职的同事需要每个系统都给他开放一个权限,那么可能需要给他开通10个账号。

随着业务规模的扩大,大点的公司,可能高达一百多个业务系统,那岂不是要配置一百多个账号,让人去做这种操作,岂不伤天害理。

面对这种繁琐而且又无效的工作,IT大佬们想到一个办法,那就是开发一套登录系统,所有的业务系统都认可这套登录系统,那么就可以实现只需要登录一次,就可以访问其他相互信任的应用系统。

这个登录系统,把它称为:单点登录系统。

好了,言归正传,下面从两个方面来介绍单点登录系统的实现。

  • 方案设计
  • 项目实践

二、方案设计

2.1、单体后端系统登录

在传统的单体后端系统中,简单点的操作,一般都会这么玩,用户使用账号、密码登录之后,服务器会给当前用户创建一个session会话,同时也会生成一个cookie,最后返回给前端。

当用户访问其他后端的服务时,只需要检查一下当前用户的session是否有效,如果无效,就再次跳转到登录页面;如果有效,就进入业务处理流程。

但是,如果访问不同的域名系统时,这个cookie是无效的,因此不能跨系统访问,同时也不支持集群环境的共享。

对于单点登录的场景,需要重新设计一套新的方案。

2.2、单点登录系统登录

先来一张图!

这个流程图,就是单点登录系统与应用系统之间的交互图。

当用户登录某应用系统时,应用系统会把将客户端传入的token,调用单点登录系统验证token合法性接口,如果不合法就会跳转到单点登录系统的登录页面;如果合法,就直接进入首页。

进入登录页面之后,会让用户输入用户名、密码进行登录验证,如果验证成功之后,会返回一个有效的token,然后客户端会根据服务端返回的参数链接,跳转回之前要访问的应用系统。

接着,应用系统会再次验证token的合法性,如果合法,就进入首页,流程结束。

引入单点登录系统后,接入的应用系统不需要关系用户登录这块,只需要对客户端的token做一下合法性鉴权操作就可以了。

而单点登录系统,只需要做好用户的登录流程和鉴权并返回安全的token给客户端。

有的项目,会将生成的token,存放在客户端的cookie中,这样做的目的,就是避免每次调用接口的时候都在url里面带上token。

但是,浏览器只允许同域名下的cookies可以共享,对于不同的域名系统, cookie 是无法共享的。

对于这种情况,可以先将 token 放入到url链接中,类似上面流程图中跳转思路,对于同一个应用系统,可以将token放入到 cookie 中,不同的应用系统,可以通过 url 链接进行传递,实现token的传输。

三、项目实践

在实践上,token的存储,有两种方案:

  • 存放在服务器,如果是分布式环境,一般都会存储在 redis 中
  • 存储在客户端,服务器做验证,天然支持分布式
3.1、存放在redis

存放在redis中,是一种比较常见的处理办法,最开始的时候也是这种处理办法。

当用户登录成功之后,会将用户的信息作为value,用uuid作为key,存储到redis中,各个服务集群共享用户信息。

代码实践也非常简单。

用户登录之后,将用户信息存在到redis,同时返回一个有效的token给客户端。

@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public TokenVO login(@RequestBody LoginDTO loginDTO){//...参数合法性验证//从数据库获取用户信息User dbUser = userService.selectByUserNo(loginDTO.getUserNo);//....用户、密码验证//创建tokenString token = UUID.randomUUID();//将token和用户信息存储到redis,并设置有效期2个小时redisUtil.save(token, dbUser, 2*60*60);//定义返回结果TokenVO result = new TokenVO();//封装tokenresult.setToken(token);//封装应用系统访问地址result.setRedirectURL(loginDTO.getRedirectURL());return result;
}

客户端收到登录成功之后,根据参数组合进行跳转到对应的应用系统。

跳转示例如下:http://xxx.com/page.html?token=xxxxxx

各个应用系统,只需要编写一个过滤器TokenFilter对token参数进行验证拦截,即可实现对接,代码如下:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException, SecurityException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;String requestUri = request.getRequestURI();String contextPath = request.getContextPath();String serviceName = request.getServerName();//添加到白名单的URL放行String[] excludeUrls = {"(?:/images/|/css/|/js/|/template/|/static/|/web/|/constant/).+$","/user/login","/user/createImage"};for (String url : excludeUrls) {if (requestUri.matches(contextPath + url) || (serviceName.matches(url))) {filterChain.doFilter(request, response);return;}}//运行跨域探测if(RequestMethod.OPTIONS.name().equals(request.getMethod())){filterChain.doFilter(request, response);return;}//检查token是否有效final String token = request.getHeader("token");if(StringUtils.isEmpty(token) || !redisUtil.exist(token)){ResultMsg<Object> resultMsg = new ResultMsg<>(4000, "token已失效");//封装跳转地址resultMsg.setRedirectURL("http://sso.xxx.com?redirectURL=" + request.getRequestURL());WebUtil.buildPrintWriter(response, resultMsg);return;}//将用户信息,存入request中,方便后续获取User user =  redisUtil.get(token);request.setAttribute("user", user);filterChain.doFilter(request, response);return;
}

上面返回的是json数据给前端,当然还可以直接在服务器采用重定向进行跳转,具体根据自己的情况进行选择。

由于每个应用系统都可能需要进行对接,因此可以将上面的方法封装成一个公共jar包,应用系统只需要依赖包即可完成对接!

3.2、token存放客户端

还有一种方案,是将token存放客户端,这种方案就是服务端根据规则对数据进行加密生成一个签名串,这个签名串就是所说的token,最后返回给前端。

因为加密的操作都是在服务端完成的,因此密钥的管理非常重要,不能泄露出去,不然很容易被黑客解密出来。

最典型的应用就是JWT

JWT 是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

如何实现呢?首先需要添加一个jwt依赖包。

<!-- jwt支持 -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version>
</dependency>

然后,创建一个用户信息类,将会通过加密存放在token

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserToken implements Serializable {private static final long serialVersionUID = 1L;/*** 用户ID*/private String userId;/*** 用户登录账户*/private String userNo;/*** 用户中文名*/private String userName;
}

接着,创建一个JwtTokenUtil工具类,用于创建token、验证token

public class JwtTokenUtil {//定义token返回头部public static final String AUTH_HEADER_KEY = "Authorization";//token前缀public static final String TOKEN_PREFIX = "Bearer ";//签名密钥public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";//有效期默认为 2hourpublic static final Long EXPIRATION_TIME = 1000L*60*60*2;/*** 创建TOKEN* @param content* @return*/public static String createToken(String content){return TOKEN_PREFIX + JWT.create().withSubject(content).withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).sign(Algorithm.HMAC512(KEY));}/*** 验证token* @param token*/public static String verifyToken(String token) throws Exception {try {return JWT.require(Algorithm.HMAC512(KEY)).build().verify(token.replace(TOKEN_PREFIX, "")).getSubject();} catch (TokenExpiredException e){throw new Exception("token已失效,请重新登录",e);} catch (JWTVerificationException e) {throw new Exception("token验证失败!",e);}}
}

同时编写配置类,允许跨域,并且创建一个权限拦截器

@Slf4j
@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {/*** 重写父类提供的跨域请求处理的接口* @param registry*/@Overridepublic void addCorsMappings(CorsRegistry registry) {// 添加映射路径registry.addMapping("/**")// 放行哪些原始域.allowedOrigins("*")// 是否发送Cookie信息.allowCredentials(true)// 放行哪些原始域(请求方式).allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")// 放行哪些原始域(头部信息).allowedHeaders("*")// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息).exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials");}/*** 添加拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {//添加权限拦截器registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");}
}

使用AuthenticationInterceptor拦截器对接口参数进行验证

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从http请求头中取出tokenfinal String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);//如果不是映射到方法,直接通过if(!(handler instanceof HandlerMethod)){return true;}//如果是方法探测,直接通过if (HttpMethod.OPTIONS.equals(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return true;}//如果方法有JwtIgnore注解,直接通过HandlerMethod handlerMethod = (HandlerMethod) handler;Method method=handlerMethod.getMethod();if (method.isAnnotationPresent(JwtIgnore.class)) {JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);if(jwtIgnore.value()){return true;}}LocalAssert.isStringEmpty(token, "token为空,鉴权失败!");//验证,并获取token内部信息String userToken = JwtTokenUtil.verifyToken(token);//将token放入本地缓存WebContextUtil.setUserToken(userToken);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//方法结束后,移除缓存的tokenWebContextUtil.removeUserToken();}
}

最后,在controller层用户登录之后,创建一个token,存放在头部即可

/*** 登录* @param userDto* @return*/
@JwtIgnore
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){//...参数合法性验证//从数据库获取用户信息User dbUser = userService.selectByUserNo(userDto.getUserNo);//....用户、密码验证//创建token,并将token放在响应头UserToken userToken = new UserToken();BeanUtils.copyProperties(dbUser,userToken);String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);//定义返回结果UserVo result = new UserVo();BeanUtils.copyProperties(dbUser,result);return result;
}

到这里基本就完成了!

其中AuthenticationInterceptor中用到的JwtIgnore是一个注解,用于不需要验证token的方法上,例如验证码的获取等等。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {boolean value() default true;
}

WebContextUtil是一个线程缓存工具类,其他接口通过这个方法即可从token中获取用户信息。

public class WebContextUtil {//本地线程缓存tokenprivate static ThreadLocal<String> local = new ThreadLocal<>();/*** 设置token信息* @param content*/public static void setUserToken(String content){removeUserToken();local.set(content);}/*** 获取token信息* @return*/public static UserToken getUserToken(){if(local.get() != null){UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class);return userToken;}return null;}/*** 移除token信息* @return*/public static void removeUserToken(){if(local.get() != null){local.remove();}}
}

对应用系统而言,重点在于token的验证,可以将拦截器方法封装成一个公共的jar包,然后各个应用系统引用即可!

和上面介绍的token存储到redis方案类似,不同点在于:一个将用户数据存储到redis,另一个是采用加密算法存储到客户端进行传输。

四、小结

在实际的使用过程中,更加倾向于采用jwt方案,直接在服务端使用签名加密算法生成一个token,然后在客户端进行流转,天然支持分布式,但是要注意加密时用的密钥要安全管理。

而采用redis方案存储的时候,需要搭建高可用的集群环境,同时保证缓存数据不会失效等等,维护成本高!

在实际的实现上,每个公司玩法不一样,有的安全性要求高,后端还会加上密钥环节进行安全验证,基本思路大同小异。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/799273.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

药店药品进销存管理系统软件可以对有效期管理查询以及对批号库存管理

药店药品进销存管理系统软件可以对有效期管理查询以及对批号库存管理 一、前言 以下软件操作教程以&#xff0c;佳易王药店药品进销存管理软件为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 软件可以对药品有效期进行管理查询&#xff0c;可以…

【C++进阶】哈希表(哈希函数、哈希冲突、开散列、闭散列)

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;c大冒险 总有光环在陨落&#xff0c;总有新星在闪烁 引言&#xff1a; 我们之前…

【Frida】【Android】 10_爬虫之WebSocket协议分析

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

实现第一个动态链接库 游戏插件 成功在主程序中运行 dll 中定义的类

devc 5.11编译环境 dll编译环境设置参考 Dev c C语言实现第一个 dll 动态链接库 创建与调用-CSDN博客 插件 DLL代码和主程序代码如下 注意 dll 代码中的class 类名需要 和主程序 相同 其中使用了函数指针和强制类型转换 函数指针教程参考 以动态库链接库 .dll 探索结构体…

HBase详解(2)

HBase 结构 HRegion 概述 在HBase中&#xff0c;会从行键方向上对表来进行切分&#xff0c;切分出来的每一个结构称之为是一个HRegion 切分之后&#xff0c;每一个HRegion会交给某一个HRegionServer来进行管理。HRegionServer是HBase的从节点&#xff0c;每一个HRegionServ…

elementPlus el-table动态列扩展及二维表格

1、循环列数据源&#xff0c;动态生成列 <template><div><el-table ref"table" :data"pageData.tableData" stripe style"width: 100%"><el-table-column v-for"column in pageData.columns" :key"column.p…

尚硅谷html5+css3(1)html相关知识

1.基本标签&#xff1a; <h1>最大的标题字号 <h2>二号标题字号 <p>换行 2.根标签<html> 包括<head>和<body> <html><head><title>title</title><body>body</body></head> </html> 3…

162 Linux C++ 通讯架构实战16,UDP/TCP协议的优缺点,使用环境对比。UDP 服务器开发

UDP/TCP协议的优缺点 TCP :面向连接的&#xff0c;可靠数据包传输。对于不稳定的网络层&#xff0c;采取完全弥补的通信方式。丢包重传 优点&#xff1a;稳定&#xff0c;数据流量稳定&#xff0c;速度稳定&#xff0c;顺序稳定 缺点&#xff1a;传输速度慢&…

青藏铁路双寨物流基地扩能改造工程接触网第一杆成功组立

4月2日凌晨&#xff0c;随着吊钩缓缓落下&#xff0c;在中铁电气化局北京电化公司现场作业人员的紧张操作下&#xff0c;青藏铁路双寨物流基地扩能改造工程首根接触网支柱稳稳落在基础上&#xff0c;标志着双寨物流基地扩能改造进入全面施工阶段。 双寨物流基地扩能改造工程包含…

Part1.Transformer架构

构成&#xff1a; 【手把手教你用Pytorch代码实现Transformer模型&#xff01;从零解读(Pytorch版本&#xff09;-哔哩哔哩】 https://b23.tv/o283hzU

JavaScript逆向爬虫——使用Python模拟执行JavaScript

使用Python模拟执行JavaScript 通过一些调试&#xff0c;我们发现加密参数token是由encrypt方法产生的。如果里面的逻辑相对简单的话&#xff0c;那么我们可以用Python完全重写一遍。但是现实情况往往不是这样的&#xff0c;一般来说&#xff0c;一些加密相关的方法通常会引用…

摄像头校准漫反射板提高识别物体

摄像头校准漫反射板是一种用于摄像头校准的重要工具。在摄像头成像过程中&#xff0c;由于各种因素的影响&#xff0c;如光线、角度、镜头畸变等&#xff0c;会导致摄像头成像出现偏差。为了消除这些偏差&#xff0c;提高摄像头的成像质量&#xff0c;需要使用摄像头校准漫反射…

从头开发一个RISC-V的操作系统(四)嵌入式开发介绍

文章目录 前提嵌入式开发交叉编译GDB调试&#xff0c;QEMU&#xff0c;MAKEFILE练习 目标&#xff1a;通过这一个系列课程的学习&#xff0c;开发出一个简易的在RISC-V指令集架构上运行的操作系统。 前提 这个系列的大部分文章和知识来自于&#xff1a;[完结] 循序渐进&#x…

第十四讲:C语言字符函数和字符串函数

目录 1. 字符分类函数 2、字符转换函数 3. strlen的使⽤和模拟实现 4. strcpy 的使⽤和模拟实现 5. strcat 的使⽤和模拟实现 6. strcmp 的使⽤和模拟实现 7. strncpy 函数的使⽤ 8. strncat 函数的使⽤ 9. strncmp函数的使⽤ 10. strstr 的使⽤和模拟实现 11. strt…

mysql的索引类型与数据存储

mysql索引与类型 什么是索引&#xff1f; 索引&#xff08;Index&#xff09;是帮助MySQL高效获取数据的数据结构。我们可以简单理解为&#xff1a;快速查找排好序的一种数据结构。Mysql索引主要有两种结构&#xff1a;BTree索引和Hash索引。我们平常所说的索引&#xff0c;如…

校园圈子小程序,大学校园圈子,三段交付,源码交付,支持二开

介绍 在当今的数字化时代&#xff0c;校园社交媒体和在线论坛成为了学生交流思想、讨论问题以及分享信息的常用平台。特别是微信小程序&#xff0c;因其便捷性、用户基数庞大等特点&#xff0c;已逐渐成为构建校园社区不可或缺的一部分。以下是基于现有资料的校园小程序帖子发…

(已解决)引入本地bootstrap无效,bootstrap和jquery的引入

问题&#xff1a; 首先我是跟着张天宇老师下载的bootstrap文件&#xff0c;新建了一个css文件夹&#xff0c;但是这样子<link rel"stylesheet" type"text/css" src"./css/bootstrap.css">在index.html引入没有用。 解决办法: 1.把建立的…

【opencv】示例-dft.cpp 该程序演示了离散傅立叶变换 (dft) 的使用,获取图像的 dft 并显示其功率谱...

#include "opencv2/core.hpp" // 包含OpenCV核心功能头文件 #include "opencv2/core/utility.hpp" // 包含OpenCV实用程序头文件 #include "opencv2/imgproc.hpp" // 包含OpenCV图像处理头文件 #include "opencv2/imgcodecs.hpp" // 包…

CSS 学习笔记 总结

CSS 布局方式 • 表格布局 • 元素定位 • 浮动布局&#xff08;注意浮动的负效应&#xff09; • flex布局 • grid布局&#xff08;感兴趣的可以看下菜鸟教程&#xff09; 居中设置 元素水平居中 • 设置宽度后&#xff0c;margin设置为auto • 父容器设置text-alig…

积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路(优化前一万多导出失败,优化后支持百万级跨库表导出)

文章目录 积木报表Excel数据量大导出慢导不出问题、大量数据导不出问题优化方案和分析解决思路&#xff08;优化前一万多导出失败&#xff0c;优化后支持百万级跨库表导出&#xff09;优化结果需求背景和解决方案的思考解决方案流程描述&#xff1a;关键代码引入easy excel新建…