目录

一、理论

1.SSO

2.JWT

#.组成

#.如何工作

3.Redis   RSA   MD5

4.AOP

二、实现过程

#.准备工作

#.登录

#.测试类

#.插拔式注解

#.测试



最近有机会接触到了单点登录,写一篇文章记录一下整个实现的流程。

技术名词

  1. SSO (SingleSignOn 单点登录)
  2. JWT(Json web token 一种认证协议)
  3. Redis(Remote Dictionary Server 远程字典、非关系型数据库、高速缓存中间件)
  4. AOP(Aspect Oriented Programming 面向切面编程)
  5. RSA  MD5 (加密算法)

一、理论

1.SSO

[图片来源于网络,版权归原作者所有,如有侵权,请告知立即删除]

举一些例子:登录了淘宝,进入之后点击天猫不用重复登录。最常用的百度,只要登陆了,进入其他应用如百度文库、贴吧等都是不用重复登录的。

总结:一处登录,处处登录,一处注销,处处注销。

再详细点:客户端请求需要携带TOKEN(内有用户信息等)才能访问一些登录后才能访问的URL,如果不传递TOKEN或者是过期以及被篡改都是不能通过校验的,就会让用户重新登录,登录成功之后服务器会返回TOKEN给客户端,客户端只要携带这个TOKEN就可以访问其他分布式的系统(这些系统都可以识别TOKEN),如果注销,TOKEN被删除,则会要求用户重新登录。

2.JWT

#.组成

格式:xxx.yyy.zzz

  • Header

    • 两部分组成:token的类型("JWT")和算法名称(如:SHA256或者RSA)
    • 用Base64对这个JSON编码就得到JWT的第一部分
  • Payload
    • 它包含声明(要求)声明有三种类型: registered, public 和 private。
    • registered :预定义声明,不是强制的,一般jwt会包含创建时间以及销毁时间
    • public:可以定义你想存到jwt里的信息,如用户的id、userName、email等,一般不存敏感信息,除非你通过加密让此这部分信息变得可靠。
    • 对payload进行Base64编码就得到JWT的第二部分
  • Signature
    • 这是一个签名,拥有防篡改等功能,三个组成部分
    • base64UrlEncode(header)      →  x
    • base64UrlEncode(payload)     →  y
    • secret   →  z
    • 以上三部分作为参数,通过签名算法(header中的算法名称)对它们进行签名(x与y通过 . 连接,加盐z 组合加密),如 HMACSHA256(x+"."+y , z)

#.如何工作

在认证的时候,用户通过凭证成功登陆之后,服务器会返回一个一个jwt(TOKEN)给前端,在此之后,它便是用户的凭证了,在每一次的访问上,可以在请求的Header上携带这个凭证,在服务器上受保护的路由会验证此TOKEN。别忘了一点,jwt是可以携带信息的,如用户的账户类型,通过此也可以做权限管理,如果jwt携带足够多的数据,可以减少不必要的数据库访问。

3.Redis   RSA   MD5

Redis是什么就不介绍了,主要说一下我在实现的过程中充当的作用。

  • 存储RSA公私钥

    • 公私钥需要不断变化,使用户登录输入的密码在前端加密之后不会每一次都相同
  • 存储用户的TOKEN
    • 用户的凭证验证通过Redis
    • 设置TOKEN的有效期

4.AOP

面向切面编程,通过阅读代码能够获得更好的理解。

二、实现过程

#.准备工作

创建一个授权中心(一个普通的SpringBoot项目即可)

按照正常的创建流程创建好之后,创建LoginController用作入口。

  • 引入jedis并配置连接池

    • 引入相关的包:redis.client.jedis
    • 修改配置文件
      • #带有"#"符号的需要手动填写
        spring:redis:database: 0
        #    host: 000.000.000.000
        #    port: 6379connect-timeout: 5000jedis:pool:max-active: 8max-idle: 8min-idle: 0
    • 添加配置文件,附配置文件:JedisConfig.java
    • 当然还需要有一个redis的服务
  • 配置JWT
    • 引入相关的包  jjwt-api、jjwt-impl、jjwt-jackson、commons-codec
    • 修改配置文件
      • #保持所有配置文件相同即可
        jwt:secret: adsfdsfsdfdsfwetrwgfsdfsdfwsEFSEAFESF# 有效期,单位秒expire-time-in-second: 10000
    • 添加配置文件,附配置文件:JwtOperator.java
  • DTO:LoginDTO.java
  • Result:Result.java
  • ParamNameEnum:ParamNameEnum.java

