【WEB系列】 Websocket消息拦截器实现聊天提醒

在上一篇文章中,我们成功地为WebSocket的聊天应用添加了身份验证功能。然而,当时遗留了一个关键问题:当一个新用户加入群聊时,我们希望向群聊内的其他成员发送一条欢迎消息,以告知他们有新朋友加入了。那么,如何实现这一需求呢?

接下来,我们将重点介绍如何使用ChannelInterceptor来实现加入/退出群聊的通知功能。

查看更多

分享到

【WEB系列】 WebSocket身份鉴权

上一篇博文介绍了如何利用STOMP和SpringBoot搭建一个能够实现相互通讯的聊天系统。通过该系统,我们了解了STOMP的基本使用方法以及一些基础概念。接下来,我们将在此基础上进行一些增强。由于聊天的本质是交流,因此我们需要知道是谁在与谁进行聊天,这就需要登录功能的支持。

接下来,我们将探讨如何为WebSocket通信添加身份验证功能。

查看更多

分享到

【WEB系列】 WebScoket整合stomp协议实例

在我们的日常工作中,我们可能会遇到需要实现双向通讯的场景。为了解决这个问题,常见的实现方案包括短轮询、长轮询、SSE和WebSocket等几种方式。本文将重点介绍如何通过整合WebSocket和STOMP协议来实现双向通讯的方案, 并给出一个应用实例,带你轻松掌握如何基于SpringBoot搭建一个在线聊天系统

查看更多

分享到

【WEB系列】 压缩返回结果实例演示

本文将介绍一个SpringBoot进阶技巧:压缩返回结果实例演示,旨在提升您的网站访问性能。

当返回的数据较大时,网络开销通常不可忽视。为了解决这个问题,我们可以考虑压缩返回的结果,以减少传输的数据量,从而降低网络开销并提高性能。对于依赖Spring生态的Java开发者来说,幸运的是SpringBoot提供了非常便捷的使用方式。

接下来,我们将介绍几种不同情况下的压缩返回的使用方式:

  • 直接返回文本:使用text/plain作为响应类型。
  • 返回JSON数据:使用application/json作为响应类型。
  • 返回静态资源文件:对于静态资源文件,可以使用压缩算法进行压缩后再返回。

查看更多

分享到

【基础系列】 从零开始:SpringBoot配置动态刷新的详细解析与实践!

关于SpringBoot的自定义配置源、配置刷新之前也介绍过几篇博文;最近正好在使用apollo时,排查配置未动态刷新的问题时,看了下它的具体实现发现挺有意思的;

接下来我们致敬经典,看一下如果让我们来实现配置的动态刷新,应该怎么搞?

查看更多

分享到

【WEB系列】 长整型精度丢失问题修复方案

javascript以64位双精度浮点数存储所有Number类型值,即计算机最多存储64位二进制数。 但是需要注意的是Number包含了我们常说的整形、浮点型,相比较于整形而言,会有一位存储小数点的偏移位,由于存储二进制时小数点的偏移量最大为52位,计算机存储的为二进制,而能存储的二进制为62位,超出就会有舍入操作,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53),十进制即9007199254740992 大于9007199254740992的可能会丢失精度

因此对于java后端返回的一个大整数,如基于前面说到的雪花算法生成的id,前端js接收处理时,就可能出现精度问题

接下来我们以Thymeleaf模板渲染引擎,来介绍一下对于大整数的精度丢失问题的几种解决方案

查看更多

分享到

【DB系列】 自定义雪花算法生成分布式id

系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,比如常见的基于数据库自增主键生成的id,随机生成的uuid,亦或者redis自增的计数器等都属于常见的解决方案;本文我们将会重点看一下业界内大名鼎鼎的雪花算法,是如何实现分布式id的

查看更多

分享到

【DB系列】 SQL执行日志打印的几种方式

sql日志打印,再我们日常排查问题时,某些时候帮助可以说是非常大的,那么一般的Spring项目中,可以怎么打印执行的sql日志呢?

本文将介绍三种sql日志打印的方式:

  1. Druid打印sql日志
  2. Mybatis自带的日志输出
  3. 基于拦截器实现sql日志输出

查看更多

分享到

【WEB系列】 基于JWT的用户鉴权实战

再传统的基于session的用户身份认证方式之中,用户相关信息存储与后端,通常基于cookie来携带用户的会话id,然后后端在基于会话id查到对应的用户身份信息;区别于session的身份认证方式,jwt作为一个基于RFC 7519的开发标准,提供了一种通过JSON形式的web令牌,用于在各系统之间的安全可信的数据传输、身份标识

本文将主要介绍jwt的相关知识点,以及如何基于jwt来实现一个简单的用户鉴权方案

I. JWT知识点

jwt,全称 json web token, JSON Web 令牌是一种开放的行业标准 RFC 7519 方法,用于在两方之间安全地表示声明。

详情可以参考: hhttps://jwt.io/introduction

1. 数据结构

JSON Web Token由三部分组成,它们之间用圆点.进行分割, 一个标准的JWT形如 xxx.yyy.zzz

  • Header
  • Payload
  • Signature

