写在前面

之前想尝试把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里写入

com.auth0

java-jwt

3.8.3

如果你是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;

}

完成

java shiro jwt_Springboot实现Shiro整合JWT的示例代码相关推荐

  1. Java Singleton类中的线程安全性的示例代码

    Java Singleton类中的线程安全性的示例代码 Singleton是最广泛使用的创建设计模式之一,用于限制应用程序创建对象.在实际应用程序中,数据库连接或企业信息系统(EIS)等资源是有限的, ...

  2. 纯java pdf转换成html,JAVA实现PDF转HTML文档的示例代码

    本文是基于PDF文档转PNG图片,然后进行图片拼接,拼接后的图片转为base64字符串,然后拼接html文档写入html文件实现PDF文档转HTML文档. 引入Maven依赖 org.apache.p ...

  3. java 静态数据_Java 静态数据初始化的示例代码

    无论创建多少个对象,静态数据都只占用一份存储区域.static关键字不能应用于局部变量,因此它只能作用于域.如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初始值:如 ...

  4. java通过单号判断快递公司的示例代码

    通过单号判断快递公司的示例代码有很多种,以下是快递100Java智能单号判断功能接入. 不过首先要拿到快递100的测试账号和密钥,获取方式只需要去官网注册后,登录后台进入用户信息模块就能看到了. ht ...

  5. java 抛异常给上级_java异常处理机制(示例代码)

    Exception 类的层次 java中所有的异常类是从 java.lang.Exception 类继承的子类. 而Exception 类是 Throwable (可抛出的)类的子类.除了Except ...

  6. java继承类大全_Java 面向对象继承部分(示例代码)

    被继承的类称为父类(超类),继承父类的类称为子类(派生类) 通过继承可以实现代码重用 子类拥有父类非 private 的属性.方法. 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展. 子类可以 ...

  7. java的数值类型举例_Java基本类型(示例代码)

    全新的世界,Hello World 大部分的编程语言都是以"Hello World"开始,它是一个敲门砖,当你用它砸开那道编程大门后,一个全新的世界会展现在你的面前.当踏入一步后, ...

  8. java人名识别_HanLP中人名识别分析(示例代码)

    HanLP中人名识别分析 在看源码之前,先看几遍论文<基于角色标注的中国人名自动识别研究> 关于命名识别的一些问题,可参考下列一些issue: HanLP参考博客: 分词 分词:给定一个字 ...

  9. RabbitMq(九) SpringBoot整合RabbitMQ消费者示例代码

    概述 在上一篇我们介绍了SpringBoot整合RabbitMQ生产者代码,本章我们介绍SpringBoot整合RabbitMQ,实现消费者工程的代码实现.与生产者集成相比,集成消费者不需要进行添加配 ...

最新文章

  1. 金九银十铁12,看完弄懂,工资少说加 5K
  2. java语言修饰符$_Java语言中的修饰符
  3. malloc和free实现的原理
  4. 在python3.x下使用如下代码: import cPickle as pk 报错
  5. Blazor带我重玩前端(五)
  6. 设计模式原则之里氏替换原则
  7. conda虚拟环境中使用pip仍然安装到全局python中
  8. Python生成exe可执行文件的两种方法(py2exe和pyinstaller)
  9. 28. magento使用细节
  10. 32bit64bit Win7系统下的IE8离线升级到IE11方法
  11. linux设计引物探针,如何设计荧光定量PCR的引物及TaqMan探针
  12. with rollup函数做合计以及行转列
  13. 财会法规与职业道德【10】
  14. 服务器装32位还是64位系统,操作系统:32位和64位系统有何区别?
  15. Java 模拟鼠标、键盘--向可编辑窗口录入内容
  16. w10控制面板卸载java_手把手教你解决Win10控制面板无法卸载软件的问题
  17. 2019年培养工作室主力计划——第1次任务
  18. 一个老码农对职场的思考
  19. 计算机编程在哪里学,高中毕业想学计算机编程,不知道从哪开始学起。
  20. ZooKeeper篇:2PC、3PC以及ZAB协议

热门文章

  1. 如何使用pandas正确读取带有中文的cvs文件
  2. Anaconda创建跟别人环境配置一样的虚拟环境(coda env creat -f environment.yml)
  3. 从零开始学Pytorch(八)之Modern CNN
  4. 从零开始学keras之使用预训练的卷积神经网络
  5. 计算机文件自动备份到移动硬盘,如何让电脑文件自动备份到指定的邮箱里?或者备份到指定的磁盘里...
  6. 大华的支持rtmp推流吗_RTSP安防摄像机(海康大华宇视等)如何推送到RTMP流媒体服务器进行直播...
  7. python制作物联网控制软件下载_Python+树莓派制作IoT(物联网)门控设备
  8. 正在播放 html全集视频教程,正在播放:HTML基础视频教程(全集)-网络编程视频-星火视频教程 21edu8.com...
  9. docker修改php.ini,docker部署更改php的upload_max_filesize
  10. 4广联达4代锁安装6.0_Aspen Plus 8.4 软件安装教程