Springboot实现Shiro整合JWT的示例代码

写在前面

之前想尝试把JWT和Shiro结合到一起,但是在网上查了些博客,也没太有看懂,所以就自己重新研究了一下Shiro的工作机制,然后自己想了个(傻逼)办法把JWT和Shiro整合到一起了

另外接下来还会涉及到JWT相关的内容,我之前写过一篇博客,可以看这里:Springboot实现JWT认证

Shiro的Session机制

由于我的方法是改变了Shiro的默认的Session机制,所以这里先简单讲一下Shiro的机制,简单了解Shiro是怎么确定每次访问的是哪个用户的

Servlet的Session机制

Shiro在JavaWeb中使用到的就是默认的Servlet的Session机制,大致流程如下:

1.用户首次发请求

2.服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个Session用来储存该用户的信息,然后生成SessionId作为对应的Key

3.服务器会在响应中,用jsessionId这个名字,把这个SessionId以Cookie的方式发给客户(就是Set-Cookie响应头)

4.由于已经设置了Cookie,下次访问的时候,服务器会自动识别到这个SessionId然后找到你上次对应的Session

Shiro带来的变化

而结合Shiro之后,上面的第二步和第三步会发生小变化:

2.—>服务器不但会创建Session,还会创建一个Subject对象(就是Shiro中用来代表当前用户的类),也用这个SessionId作为Key绑定

3.—>第二次接受到请求的时候,Shiro会从请求头中找到SessionId,然后去寻找对应的Subject然后绑定到当前上下文,这时候Shiro就能知道来访的是谁了

我的思路

由于这个是我自己想出来的,所以可能会存在一定的问题,还请大佬指点

主要思想是:用JWT Token来代替Shiro原本返回的Session

工作流程:

  • 用户登录
  • 若成功则shiro会默认生成一个SessionId用来匹配当前Subject对象,则我们将这个SessionId放入JWT中
  • 返回JWT
  • 用户第二次携带JWT来访问接口
  • 服务器解析JWT,获得SessionId
  • 服务器把SessionId交给Shiro执行相关认证

代码实现

导入JWT相关包

导入java-jwt包:

这个包里实现了一系列jwt操作的api(包括上面讲到的怎么校验,怎么生成jwt等等)

如果你是Maven玩家:

pom.xml里写入

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.8.3</version>
</dependency>

如果你是Gradle玩家:

build.gradle里写入

compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'

如果你是其他玩家:

maven中央仓库地址点这里

JWT工具类

JwtUtils,代码如下:

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;

/**
 * @author Lehr
 * @create: 2020-02-04
 */
public class JwtUtils {

  /**
   签发对象:这个用户的id
   签发时间:现在
   有效时间:30分钟
   载荷内容:暂时设计为:这个人的名字,这个人的昵称
   加密密钥:这个人的id加上一串字符串
   */
  public static String createToken(String userId,String realName, String userName) {

    Calendar nowTime = Calendar.getInstance();
    nowTime.add(Calendar.MINUTE,30);
    Date expiresDate = nowTime.getTime();

    return JWT.create().withAudience(userId)  //签发对象
        .withIssuedAt(new Date())  //发行时间
        .withExpiresAt(expiresDate) //有效时间
        .withClaim("userName", userName)  //载荷,随便写几个都可以
        .withClaim("realName", realName)
        .sign(Algorithm.HMAC256(userId+"HelloLehr"));  //加密
  }

  /**
   * 检验合法性,其中secret参数就应该传入的是用户的id
   * @param token
   * @throws TokenUnavailable
   */
  public static void verifyToken(String token, String secret) throws TokenUnavailable {
    DecodedJWT jwt = null;
    try {
      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"HelloLehr")).build();
      jwt = verifier.verify(token);
    } catch (Exception e) {
      //效验失败
      //这里抛出的异常是我自定义的一个异常,你也可以写成别的
      throw new TokenUnavailable();
    }
  }

  /**
  * 获取签发对象
  */
  public static String getAudience(String token) throws TokenUnavailable {
    String audience = null;
    try {
      audience = JWT.decode(token).getAudience().get(0);
    } catch (JWTDecodeException j) {
      //这里是token解析失败
      throw new TokenUnavailable();
    }
    return audience;
  }

  /**
  * 通过载荷名字获取载荷的值
  */
  public static Claim getClaimByName(String token, String name){
    return JWT.decode(token).getClaim(name);
  }
}