1.1 header

即第一部分,由两部分组成:token的类型(JWT)和算法名称(比如:HMAC SHA256或者RSA等等)。

一个具体实例如

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

然后,用Base64对这个JSON编码就得到JWT的第一部分

1.2 Payload

第二部分具体的实体,可以写入自定义的数据信息,有三种类型

  • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer 签发者), exp (expiration time 有效期), sub (subject), aud (audience)等。
  • Public claims : 可以随意定义。
  • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明

如一个具体实例

1
2
3
4
5
6
7
{
"iss": "一灰灰blog",
"exp": 1692256049,
"wechat": "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg",
"site": "https://spring.hhui.top",
"uname": "一灰"
}

对payload进行Base64编码就得到JWT的第二部分

1.3 Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

1.4 具体实例

下面给出一个基于 java-jwt 生成的具体实例

1
2
3
4
5
6
public static void main(String[] args) {
String token = JWT.create().withIssuer("一灰灰blog").withExpiresAt(new Date(System.currentTimeMillis() + 86400_000))
.withPayload(MapUtils.create("uname", "一灰", "wechat", "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg", "site", "https://spring.hhui.top"))
.sign(Algorithm.HMAC256("helloWorld"));
System.out.println(token);
}

II. 使用实例

接下来我们基于jwt方案实现一个用户鉴权的示例demo

1. 项目搭建

首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

添加web支持,用于配置刷新演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

我们采用thymeleaf来进行前端页面的渲染,添加一些相关的配置 application.yml

1
2
3
4
5
6
7
8
9
10
server:
port: 8080

spring:
thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false

2. JWT鉴权流程

一个简单的基于jwt的身份验证方案如下图

基本流程分三步:

  1. 用户登录成功之后,后端将生成的jwt返回给前端,然后前端将其保存在本地缓存;

  2. 之后前端与后端的交互时,都将jwt放在请求头中,我们这里借助Http的身份认证的请求头Authorization

  3. 后端接收到用户的请求,从请求头中获取jwt,然后进行校验,通过之后,才响应相关的接口;否则表示未登录

3. 实现方案

基于上面的流程,我们可以实现一个非常简单的登录认证演示工程

首先在内存中,维护几个简单用户名/密码信息,用于模拟用户名+密码的校验

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class LoginController {
private Map<String, String> userCache = create("一灰灰", "123", "yihui", "hello");

private static <K, V> Map<K, V> create(K k, V v, Object... kvs) {
Map<K, V> map = new HashMap<>(kvs.length + 1);
map.put(k, v);
for (int i = 0; i < kvs.length; i += 2) {
map.put((K) kvs[i], (V) kvs[i + 1]);
}
return map;
}
}

然后提供登录接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping(path = "/login")
@ResponseBody
public String login(String uname, String pwd, HttpServletResponse response) {
if (!userCache.containsKey(uname) || !Objects.equals(pwd, userCache.get(uname))) {
return "用户名密码错误,登录失败";
}

String token = JWT.create()
.withIssuer("一灰灰blog")
.withExpiresAt(new Date(System.currentTimeMillis() + 86400_000L))
.withPayload(create("uname", uname, "wechat", "https://spring.hhui.top/spring-blog/imgs/info/wx.jpg", "site", "https://spring.hhui.top"))
.sign(Algorithm.HMAC256("helloWorld"));

response.addCookie(new Cookie("Authorization", token));
return token;
}

上面的接口实现,接收两个请求参数: 用户名 + 密码

当用户身份校验通过之后,将生成一个jwt,这里直接使用开源项目java-jwt来生成(当然有兴趣的小伙伴也可以自己来实现)

需要注意的一点是,我们在上面的实现中,除了直接返回jwt之外,也将这个jwt写在cookie中,这种将jwt写入cookie的方案,主要的好处就是前端不需要针对jwt进行特殊处理
当然对应的缺点也和直接使用session的鉴权方式一样,存在csrf风险,以及对于跨资源共享时的资源共享问题(CORS)

本项目的实际演示中,采用前端存储返回的jwt,然后通过请求头方式来传递jwt

上面登录完成之后,再提供一个简单的要求登录之后才能查看的查询接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@GetMapping("query")
@ResponseBody
public Object queryInfo(HttpServletRequest request) {
// 1. 从请求头中获取jwt
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
return "未登录";
}
token = token.substring(token.indexOf(" ")).trim();

// 2. 验证jwt是否合法
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("helloWorld")).withIssuer("一灰灰blog").build();
DecodedJWT decodedJWT = verifier.verify(token);
HashMap pay = JSONObject.parseObject(new String(Base64Utils.decodeFromString(decodedJWT.getPayload())), HashMap.class);
pay.put("query", "查询成功!");
return pay;
} catch (Exception e) {
e.printStackTrace();
return "鉴权失败: " + e.getMessage();
}
}

最后再写一个前端页面来完成整个测试

1
2
3
4
@GetMapping(path = {"/", "", "/index"})
public String index() {
return "index";
}