#.登录

package com.shixin.security.controller;
/*** @Description* @Author shixin* @Date 2021/4/23 23:38*/
@Slf4j
@RestController
@RequestMapping("/login")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class LoginController {private final PcUserMapper pcUserMapper;private final JedisPool jedisPool;private final JwtOperator jwtOperator;private static String RSA_PUBLIC_KEY = "RSA_PUBLIC_KEY";private static String RSA_PRIVATE_KEY = "RSA_PRIVATE_KEY";@Value("${jwt.expire-time-in-second}")private Integer EXPIRATION_TIME_IN_SECOND;/*** @Description: 创建RSA秘钥对 返回公钥给前端 redis保存密钥对,主要作用为不断更改用户使用*               相同数据加密后的结果,使得数据难以伪造* @Date: 2021/4/24 11:52*/@PostMapping("/refreshKey")public Result<String> getRSA() throws NoSuchAlgorithmException {Jedis jedis = jedisPool.getResource();Map<String, String> keyMap = RSAUtil.genKeyPair(RSA_PUBLIC_KEY,RSA_PRIVATE_KEY);String publicKey = keyMap.get(RSA_PUBLIC_KEY);String privateKey = keyMap.get(RSA_PRIVATE_KEY);jedis.set(RSA_PUBLIC_KEY,publicKey);jedis.set(RSA_PRIVATE_KEY, privateKey);return Result.success("请求成功",publicKey);}/*** @Description: 传递公钥加密过的密码,后端解密后用MD5J加密和数据库面对比* @Date: 2021/4/24 11:48*/@PostMapping("/check")public Result<LoginDTO> login(@RequestBody PcUser param) throws Exception {Jedis jedis = jedisPool.getResource();//校验空值if (!StringUtils.isBlank(param.getUserId()) && !StringUtils.isBlank(param.getPassword())){//获取信息,没有连接数据库可以自行伪造数据PcUser pcUser = pcUserMapper.selectByUserId(param.getUserId());//通过redis获取的私钥对前端传过来的用公钥加密过的密码进行解密String password = RSAUtil.decrypt(param.getPassword(), jedis.get(RSA_PRIVATE_KEY));//通过getSaltverifyMD5这个方法可以校验数据库密文的明文是否是password,如果是,就可以开始颁发TOKEN了if (pcUser != null && MD5Util.getSaltverifyMD5(password,pcUser.getPassword())){//构件jwt第二部分的public部分,如果不知道什么意思的可以看理论-JWT部分的介绍Map<String,Object> userInfo = new HashMap<>(5);userInfo.put("id",pcUser.getId());userInfo.put("type",pcUser.getType());userInfo.put("userName",pcUser.getUserName());userInfo.put("email",pcUser.getEmail() == null ? "":pcUser.getEmail());userInfo.put("avatar",pcUser.getAvatar() == null ? "":pcUser.getAvatar());//生成TOKENString token = jwtOperator.generateToken(userInfo);//将TOKEN存到Redis里面,格式:TOKEN_[userId] : TOKENjedis.setex(ParamNameEnum.TOKEN_ +pcUser.getUserId(),EXPIRATION_TIME_IN_SECOND,token);//构建返回给前端的数据,在此之后,前端的请求都会携带token访问,只要token验证通过,便会放行return Result.success(LoginDTO.builder().token(token).userInfo(LoginDTO.UserInfo.builder().id(pcUser.getId()).type(pcUser.getType()).userId(pcUser.getUserId()).userName(pcUser.getUserName()).avatar(pcUser.getAvatar() == null ? "" : pcUser.getAvatar()).email(pcUser.getEmail() == null ? "" : param.getEmail()).build()).build());}else {return Result.fail("账号或密码错误");}}else {return Result.fail("用户或密码为空");}}
}

#.测试类

package com.shixin.pawcode.admin.controller;
/*** @Description* @Author shixin* @Date 2021/4/24 22:15*/
@Slf4j
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {private final JedisPool jedisPool;@Value("${jwt.expire-time-in-second}")private Integer EXPIRATION_TIME_IN_SECOND;@GetMapping("/checkAspect")//这个注解的编写后面会写到,这就是上面提到过的AOP编程,进入此方法的请求都会被拦截@CheckLoginpublic String checkAspect(HttpServletRequest request, HttpServletResponse response){//处理业务逻辑//获取经过注解处理之后在request添加的TOKEN,怎么处理,下面有介绍String token = (String) request.getAttribute(ParamNameEnum.L_TOKEN.name());if (!StringUtils.isBlank(token)){Jedis jedis = jedisPool.getResource();//在业务逻辑处理完毕并且不出错的情况下才刷新TOKEN,否则还是使用原来的TOKENresponse.setHeader(ParamNameEnum.L_TOKEN.name(),token);jedis.setex(ParamNameEnum.TOKEN_.name() + request.getAttribute("userId"),EXPIRATION_TIME_IN_SECOND,token);}return token;}
}

#.插拔式注解

CheckLogin.java

package com.shixin.common.auth;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
}