一点小说明:

关于jwt生成时的加密和验证方法:

jwt的验证其实就是验证jwt最后那一部分(签名部分)。这里在指定签名的加密方式的时候,还传入了一个字符串来加密,所以验证的时候不但需要知道加密算法,还需要获得这个字符串才能成功解密,提高了安全性。我这里用的是id来,比较简单,如果你想更安全一点,可以把用户密码作为这个加密字符串,这样就算是这段业务代码泄露了,也不会引发太大的安全问题(毕竟我的id是谁都知道的,这样令牌就可以被伪造,但是如果换成密码,只要数据库没事那就没人知道)

关于获得载荷的方法:

可能有人会觉得奇怪,为什么不需要解密不需要verify就能够获取到载荷里的内容呢?原因是,本来载荷就只是用Base64处理了,就没有加密性,所以能直接获取到它的值,但是至于可不可以相信这个值的真实性,就是要看能不能通过验证了,因为最后的签名部分是和前面头部和载荷的内容有关联的,所以一旦签名验证过了,那就说明前面的载荷是没有被改过的。

Controller层

登录逻辑

  /**
   * 用户登录
   * @param userName
   * @param password
   * @param req
   * @return
   * @throws Exception
   */
  @SneakyThrows
  @PostMapping(value = "/login")
  public AccountVO login(String userName, String password, HttpServletRequest req){
    //尝试登录
    Subject subject = SecurityUtils.getSubject();
    try {
      subject.login(new UsernamePasswordToken(userName, password));
    } catch (Exception e) {
      throw new LoginFailed();
    }
    AccountVO account = accountService.getAccountByUserName(userName);
    String id = account.getId();
    //生成jwtToken
    String jwtToken = JwtUtils.createToken(id, account.getRealName(),account.getUserName(), subject.getSession().getId().toString());
    //设置好token,后来会在全局处理的时候放入响应里
    req.setAttribute("token", jwtToken);
    return account;
  }

主要是:在登录成功之后把这个Subject的SessionId放入JWT然后生成token:

String jwtToken = JwtUtils.createToken(id,account.getRealName(),account.getUserName(),subject.getSession().getId().toString());

以后我们就可以通过解析JWT来获取SessionId了,而不是每次把SessionId作为Cookie返回

退出逻辑

首先,由于JWT令牌本身就会失效,所以如果JWT令牌失效,也就相当与退出了

然后我们还可以同样实现Shiro中传统的手动登出:

public String logout(HttpServletRequest req) {
    SecurityUtils.getSubject().logout();
    return "用戶已经安全登出";
  }

这样的话Realm中的用户状态就变成未认证了,就算JWT没过期也需要重新登录了

自定义SessionManager

先上代码:

package com.imlehr.internship.shiroJwt;

import com.imlehr.internship.exception.TokenUnavailable;
import lombok.SneakyThrows;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.UUID;

/**
 * @author Lehr
 * @create: 2020-02-10
 */
public class CustomSessionManager extends DefaultWebSessionManager {

  //这里我为了省事用了lombok的标签
  @SneakyThrows
  @Override
  protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

    String token = WebUtils.toHttp(request).getHeader("token");
    System.out.println("会话管理器得到的token是:" + token);
    if (token == null || token.length()<1) {
      return UUID.randomUUID().toString();
    }

    //在这里验证一下jwt了,虽然我知道这样不好
    String userId = JwtUtils.getAudience(token);
    JwtUtils.verifyToken(token, userId);
    String sessionId = JwtUtils.getClaimByName(token, "sessionId").asString();

    if (sessionId == null) {
      return new TokenUnavailable();
    }

    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
    request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
    request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());

    return sessionId;
  }

}

之前的Session的获取,就是在DefaultWebSessionManager里实现的,所以我们现在只需要重写这个类,把我们如何获取Session的逻辑写进去就好了

这里说两个方法:

getSessionId(SessionKey key)

这个方法是在DefaultWebSessionManager的,这里并没有重写,我们上面重写的是后面第二个同名方法,只是想在这里谈谈,读者可以直接跳过这段也不影响

源码逻辑

在Shiro想要获取SessionId的时候,首先会调用的就是这个方法,而不是那个传入httpRequest的方法

在DefaultWebSessionManager中,他是这样做的

@Override
public Serializable getSessionId(SessionKey key) {
  Serializable id = super.getSessionId(key);
  if (id == null && WebUtils.isWeb(key)) {
    ServletRequest request = WebUtils.getRequest(key);
    ServletResponse response = WebUtils.getResponse(key);
    //调用第二个同名方法
    id = getSessionId(request, response);
  }
  return id;
}
  • 如果没能找到id,就调用第二个同名方法
  • 如果有,就返回

这里需要注意的是,这个方法会在整个验证过程中多次被反复调用,而在服务器接受到用户请求的时候,只会调用一次的方法是下面这个,也就是我们重写的这个

getSessionId(ServletRequest request, ServletResponse response)

这个才是真正涉及到服务器接受到请求的时候获取Session逻辑,从用户的请求报文中获取SessionId

所以我们要重写的就是这一步

原版中的逻辑是:从Cookie里找到sessionId的值

我们只需要把逻辑该为:从Header中找出JWT(也就是从请求头的'token'头中找),然后解析JWT,获取到我们存放在其中的SessionId属性即可

ShiroConfiguration

我们只需要把自己写的SessionManager配置进去就好了

首先配好:

public DefaultWebSessionManager sessionManager()
{
  CustomSessionManager customSessionManager = new CustomSessionManager();
  return customSessionManager;
}

然后放入SecurityManager

public SecurityManager securityManager(MyRealm myRealm) {

  DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  securityManager.setRealm(myRealm);

  return securityManager;
}

完成🎉

测试

登录

我们获取到了JWT,JWT里面就带有SessionId

后续请求不带token

显然,没过认证,我们看下后台:

因为不能获得token所以无法得到该用户对应的sessionId,所以被授权拦截了

后面那个JSESSIONID是因为没得到sessionId新生成的,所以对应了一个没有登录的用户,自然就会被拒绝

只有带上之前的token,shiro才会认为我们是之前那个已经登录过的用户

后续请求带token

后台:

成功!

另外,因为JWT本身就适合RESTful API服务,所以,如果把Shiro和Redis整合起来做成分布式的,那么效果会更好