对应的前端页面如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="SpringBoot thymeleaf"/>
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>JWT示例demo</title>
</head>
<body>

<div>
<div>准备登录</div>
<br/>
<div>
用户名: <input type="text" id="uname">
</div>
<div>
密码: <input type="password" id="pwd">
</div>
<div>
<button onclick="login()">登录</button>
</div>
<div id="tip"></div>
</br>
<hr/>
<div> --- 分割线 ---</div>
</br>
<button onclick="query()">查询用户信息</button>
</br>
<div>
<pre id="res"></pre>
</div>
</div>
<script>
function login() {
const uname = document.getElementById("uname").value;
const pwd = document.getElementById("pwd").value;
console.log("开始登录", uname, pwd);
var httpRequest = new XMLHttpRequest();//第一步:创建需要的对象
httpRequest.open('POST', '/login', true); //第二步:打开连接
httpRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");//设置请求头 注:post方式必须设置请求头(在建立连接后设置请求头)
httpRequest.send(`uname=${uname}&pwd=${pwd}`);//发送请求 将情头体写在send中
/**
* 获取数据后的处理程序
*/
httpRequest.onreadystatechange = function () {//请求后的回调接口,可将请求成功后要执行的程序写在其中
if (httpRequest.readyState == 4 && httpRequest.status == 200) {//验证请求是否发送成功
const res = httpRequest.responseText;//获取到服务端返回的数据
console.log(res);
document.getElementById("tip").innerText = "登录完成:" + res;
window.sessionStorage.setItem("jwt", res);
}
};
}

function query() {
var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
httpRequest.open('GET', '/query', true);//第二步:打开连接
httpRequest.setRequestHeader("Authorization", "Bearer " + window.sessionStorage.getItem("jwt"));
httpRequest.send();//第三步:发送请求 将请求参数写在URL中
/**
* 获取数据后的处理程序
*/
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState == 4 && httpRequest.status == 200) {
var json = httpRequest.responseText;//获取到json字符串,还需解析
console.log(json);
document.getElementById("res").innerText = json;
}
};
}
</script>
</body>
</html>

4. 实例演示

基于上面的实现,接下来我们看一下具体表现情况

从上面的两张图也可以看出,登录成功之后,jwt写入到本地的session storage中,再后续的请求中,若请求头Authroization中携带了jwt信息,则后端可以进行正常校验

有兴趣的小伙伴可以尝试修改一下本地存储中的jwt值,看一下非法或者过期的jwt会怎么表现

5. 小结

本文主要介绍了jwt的基本知识点,并给出了一个基于jwt的使用实例,下面针对jwt和session做一个简单的对比

jwt session
前端存储,通用的校验规则,后端再获取jwt时校验是否有效 前端存索引,后端判断session是否有效
验签,不可篡改 无签名保障,安全性由后端保障
可存储非敏感信息,如用户名,头像等 一般不存储业务信息
jwt生成时,指定了有效期,本身不支持续期以及提前失效 后端控制有效期,可提前失效或者自动续期
通常以请求头方式传递 通常以cookie方式传递
可预发csrf攻击 session-cookie方式存在csrf风险

关于上面的两个风险,给一个简单的扩展说明

csrf攻击

如再我自己的网站页面上,添加下面内容

1
<img src="https://paicoding.com/logout" style="display:none;"/>

然后当你访问我的网站时,结果发现你在技术派上的登录用户被注销了!!!

使用jwt预防csrf攻击的主要原理就是jwt是通过请求头,由js主动塞进去传递给后端的,而非cookie的方式,从而避免csrf漏洞攻击

III. 不能错过的源码和相关知识点

0. 项目

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到

【基础系列】自定义属性配置绑定极简实现姿势介绍

使用过SpringBoot应用的小伙伴应该对它配套的配置文件application.yml不会陌生,通常我们将应用需要的配置信息,放在配置文件中,然后再应用中,就可以通过 @Value 或者 @ConfigurationProperties来引用

那么配置信息只能放在这些配置文件么? 能否从db/redis中获取配置信息呢? 又或者借助http/rpc从其他的应用中获取配置信息呢?

答案当然是可以,比如我们熟悉的配置中心(apollo, nacos, SpringCloudConfig)

接下来我们将介绍一个不借助配置中心,也可以实现自定义配置信息加载的方式,并且支持配置的动态刷新

查看更多

分享到

【基础系列】 编程式属性绑定Binder

SpringBoot中极大的简化了项目中对于属性配置的加载方式,可以简单的通过 @Value, @ConfigurationProperties 来实现属性配置与Java POJO对象、Bean的成员变量的绑定,那如果出现一个某些场景,需要我们手动的、通过编程式的方式,将属性配置与给定的pojo对象进行绑定,我们又应该怎么实现呢?

查看更多

分享到

【WEB系列】 实战之基于WebListener实现实时在线人数统计

很多pc网站都有一个实时在线人数的统计功能,那么一般这种是采用什么方式来实现的呢? 这里我们介绍一个最基础的是实现方式,基于Session结合WebListener来实现在线人数统计

查看更多

分享到