CheckLoginAspect.java

package com.shixin.common.auth;
/*** @Description 只要添加了CheckLogin注解的都会经过此方法* @Author shixin* @Date 2021/4/24 21:46*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CheckLoginAspect {private final JwtOperator jwtOperator;private final JedisPool jedisPool;@Value("${jwt.expire-time-in-second}")private Integer EXPIRATION_TIME_IN_SECOND;//如果不了解此注解的作用可以自行百度关于AOP切面编程,注意里的参数要填写注解的包名全路径@Around("@annotation(com.shixin.common.auth.CheckLogin)")public Object checkLogin(ProceedingJoinPoint point) throws Throwable {//1.从header里获取tokenRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = attributes.getRequest();//这里要去redis校验 并且在业务完成后刷新过期时间 token会变//在前端传输的过程中,我设定多传了一个ID,用于创建Redis的key和获取,可以直接通过id和token来确定是否登录String token = request.getHeader(ParamNameEnum.L_TOKEN.name());String userId = request.getHeader(ParamNameEnum.U_ID.name());//校验token 是否和过期Jedis jedis = jedisPool.getResource();String redisToken = jedis.get(ParamNameEnum.TOKEN_.name()+ userId);if (!StringUtils.isBlank(redisToken) || !StringUtils.isBlank(token)) {if (!redisToken.equals(token) ) {  // || !jwtOperator.validateToken(token) 这个是校验是否合法,redis里存的jwt本身就是合法的了,而且有效期也有,所以可以省略这个校验//自定义异常类,自己随意创建throw new SecurityException("Token不合法");} else {//添加用户信息到attributeClaims claims = jwtOperator.getClaimsFromToken(token);Map<String,Object> userInfo = new HashMap<>(5);Object id = claims.get("id");request.setAttribute("id", id);userInfo.put("id",id);Object type = claims.get("type");request.setAttribute("type", type);userInfo.put("type",type);Object userName = claims.get("userName");request.setAttribute("userName", userName);userInfo.put("userName",userName);Object email = claims.get("email") == null ? "" : claims.get("email");request.setAttribute("email", email);userInfo.put("email",email);Object avatar = claims.get("avatar") == null ? "" : claims.get("avatar");request.setAttribute("avatar", avatar);userInfo.put("avatar",avatar);//生成新的TOKENString newToken = jwtOperator.generateToken(userInfo);//通过request将TOKEN传递给方法request.setAttribute(ParamNameEnum.L_TOKEN.name(), newToken);//执行被注解的方法return point.proceed();}} else {throw new SecurityException("用户未登录");}}
}

#.测试

假定:本地测试,端口8083

  • 访问localhost:8083/login/refreshKey

    • 生成一对公私钥
  • 访问localhost:8083/login/check
    • 也就是登录接口
    • 通过postman测试的要让密码先用第一步生成的RSA公钥加密再传输
    • 此方法的返回结果会包含一个token
  • 携带上一步返回的token以及userId访问用来测试的路由localhost:8083/checkAspect
    • 复制返回值(②)中的TOKEN到请求体的TOKEN(①)中,重复这个操作
    • 实现每次访问本站都可以免登陆所设定的最长时长

JWT实现单点登录(SSO)相关推荐

  1. JWT实现单点登录(sso)功能

    单点登录描述: 单点登录主要时应用在微服务架构中,在任意一个子服务中输入用户的用户名,密码进行登录时, 在跳转到其他系统的时候,就无需在进行登录,直接可以识别出用户的身份,权限以及角色等信息 . . ...

  2. SpringBoot+OAuth2+JWT实现单点登录SSO完整教程,竟如此简单优雅!

    作者:狂乱的贵公子 来源:https://www.cnblogs.com/cjsblog/p/10548022.html 1.前言 技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题, ...

  3. 基于Spring Security与JWT实现单点登录

    基于RBAC的权限管理 RBAC(Role-Based Access Control):基于角色的访问控制 当前项目中,RBAC具体的表现为: 管理员表:ams_admin 角色表:ams_role ...

  4. jwt单点登录_单点登录SSO技术选型

    一些人存在的意义总归是让另一些人成长,然后消失. --刘同<谁的青春不迷茫> 1.单点登录是什么? 单点登录主要用于多系统集成,即在多个系统中,用户只需要到一个中央服务器登录一次即可访问这 ...

  5. jwt认证机制优势和原理_最详细的Spring Boot 使用JWT实现单点登录

    Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(S ...

  6. OAuth2 实现单点登录 SSO

    转载自  OAuth2 实现单点登录 SSO 1. 前言 技术这东西吧,看别人写的好像很简单似的,到自己去写的时候就各种问题,"一看就会,一做就错".网上关于实现SSO的文章一大堆 ...

  7. 单点登录SSO(Single Sign On)

    文章目录 一.什么是Session 跨域问题 二.Token 机制 1.传统身份认证 2.Token 身份认证 三.Session跨域共享实现方案 1.Nginx Session共享 2.Spring ...

  8. OAuth2实现单点登录SSO

    本文转载自:https://www.cnblogs.com/cjsblog/p/10548022.html OAuth2实现单点登录SSO 1.  前言 技术这东西吧,看别人写的好像很简单似的,到自己 ...

  9. 浅谈单点登录SSO实现方案 | StartDT Tech Lab 06

    写在前面 这是奇点云全新技术专栏「StartDT Tech Lab」的第6期. 在这里,我们聚焦数据技术,分享方法论与实战.一线的项目经历,丰富的实践经验,真实的总结体会-滑到文末,可以看到我们的往期 ...

最新文章

  1. spring boot 2.0 集成shiro注意事项
  2. 学习使用Bing Maps Silverlight Control(一):准备和新建
  3. node.js Promise简单介绍
  4. 前端学习(2465):ajax发送请求
  5. SendMessage、PostMessage原理和源代码详解
  6. 病毒周报(100301至100307)
  7. (一)golang工作区
  8. spring中定时器的使用
  9. php环境模拟stphp_一个模拟浏览器请求的php类,模拟请求ua设置
  10. 好好学习 天天编程—C语言之我的第一个hello world(二)
  11. 计算机无法连接声音怎么办,电脑耳机没声音怎么设置|耳机插电脑没有声音解决方法...
  12. 淘宝带你走进——幽灵Crash迷踪案
  13. xxl-job集群原理
  14. PCB焊接——原理篇
  15. 贝壳找房《2018城市居住报告》:新一线租房量持续攀升
  16. CPU的主频/核心数
  17. 这几个网站的使用技巧,值得反复读,反复练~
  18. 聊聊C++任务定时器的设计与具体实现
  19. php微信小程序服务商支付模式
  20. Vue: Runtime-Compiler和Runtime-only的区别

热门文章

  1. 设计模式之工厂设计模式及抽象工厂设计模式
  2. 多重背包的优化 二进制/单调队列解析
  3. 树的概念:层次、高度、深度、宽度
  4. 纯JavaScript入门级小游戏:兔子抢金币(附演示地址+源码)
  5. cd40系列芯片_cd40110的工作原理详细(cd40110引脚图功能_如何计数及应用电路分享) - 全文...
  6. 华硕(ASUS)枪神系列出厂系统win10/11原厂OEM系统
  7. 文正机械电子工程专业课_机械电子工程专业解读_机械电子工程专业介绍_机械电子工程专业开设课程-高考圈...
  8. 百度贴吧--------签到程序
  9. Python 列表(list)
  10. 【SQL】查询表中姓名“王”开头,并且只有二个字的数据