到此这篇关于Springboot实现Shiro整合JWT的示例代码的文章就介绍到这了,更多相关Springboot Shiro整合JWT内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring boot整合shiro+jwt实现前后端分离

    本文实例为大家分享了Spring boot整合shiro+jwt实现前后端分离的具体代码,供大家参考,具体内容如下 这里内容很少很多都为贴的代码,具体内容我经过了看源码和帖子加了注释.帖子就没用太多的内容 先下载shiro和jwt的jar包 <!-- shiro包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId

  • Shiro + JWT + SpringBoot应用示例代码详解

    1.Shiro的简介 Apache Shiro是一种功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理,可用于保护 从命令行应用程序,移动应用程序到Web和企业应用程序等应用的安全. Authentication 身份认证/登录,验证用户是不是拥有相应的身份: Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限:即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色.或者细粒度的验证某个用户对某个资源是否具有某个权限: Crypto

  • Springboot实现Shiro整合JWT的示例代码

    写在前面 之前想尝试把JWT和Shiro结合到一起,但是在网上查了些博客,也没太有看懂,所以就自己重新研究了一下Shiro的工作机制,然后自己想了个(傻逼)办法把JWT和Shiro整合到一起了 另外接下来还会涉及到JWT相关的内容,我之前写过一篇博客,可以看这里:Springboot实现JWT认证 Shiro的Session机制 由于我的方法是改变了Shiro的默认的Session机制,所以这里先简单讲一下Shiro的机制,简单了解Shiro是怎么确定每次访问的是哪个用户的 Servlet的Se

  • SpringBoot 整合 JMSTemplate的示例代码

    1.1 添加依赖   可以手动在 SpringBoot 项目添加依赖,也可以在项目创建时选择使用 ActiveMQ 5 自动添加依赖.高版本 SpringBoot (2.0 以上) 在添加 activemq 连接池依赖启动时会报 Error creating bean with name 'xxx': Unsatisfied dependency expressed through field 'jmsTemplate'; 可以将 activemq 连接池换成 jms 连接池解决. <depen

  • SpringBoot整合SpringDataRedis的示例代码

      本文介绍下SpringBoot如何整合SpringDataRedis框架的,SpringDataRedis具体的内容在前面已经介绍过了,可自行参考. 1.创建项目添加依赖   创建SpringBoot项目,并添加如下依赖: <dependencies> <!-- springBoot 的启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId

  • SpringBoot集成Auth0 JWT的示例代码

    目录 前言 session认证与Token认证 session认证 Token认证 JWT简介 JWT定义 JWT的类库 具体实现 JWT配置 JWT工具类 测试接口 前言 说说JWT,先说下互联网服务常见的两种用户认证方式: session认证与Token认证 session认证 传统的Session认证的大体流程可以表示为用户提供用户名和密码登录后由服务器存储一份用户登录信息并传递给浏览器保存为Cookie,并在下次请求中根据Cookie来识别用户,但这种方式缺陷明显: Session都是保

  • SpringBoot整合ShardingSphere的示例代码

    目录 一.相关依赖 二.Nacos数据源配置 三.项目配置 四.验证 概要: ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC.Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成. 他们均提供标准化的数据分片.分布式事务和数据库治理功能,可适用于如Java同构.异构语言.云原生等各种多样化的应用场景. 官网地址:https://shardingsphere.apache.org/ 一.相关

  • SpringBoot整合MyBatis-Plus的示例代码

    目录 前言 源码 环境 开发工具 SQL脚本 正文 单工程 POM文件(注意) application.properties(注意) 自定义配置(注意) 实体类(注意) Mapper接口(注意) Service服务实现类(注意) Controller前端控制器(注意) SpringBoot启动类(注意) 启用项目,调用接口(注意) 多工程 commons工程-POM文件 MyBatis-Plus commons工程-system.properties commons工程- 自定义配置 commo

  • springboot 整合sentinel的示例代码

    目录 1. 安装sentinel 2.客户端连接 1. 安装sentinel 下载地址:https://github.com/alibaba/Sentinel/releases/tag/1.7.0 ,由于我无法下载,所以使用docker安装, yuchunfang@yuchunfangdeMacBook-Pro ~ % docker pull bladex/sentinel-dashboard:1.7.0 yuchunfang@yuchunfangdeMacBook-Pro ~ % docker

  • springboot 整合hbase的示例代码

    目录 前言 HBase 定义 HBase 数据模型 物理存储结构 数据模型 1.Name Space 2.Region 3.Row 4.Column 5.Time Stamp 6.Cell 搭建步骤 1.官网下载安装包: 2.配置hadoop环境变量 3.修改 hbase-env.cmd配置文件 4.修改hbase-site.xml 文件 5.启动hbase服务 6.hbase客户端测试 Java API详细使用 1.导入客户端依赖 2.DDL相关操作 3.DML相关操作 插入数据与查询数据 H

  • springboot整合xxl-job的示例代码

    目录 关于xxl-job 调度中心 执行器 关于xxl-job 在我看来,总体可以分为三大块: 调度中心 执行器 配置定时任务 调度中心 简单来讲就是 xxl-job-admin那个模块,配置: 从doc里面取出xxl-job.sql的脚本文件,创建对应的数据库. 进行配置文件的配置,如下图 进行日志存放位置的修改 然后idea打包之后就能当作调度中心运行了 访问地址:ip:port/xxl-job-admin 默认的账号密码:admin/123456 注意:你进去后修改密码,有些浏览器就算你账

  • spring boot整合Swagger2的示例代码

    Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化RESTful风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步.Swagger 让部署管理和使用功能强大的API从未如此简单. 1.代码示例 1).在pom.xml文件中引入Swagger2 <dependency> <groupId>io.springfox</groupId> <artifa

随机推荐