技术标签: 安全 jwt spring-boot json boot
官网地址: https://jwt.io/introduction/
译文:JSON Web Token
(JWT)是一个开放标准(rfc7519);
它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名
白话解释:JWT是JSON Web Token
的简称,是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成对数据加密、签名等相关处理。
JSON Web Token
是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保请求发起人是否合法;并且由于签名是使用标头和有效负载计算的,甚至能够验证您的请求内容是否被第三方拦截后进行了篡改一说到jwt,那么咱们就不得不提session
、redis
我们最早在做项目的时候,由于功能单一或者用户数量较低,通常是将用户登录后的认证信息存于服务器的session的,每次请求时候呢,都会根据客户端请求携带的cookie(sessionId)去服务器校验比对,这种方式呢,当cookie被截获,用户就会很容易受到跨站请求伪造的攻击,且当用户数量上来的时候,存储用户信息的session会增大服务器的开销!前后端分离,服务器集群都是单session面临的窘境!
由于服务器集群需要解决session共享问题,所以呢,后续随着发展,慢慢的将用户信息存于了redis中,但redis也是一个纯吃内存的服务,大用户量下对redis内存扩展的需要又要不断增大!
总结:
session: 消耗服务器资源 安全问题 需要解决分布式session
redis:内存资源消耗问题
简洁: 可以通过URL将JWT携带在HTTP请求参数 (param)或者在HTTP 请求头(header)进行发送,数据量小,传输速度快
可自包含,jwt中可自定义存储用户一些信息,适用于大多数业务逻辑场景,避免频繁查库
jwt 支持任何web形式请求
保存在客户端,无需在服务端保存用户会话信息,特别适用于分布式微服务
第一步:前端通过Web表单将自己的用户名和密码发送到后端的接口
第二步:后端根据用户密码查询数据库校验账户合法性,验证成功后将一些用户信息构造进jwt中,此时jwt会进行加密成为一串字符串,前端将jwt生成的字符串返回给前端
第三步:前端后续操作请求将必须携带后端返回的jwt字符串 可存于Header或参数中(根据前后端协商结果,但一般都是header 且使用Authorization位,这样可以防止请求伪造),后端则检查前端是否传递了jwt或者传递过来的jwt是否合法是否过期等等
第四步:如后端接口需要验证才可访问,则对请求校验jwt,验证通过则进行逻辑操作,否则让用户登录
第五步:客户端用户退出时前端可选择删除jwt(也可不删(jwt一般都会设置有效时间))
整个链路图示如下:
前边说了一些jwt的各种好各种优点,但仅仅初略提了一嘴jwt就是一个加密的后的字符串…
那么,其具体结构属性是怎样的呢?
令牌组成
JWT(JSON WEB TOKEN)共有三部分组成:标头.负载.签名
jwt: String ====> header.payload.singnature
例如这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的第一部分是标头(Header),标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
一般我们都会使用默认标头:
{
"alg": "HS256",
"typ": "JWT"
}
令牌的第二部分是有效负载(Payload),其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
这一部分,我们通常会存储一些项目业务所需要的使用的普遍属性
{
"user_id": "1",
"name": "tom",
"organization_id": 1
}
令牌的第三部是签名(Signature)签名 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是防止JWT篡改
例如这样:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);
公式:加密算法(标头base64编码+“.”+负载base64编码,密钥)
签名目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
前边也说过 标头(Header)和负载(Payload)是用base64进行编码的,而Base64是可逆的,这就意味着,其可以被外部解码!!!!那么我的信息不是暴露了??这不是很沙雕吗??
确实,JWT中的Header和Payload 可被反编,我不往其中存敏感信息不好了吗,这不是虎吗?我只存个user_id或者user_nick_name,就算被反编码知道了,那又怎样呢???他又能从这无关紧要的属性中推测出啥呢???就像你知道我叫神威马超
或者我叫玉麒麟卢俊义
,我不违法乱纪你能咋地!
官网也警示了!不要在JWT中填充敏感信息!!!!
请一定要注意:不要在JWT中填入敏感信息!!!
请一定要注意:不要在JWT中填入敏感信息!!!
请一定要注意:不要在JWT中填入敏感信息!!!
强调+∞∞∞∞∞
我们要使用jwt呢,肯定需要引入其相应的依赖
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
接下来,咱们就先来简单使用一下jwt
首先,咱们来编写一个demo
我们构建jwt 只需使用其JWT.create()
方法,一直Build我们的参数即可
header
:可不写,其有一个默认参数,当然您也可自行更改,详见其JWT.create()
方法
withClaim
:方法即为我们的负载(payload)可多次build
withSubject
: 也是填充负载的一种方式
sign
:即我们的签名方式,起中班包含加密密钥,sign一定是要在最后一个build的
@Test
void contextLoads() {
String jwt = JWT.create()
.withClaim("username", "王二麻子")
.sign(Algorithm.HMAC256("secret"));
System.out.println(jwt);
}
执行后呢,拿到了一个jwt字符串
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IjExMSJ9.2g5robitasDRAv4RKEOwn6O7VGbpARjT06zistUGfCI
我们使用在线jwt解析,将我们jwtcopy进去看看
成功获取到了jwt的header以及 payload,这呢,也再次证明和警示了我们,不要将用户敏感信息存入JWT负载中…
我们服务器呢,亦可对JWT进行校验
我们需要根据JWT构建时的密钥进行生成一个解析对象JWTVerifier
然后将调用解析对象的verify
方法,将我们的JWT传入进去,生成一个JWT解码对象
JWT解码对象调用不同的方法拿到对应的值…
例如调用getClaim("负载中某一字段名")
可拿到某具体字段的值
也可调用getClaims()
拿到所有负载内容,返回值为map
@Test
void verifyJWT() {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("secret")).build();
DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IueOi-S6jOm6u-WtkCJ9.Q71pvsTR5TYFePJFIMLvCbO8MGnQ0oiSJNCuSs1JiM4");
String payload = decodedJWT.getPayload();
System.out.println("负载:" + payload);
System.out.println("标头:" + decodedJWT.getHeader());
System.out.println("签名:" + decodedJWT.getSignature());
System.out.println("------");
Map<String, Claim> payloads = decodedJWT.getClaims();
payloads.forEach((k,v)-> System.out.println( k + ":" + v.asString()));
}
注意的点:拿到负载字段后,其值时需要强转类型的,比如你原来某一键值对是"username":“zs”,那么你需要as为String… 如果你有键值对为"age": 1,那么你需要对其值asInt…以此类推…需要严格的对应字段值才能成功正确的获取到…否则拿不到真正的值
例如,我之前的是"“username”, "王二麻子”, 那么我拿到值需要再asString…如果类型不对,是无法获取到正确的值…
如此,校验这一步也基本走通了
事实上,我们会对服务器token做一个过期限制,比如十二小时自动过期或者每天凌晨自动过期等等…
JWT呢,其构造方法默认即可设置构造的时间
@Test
void contextLoads() {
String jwt = JWT.create()
.withClaim("username", "admin")
.withExpiresAt(new Date(System.currentTimeMillis()+60000))
.sign(Algorithm.HMAC256("secret"));
System.out.println(jwt);
}
我这里呢,是设置了构造后的一分钟的过期,那么此token仅仅只有一分钟的有效时间
我们来测试一下
丢到JWT解析器中,发现其负载中新增了一个exp字段,其值便是我们这个TOKEN过期时间点的时间戳(秒单位)…
时隔一会,代码解析JWT 报错JWT超时异常
用的是mysql数据库,mybatisplus ORM框架
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
用户表实体
@Data
@TableName("user")
public class User {
private Integer id;
private String username;
private String password;
}
@Data
public class UserSub {
private Integer id;
private String username;
}
我们不可能每次都对jWT做如此复杂的操作,为了高效开发以及复用,我们需要对JWT的使用做一定的代码封装
注意:这里的Jwt构建与解析与上方展示方式做了一些微调,主要是针对于payLoad,一般生产情况我们会直接将一个json格式的数据(注意不要填充敏感感信息)直接填充至payload,解析时拿到payLoad反序列化为对象
package com.leilei.jwt;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.leilei.entity.UserSub;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @author lei
* @version 1.0
* @date 2020/11/28 17:43
* @desc
*/
@Service
public class JwtSupport {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expireTime}")
private Long expireTime;
/**
* 生成token
*
* @param payload 载荷
* @return 返回token
*/
public String buildToken(UserSub payload) {
JWTCreator.Builder jwt = JWT.create();
jwt.withSubject(JSON.toJSONString(payload));
jwt.withExpiresAt(new Date(System.currentTimeMillis() + expireTime * 60 * 60 * 1000));
return jwt.sign(Algorithm.HMAC256(secret));
}
/**
* 验证token
*
* @param token
* @return
*/
public void verify(String token) {
JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
/**
* 获取token中payload
*
* @param jwt
* @return
*/
public UserSub parseJwt(String jwt) {
DecodedJWT decodedJwt = JWT.require(Algorithm.HMAC256(secret)).build().verify(jwt);
String subject = decodedJwt.getSubject();
return JSON.parseObject(subject, UserSub.class);
}
}
yml新增配置
jwt:
# 密钥 后续对密码配置需要加密
secret: 'lei#ae86..'
#token过期时间 单位小时
expireTime: 12
这里也要注意:我们的密钥后续需要做一些安全配置,例如加密,使用启动参数覆盖等等,一定要保护密钥的安全,不然JWT加密就成了空壳…
接下来我们简单的模拟一下登录
public AjaxResult login(User user) {
User existUser = userMapper.selectOne(new QueryWrapper<User>()
.lambda()
.eq(User::getUsername, user.getUsername())
.eq(User::getPassword, user.getPassword()));
if (existUser == null) {
return AjaxResult.error("账户或密码错误");
}
UserSub userSub = new UserSub();
BeanUtils.copyProperties(existUser, userSub);
return AjaxResult.success(jwtSupport.buildToken(userSub));
}
@Target({
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccPermission {
/**
* jwt校验,如果设置为false则表示接口不需要校验token合法性
* @return
*/
boolean jwt() default true;
}
package com.leilei.jwt;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.leilei.common.AjaxResult;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author lei
* @version 1.0
* @date 2020/11/28 18:35
* @desc
*/
@Component
@Log4j2
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtSupport jwtSupport;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
String url = request.getRequestURI();
HandlerMethod handlerMethod = (HandlerMethod) handler;
AccPermission annotation = handlerMethod.getMethod().getAnnotation(AccPermission.class);
if (annotation == null) {
annotation = handlerMethod.getMethodAnnotation(AccPermission.class);
//无校验注解,则仍默认校验JWT合法性
if (annotation == null) {
return true;
// return checkJWT(request,response,url);
}
}
//如果设置了jwt=false,则直接放行
if (!annotation.jwt()) {
return true;
}
//jwt=true,则开始校验
return checkJwt(request, response, url);
}
return true;
}
/**
* 校验JWT
*
* @param request
* @param response
* @param url
* @return
* @throws Exception
*/
public boolean checkJwt(HttpServletRequest request, HttpServletResponse response, String url) throws Exception {
String authToken = request.getHeader("Authorization");
if (StringUtils.isBlank(authToken)) {
return checkError(response, url);
}
jwtSupport.verify(authToken);
return true;
}
/**
* 校验失败
*
* @param response
* @param url
* @return
* @throws Exception
*/
public boolean checkError(HttpServletResponse response, String url) throws Exception {
response.setHeader("Content-Type", "application/json");
response.setCharacterEncoding("UTF-8");
log.error("当前接口-{}需要登录!", url);
response.getWriter().print(JSON.toJSONString(AjaxResult.error("Token校验失败,当前请求需要登录!!!", 401)));
return false;
}
}
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
/**
* 添加;拦截器 以及拦截 或者放行规则
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//将自定义的拦截器添加进去 必须是使用方法获取,不可直接New 否则该拦截器中中无法注入Bean
registry.addInterceptor(getJwtInterceptor())
//拦截所有
.addPathPatterns("/**")
//排除路径 排除中的路径 不需要进行拦截
.excludePathPatterns("/login/**");
}
/**
* 定义方法获取自定义的 JWTInterceptor
* @return
*/
@Bean
public JWTInterceptor getJwtInterceptor() {
return new JWTInterceptor();
}
}
package com.leilei.config;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.leilei.common.AjaxResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author lei
* @create 2022-10-19 14:59
* @desc 全局异常拦截
**/
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(TokenExpiredException.class)
public AjaxResult tokenExpiredException() {
return AjaxResult.error("token已过期", -2);
}
@ExceptionHandler(JWTVerificationException.class)
public AjaxResult jwtVerificationException() {
return AjaxResult.error("token解析异常", -3);
}
}
现有接口如下,一个登录,一个测试,我们测试接口打上了自定的的注解AccPermission
,且对该url进行了拦截,因此,当前/test
url 需要携带登录后获取的token令牌
…抛出需要登录信息
…可正常访问
…成功拿到token信息,这里前端需要保存起来
…可正常访问接口
…抛出token错误,需要登录信息
…token仍然有效,无需再次登录服务器,直到token过期为止
…
从以上案例中,我们已然看出了JWT的强大之处…例如轻量级,无需每次查询数据库,凭证由客户端存储,项目重起服务端无需从新颁发凭证等等
NSPredicate使用Cocoa提供了一个名为NSPredicate的类,它用于指定过滤器的条件。可以创建NSPredicate对象,通过该对象准确地描述所需的条件,对每个对象通过谓词进行筛选,判断它们是否与条件相匹配。这里的“谓词”通常用在数学和计算机科学概念中,表示计算真值或假值的函数。Cocoa用NSPredicate描述查询的方式,原理类似于在数据库中进行查询。...
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。原文出处:https://wanago.io/2018/07/23/webpack-4-course-part-three-working-with-plugins/大家好!今天我们介绍插件这个概念。插件与loader的不同之处在于它能完成更复杂的任务。基本上,loader做不了的...
网上介绍安卓7.0调用系统拍照的博客有很多,但感觉都不是很清晰,遂决定自己来写。Demo要实现的功能:1.支持拍照并且可以对图片进行裁剪2.支持从图库中选择图片并进行裁剪3.无论是拍照的照片还是从图库中选择的照片(都是裁剪后的)统一存储在同一个目录下问题点:1.安卓7.0及以后,获取图片的uri方式发生了变化,7.0及以后的uri需要通过contentProvider提供,...
前端模块化问题: 为什么前端要使用模块化? 模块化: 是具有特定功能的一个对象( 广义理解 ) 模块定义的流程: 1.定义模块(对象) 2.导出模块 3.引用模块 好处:可以存储多个独立的功能块复用性高分类1. AMDAMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函...
有时候需要将 MySQL 的数据导出成 excel,这很简单,无需第三方工具,直接 MySQL 命令行就自带了这样的功能。比如: SELECT * FROM nowamagic into outfile 'D:\\nowamagic.xls'; 双斜杠是带转义识别目录。 当然也可以加入筛选条件,将特定的数据导出成 exce有时候需要将 MySQL 的数据导出成 excel,这很简单,无需第三方工具...
运行命令:adbshellmonkey-pcom.crazyhornets.MyHokageAndroidZSY-v-v-v20--throttle1000Log::Monkey:seed=0count=20//伪随机种子为0,事件总数20:AllowPackage:com.crazyhornets.MyHokageAndroidZSY//包名:...
点击上方逛逛GitHub,选择设为星标优质项目,及时送达来自量子位Vim 难学难用?但事实是,它依旧受许多程序员的欢迎。或许,只是你的「打开方式」不对。最近,在 GitHub 上便出...
树状数组可以说是线段树的分支;树状数组可以解决的问题线段树都可以解决,而线段树可以解决的问题树状数组却不一定可以解决;一 定义线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段
MS SQL Server的NFS远程备份设置
hadoop客户端读数据流程分析
I'm trying to get HTTPS working on express.js for node, and I can't figure it out. 我正在尝试让HTTPS在expr
JavaSE计算机结构计算机网络:您的计算机 朋友的计算机 ----> 互联网协议: IP Internet protocol是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。作用: 共享信息、程序和数据分类局域网(LAN,Local Area Network) 城域网(MAN,Metropolis Area Network) 广域