单点登录原理及JWT实现
单点登录原理及JWT实现
一、单点登录效果
首先我们看通过一个具体的案例来加深对单点登录的理解。案例地址:https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search 把案例代码直接导入到IDEA中
然后分别修改下server和samples中的配置信息
在host文件中配置
127.0.0.1 sso.server.com
127.0.0.1 client1.com
127.0.0.1 client2.com
然后分别启动server和两个simple服务。
访问测试:
其中一个节点登录成功后其他节点就可以访问了
自行测试。
二、单点登录实现
清楚了单点登录的效果后,我们就可以自己来创建一个单点登录的实现了。来加深下单点登录的理解了。
1.创建项目
通过Maven创建一个聚合工程,然后在工程中创建3个子模块,分别为认证服务和客户端模块。
引入相同的依赖
2.client1
我们先在client1中来提供相关的接口。我们提供一个匿名访问的接口和一个需要认证才能访问的接口。
@Controller
public class UserController {@ResponseBody@GetMapping("/hello")public String hello(){return "hello";}@GetMapping("/queryUser")public String queryUser(Model model){model.addAttribute("list", Arrays.asList("张三","李四","王五"));return "user";}
}
user.html中的代码为:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>$Title$</title>
</head>
<body><h1>用户管理:</h1><ul><li th:each="user:${list}">[[${user}]]</li></ul>
</body>
</html>
访问测试:
没有认证就能访问,所以得加上验证的逻辑。
@GetMapping("/queryUser")public String queryUser(Model model, HttpSession session){Object userLogin = session.getAttribute("userLogin");if(userLogin != null){// 说明登录过了,直接放过model.addAttribute("list", Arrays.asList("张三","李四","王五"));return "user";}// 说明没有登录,需要跳转到认证服务器认证 为了能在登录成功后跳回到当前页面,传递参数return "redirect:http://sso.server:8080/loginPage?redirect=http://client1.com:8081/queryUser";}
可以看到当我们访问queryUser请求的时候,因为没有登录所以会重定向到认证服务中的服务,做登录处理。这时就需要进入到server服务中处理
3.server服务
在服务端我们需要提供两个接口,一个调整到登录界面,一个处理认证逻辑以及一个登录页面
@Controller
public class LoginController {/*** 跳转到登录界面的逻辑* @return*/@GetMapping("/loginPage")public String loginPage(@RequestParam(value = "redirect" ,required = false) String url, Model model){model.addAttribute("url",url);return "login";}/*** 处理登录请求* @return*/@PostMapping("/ssoLogin")public String login(@RequestParam("userName") String userName,@RequestParam("password") String password,@RequestParam(value = "url",required = false) String url){if("zhangsan".equals(userName) && "123".equals(password)){// 登录成功return "redirect:"+url;}// 登录失败重新返回登录页面return "redirect:loginPage";}}
登录页面代码逻辑
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>sso-server-login</title>
</head>
<body><h1>Server登录页面</h1><form action="/ssoLogin" method="post" >账号:<input type="text" name="userName" ><br/>密码:<input type="password" name="password"><br/><input type="hidden" name="url" th:value="${url}"><input type="submit" value="提交"></form>
</body>
</html>
然后当我们在client1中访问需要认证的服务的时候就会跳转到登录界面
提交登录操作。当我们提交登录成功的情况,应该要重定向会原来的访问地址,但实际情况和我们所想的有点出入:
原来的queryUser中的逻辑为:
4. 认证凭证
上面的问题是我们在认证服务登录成功了,但是client1中并不知道登录成功了,所以认证成功后需要给client1一个认证成功的凭证。也就是Token信息。
/*** 处理登录请求* @return*/@PostMapping("/ssoLogin")public String login(@RequestParam("userName") String userName,@RequestParam("password") String password,@RequestParam(value = "url",required = false) String url){if("zhangsan".equals(userName) && "123".equals(password)){// 通过UUID生成Token信息String uuid = UUID.randomUUID().toString().replace("-","");// 把生成的信息存储在Redis服务中redisTemplate.opsForValue().set(uuid,"zhangsan");// 登录成功return "redirect:"+url+"?token="+uuid;}// 登录失败重新返回登录页面return "redirect:loginPage";}
生成的Token同步保存在了Redis中,然后在重定向的地址中携带了token信息。然后在client1中处理
@GetMapping("/queryUser")public String queryUser(Model model,HttpSession session,@RequestParam(value = "token",required = false) String token){if(token != null){// token有值 说明认证了// TODO 基于token 去服务器获取用户信息session.setAttribute("userLogin","张三");}Object userLogin = session.getAttribute("userLogin");if(userLogin != null){// 说明登录过了,直接放过model.addAttribute("list", Arrays.asList("张三","李四","王五"));return "user";}// 说明没有登录,需要跳转到认证服务器认证 为了能在登录成功后跳回到当前页面,传递参数return "redirect:http://sso.server.com:8080/loginPage?redirect=http://client1.com:8081/queryUser";}
然后我们就可以来访问client1中的服务了
5. client2
控制器逻辑:
@Controller
public class OrderController {@GetMapping("/order")public String getOrder(HttpSession session, Model model){Object userLogin = session.getAttribute("userLogin");if(userLogin != null){// 说明认证了model.addAttribute("list", Arrays.asList("order1","order2","order3"));return "order";}return "redirect:http://sso.server.com:8080/loginPage?redirect=http://client2.com:8082/order";}
}
order.html页面内容:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>$Title$</title>
</head>
<body><h1>订单管理:</h1><ul><li th:each="order:${list}">[[${order}]]</li></ul>
</body>
</html>
通过前面的介绍我们可以发现clent1认证后可以访问了,但是client2提交请求的时候还是会跳转到server服务,做认证的处理。
造成这个的原因是client1认证成功后在Session中保存了认证信息,但是在client2是获取不到的,这时我们可以在Server服务登录成功后在浏览器的Cookie中存储一个token信息,然后在其他服务跳转到要进入登录页面之前的接口服务中判断Cookie中是否有值,如果有则认为是其他服务登录过的,直接放过。
提交请求的时候校验
搞定
三、JWT实现
1.JWT介绍
1.1 什么是JWT
官方:JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA .
JSON Web 令牌(JWT)是一种开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。可以验证和信任此信息,因为它是数字签名的。JWTs 可以使用 secret (使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
通俗的解释:JWT简称 JSON Web Token,也就是JSON形式作为Web应用中的令牌信息,用于在各方之间安全的将信息作为JSON对象传输,在数据传输过程中可以完成数据加密,签名等操作。
1.2 基于Session认证
我们最先接触到的认证方式就是基于Session的认证方式,每一个会话在服务端都会存储在HttpSession中,相当于一个Map,然后通过Cookie的形式给客户端返回一个jsessionid,然后每次访问的时候都需要从HttpSession中根据jsessionid来获取,通过这个逻辑来判断是否是认证的状态。
存在的问题:
- 每个用户都需要做一次记录,而Session一般情况下都会存在内存中,增大了服务器的开销
- 集群环境下Session需要同步,或者分布式Session来处理
- 因为是基于Cookie来传输的,如果Cookie被解惑,用户容易受到CSRF攻击。
- 前后端分离项目中会更加的麻烦
1.3 基于JWT的认证
具体流程如下:
认证的流程:
- 用户通过表单把账号密码提交到后端服务后,如果认证成功就会生成一个对应的Token信息
- 之后用户请求资源都会携带这个Token值,后端获取到后校验通过放行,校验不通过拒绝
jwt的优势:
- 简介:可以通过URL,POST参数或者HTTP header发送,因为数据量小,传输速度快。
- 自包含:负载中包含了所有用户所需的信息,避免多次查询数据
- 夸语音:以JSON形式保存在客户端。
- 不需要服务端保存信息,适合分布式环境。
1.4 JWT的结构
令牌的组成:
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
因此JWT的格式为: xxxx.yyyy.zzzz Header.Payload.Signature
Header:
header通常由两部分组成:令牌的类型【JWT】和所使用的签名算法。例如HMAC、SHA256或者RSA,它会使用 Base64 编码组成 JWT结构的第一部分。注意:Base64是一种编码,是可以被翻译回原来的样子的。
{"alg":"HS256","typ":"JWT"
}
Payload:
令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常是用户信息)和其他数据的声明,它会使用Base64来编码,组成JWT结构的第二部分。
{"userId":"123","userName":"波波烤鸭","admin":true
}
因为会通过Base64编码,所以不要把敏感信息写在Payload中。
Signature:
签名部分,前面两部分都是使用 Base64 进行编码的,即前端可以解开header和payload中的信息,Signature需要使用编码后的 header 和 payload 以及我们提供的一个秘钥,然后使用 header 中指定的前面算法(HS256) 进行签名,签名的作用是保证 JWT 没有被篡改过
2.JWT实现
2.1 JWT基本实现
生成Token令牌
/*** 生成Token信息*/@Testvoid generatorToke() {Map<String,Object> map = new HashMap<>();map.put("alg","HS256");map.put("typ","JWT");Calendar calendar = Calendar.getInstance();calendar.add(Calendar.SECOND,60);String token = JWT.create().withHeader(map) // 设置header.withClaim("userid", 666) // 设置 payload// 设置过期时间.withExpiresAt(calendar.getTime()).withClaim("username", "波波烤鸭") // 设置 payload.sign(Algorithm.HMAC256("qwaszx")); // 设置签名 保密System.out.println(token);}
根据Token来验证是否正确。
/*** 验证Token信息*/@Testpublic void verifier(){String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTMwNTE5ODUsInVzZXJpZCI6NjY2LCJ1c2VybmFtZSI6IuazouazoueDpOm4rSJ9.0LW5MFihMeYNfRfez0a68ncaKQ13j5pSnVZTB7m1CDw";JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("qwaszx")).build();DecodedJWT verify = jwtVerifier.verify(token);System.out.println(verify.getClaim("userid").asInt());System.out.println(verify.getClaim("username").asString());}
验证中场景的异常信息:
- SignatureVerificationException 签名不一致异常
- TokenExpiredException Token过期异常
- AlgorithmMismatchException 算法不匹配异常
- InvalidClaimException 失效的payload异常
2.2 JWT封装
为了简化操作我们可以对上面的操作进一步封装来简化处理
package com.bobo.jwt.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Calendar;
import java.util.Map;/*** JWT操作的工具类*/
public class JWTUtils {private static final String SING = "123qwaszx";/*** 生成Token header.payload.sing 组成* @return*/public static String getToken(Map<String,String> map){Calendar instance = Calendar.getInstance();instance.add(Calendar.DATE,7); // 默认过期时间 7天JWTCreator.Builder builder = JWT.create();// payload 设置map.forEach((k,v)->{builder.withClaim(k,v);});// 生成Token 并返回return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));}/*** 验证Token* @return* DecodedJWT 可以用来获取用户信息*/public static DecodedJWT verify(String token){// 如果不抛出异常说明验证通过,否则验证失败return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);}
}
2.3 SpringBoot应用
首先是在登录方法中,如果登录成功,我们需要生成对应的Token信息,然后将Token信息响应给客户端。
@PostMapping("/login")public Map<String,Object> login(User user){Map<String,Object> res = new HashMap<>();if("zhang".equals(user.getUserName()) && "123".equals(user.getPassword())){// 登录成功Map<String,String> map = new HashMap<>();map.put("userid","1");map.put("username","zhang");String token = JWTUtils.getToken(map);res.put("flag",true);res.put("msg","登录成功");res.put("token",token);return res;}res.put("flag",false);res.put("msg","登录失败");return res;}
然后就是用户提交请求的时候需要携带Token信息,然后我们在controller中处理请求之前需要对token做出校验。如果验证通过就继续处理请求,否则就拦截该请求。
@PostMapping("/queryUser")public Map<String,Object> queryUser(@RequestParam("token") String token){// 获取用信息之前校验Map<String,Object> map = new HashMap<>();try{DecodedJWT verify = JWTUtils.verify(token);map.put("state",true);map.put("msg","请求成功");return map;}catch (SignatureVerificationException e){e.printStackTrace();map.put("msg","无效签名");}catch (TokenExpiredException e){e.printStackTrace();map.put("msg","Token过期");}catch (AlgorithmMismatchException e){e.printStackTrace();map.put("msg","算法不一致");}catch (Exception e){e.printStackTrace();map.put("msg","Token无效");}map.put("state",false);return map;}
但是上面的情况我们看到在controller中添加了大幅度的Token校验的代码,增大的冗余代码,这时我们可以考虑把Token校验的代码放在拦截器中处理。我们创建一个自定义的拦截器.
/*** 自定义的拦截器* 对特定的情况校验是否携带的有Token信息,如果不携带直接拒绝* 然后对Token校验合法性*/
public class JWTInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getParameter("token");// 获取用信息之前校验Map<String,Object> map = new HashMap<>();try{DecodedJWT verify = JWTUtils.verify(token);return true;}catch (SignatureVerificationException e){e.printStackTrace();map.put("msg","无效签名");}catch (TokenExpiredException e){e.printStackTrace();map.put("msg","Token过期");}catch (AlgorithmMismatchException e){e.printStackTrace();map.put("msg","算法不一致");}catch (Exception e){e.printStackTrace();map.put("msg","Token无效");}map.put("state",false);// 把Map转换为JSON响应String json = new ObjectMapper().writeValueAsString(map);response.setContentType("application/json;charset=UTF-8");response.getWriter().println(json);return false;}
}
要让拦截器生效我们还需要添加对应的配置类。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/queryUser") // 需要拦截的请求.addPathPatterns("/saveUser") // 需要拦截的请求.excludePathPatterns("/login"); // 需要排除的请求}
}
然后添加一个测试的方法 /saveUser
@PostMapping("/saveUser")public String saveUser(){System.out.println("------------>");return "success";}
测试访问,把过期时间缩短到1分钟
正常的访问
Token过期后再访问
搞定
相关文章
- Vulkan-着色器及编译SPIR-V
1.着色器模块介绍 Vulkan着色器代码一定要用字节码格式,而不是人类可读的语法如GLSL和HLSL。这个字节码就是SPIR-V,设计用于Vulkan和OpenCL。这是一个可以用于编写图形和计算着色器的格式,但是我们主要关注的是Vulkan的图形管线。使用字节码格…...
2023/9/29 12:32:57 - SpringCloud + SpringGateway 解决Get请求传参为特殊字符导致400无法通过网关转发的问题
title: “SpringCloud SpringGateway 解决Get请求传参为特殊字符导致400无法通过网关转发的问题” createTime: 2021-11-24T10:27:5708:00 updateTime: 2021-11-24T10:27:5708:00 draft: false author: “Atomicyo” tags: [“tomcat”] categories: [“java”] description: …...
2023/9/29 12:29:49 - 软件测试基础学习
注意: 各位同学们,今年本人求职目前遇到的情况大体是这样了,开发太卷,学历高的话优势非常的大,公司会根据实际情况考虑是否值得培养(哪怕技术差一点);学历稍微低一些但是技术熟练的…...
2023/9/29 12:27:31 - 自学成为一名黑客(自学笔记)
前言: 想自学网络安全(黑客技术)首先你得了解什么是网络安全!什么是黑客 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“…...
2023/9/29 12:18:42 - 最快的包管理器--pnpm创建vue项目完整步骤
1.用npm全局安装pnpm npm install -g pnpm 2.在要创建vue项目的包下进入cmd,输入: pnpm create vue 3.输入项目名字,选择Router,Pinia,ESLint,Prettier之后点确定 4.cd到创建好的项目 ,安装依赖 cd .\刚创建好的项目名称\ p…...
2023/9/29 12:16:14 - C#,数值计算——Ranbyte的计算方法与源程序
1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Generator for random bytes using the algorithm generally known as RC4. /// </summary> public class Ranbyte { private int[] s { get; set; } n…...
2023/9/29 12:08:19
最新文章
- Vulkan-着色器及编译SPIR-V
1.着色器模块介绍 Vulkan着色器代码一定要用字节码格式,而不是人类可读的语法如GLSL和HLSL。这个字节码就是SPIR-V,设计用于Vulkan和OpenCL。这是一个可以用于编写图形和计算着色器的格式,但是我们主要关注的是Vulkan的图形管线。使用字节码格…...
2023/9/29 12:32:57 - SpringCloud + SpringGateway 解决Get请求传参为特殊字符导致400无法通过网关转发的问题
title: “SpringCloud SpringGateway 解决Get请求传参为特殊字符导致400无法通过网关转发的问题” createTime: 2021-11-24T10:27:5708:00 updateTime: 2021-11-24T10:27:5708:00 draft: false author: “Atomicyo” tags: [“tomcat”] categories: [“java”] description: …...
2023/9/29 12:29:49 - 软件测试基础学习
注意: 各位同学们,今年本人求职目前遇到的情况大体是这样了,开发太卷,学历高的话优势非常的大,公司会根据实际情况考虑是否值得培养(哪怕技术差一点);学历稍微低一些但是技术熟练的…...
2023/9/29 12:27:31 - 自学成为一名黑客(自学笔记)
前言: 想自学网络安全(黑客技术)首先你得了解什么是网络安全!什么是黑客 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“…...
2023/9/29 12:18:42 - 最快的包管理器--pnpm创建vue项目完整步骤
1.用npm全局安装pnpm npm install -g pnpm 2.在要创建vue项目的包下进入cmd,输入: pnpm create vue 3.输入项目名字,选择Router,Pinia,ESLint,Prettier之后点确定 4.cd到创建好的项目 ,安装依赖 cd .\刚创建好的项目名称\ p…...
2023/9/29 12:16:14 - C#,数值计算——Ranbyte的计算方法与源程序
1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Generator for random bytes using the algorithm generally known as RC4. /// </summary> public class Ranbyte { private int[] s { get; set; } n…...
2023/9/29 12:08:19 - 国内可使用chatGPT的十三种方式
国内AI 1. 开放猫 Chat机器人https://mirrorchat.extkj.cn/ chat机器人: Chat机器人https://mirrorchat.extkj.cn/ 3.免费学习测试 免费学习测试https://chat.wuguokai.cn/#/chat/1683348236237 4.AI文本工具站 AI文本工具站一个用于提高工作效率的文本工具网站,应用…...
2023/8/14 13:04:36 - 基于ChatGPT3.5 API实现的私有化web程序源码+使用说明,一键部署属于自己定制化的 chatgpt web 程序
chatgpt-web 本项目可以一键部署属于自己定制化的 chatgpt web 程序(兼容gpt3.5), 只需下载release中对应平台的项目文件,修改配置后执行,打开 http://127.0.0.1:8080 ,便可以获得属于自己的chatgpt网站。 参考项目:co…...
2023/8/14 19:59:25 - ChatGPT Plus用户专享:86款高效功能插件,详尽安装与使用全攻略
在前天的文章中,我们介绍了 ChatGPT 开放的全新模式 Web Browsing(网页浏览),启用后 ChatGPT 就可以开始上网,收集最新的互联网资料进行作答。 其他关于chatgpt使用方面:请访问: 链接…...
2023/8/14 10:16:53 - ChatGPT自然语言处理的新里程碑
ChatGPT中文网是一个面向中国用户的聊天机器人网站,旨在为国内用户提供一个自然的环境、有趣、实用的聊天体验。它使用最新的自然语言处理技术来帮助用户更好地理解他们的聊天对话,还可以帮助用户解决日常生活中的问题,提供有趣的谈话内容以及…...
2023/8/15 8:22:45 - 国内版ChatGPT最全使用方法及使用用途技巧汇总
ChatGPT人工智能技术的出现确实会让一些人担心自己的工作会不会被取代。但实际上,人工智能技术只会替代那些可以被程序自动化的重复性、标准化、无脑力的工作,而对于需要人类创意、想象力和复杂思维的工作来说,AI人工智能技术的发展对于人类来…...
2023/8/14 10:55:47 - ChatGPT和Midjourney王炸组合,开启AI新时代
目录 序言 一:使用ChatGPT进行对话 二:调用newbies robot 三:举例说明 四:付费和使用限制 序言 随着人工智能技术的不断发展,越来越多的人开始使用人工智能工具来创作图画。在这里,我将分享如何结合Ch…...
2023/8/15 10:03:43