Jwt(Json web token)——从Http协议到session+cookie到Token Jwt介绍 Jwt的应用:登陆验证的流程
目录
- 引出
- 从Http协议到session&cookie到Token
- HTTP协议
- session & cookie
- session
- cookie
- 为什么需要session & cookie?
- JavaEE传统解决长连接方案
- 问题:分布式不适用
- 解决方案:令牌Token
- Jwt,Json web token
- jwt的结构
- Header
- 加密算法
- Base64 + 图片编码
- Payload 载荷:比如唯一的用户ID
- Registered claims
- Public claims
- Private claims
- Payload案例
- Signature:签名
- jwt使用初步
- 导包
- 生成token
- 解析token
- 获取token的内容
- 验证token
- 几种token验证的情况
- token的过期时间
- token的内容被改过
- token的载荷被更改
- Jwt的应用:登陆验证的流程
- 用户表
- dao数据库
- service业务
- 定义异常
- 异常的拦截处理
- controller层
- swagger测试
- 使用拦截器统一拦截方式
- Vo对象给前端
- DTO对象
- 日期json问题
- 枚举类型的json化
- application.yml配置文件
- pom文件
- 总结
引出
1.http协议的特点,无状态,被动请求;
2.session+cookie解决浏览器的无状态问题,但是分布式不适用;
3.Jwt,Json web token,token令牌入门,头部+载荷+签名;
4.Jwt的使用初步,如果过期,报错com.auth0.jwt.exceptions.TokenExpiredException;
5.应用:登陆验证的流程;
从Http协议到session&cookie到Token
HTTP协议
特点
-
无记忆性(无状态)
-
请求响应
短连接
被动请求
session & cookie
session
Session: Session存在服务器端的对象。
cookie
Cookie就是存储在浏览器的一个对象(客户端)
为什么需要session & cookie?
本质上讲 : http协议的问题(无记忆性)。
JavaEE传统解决长连接方案
问题:分布式不适用
- 只适合于单体架构,集群/分布式架构的不合适
- Session要进行同步,比较困难(网络抖动、延迟等),有状态的
- Cookie: 不支持跨域,前后端分离
- 移动端(很少使用cookie),比如微信小程序不支持cookie
session需要同步
- 对于多服务器集群,每台服务器都要读取Session,共享困难。
解决方案:令牌Token
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
- 对称加密,如AES
- 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
- 优势:算法公开、计算量小、加密速度快、加密效率高
- 缺陷:双方都使用同样密钥,安全性得不到保证
- 非对称加密,如RSA
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 不可逆加密,如MD5,SHA
- 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。
RSA算法历史:
1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA
目前流行的还有oauth2
Jwt,Json web token
JSON Web Token(令牌)
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact压缩的 and self-contained自包含的 way for securely transmitting information安全传输 between parties as a JSON object。
This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
- 基于
JSON
格式用于网络传输的令牌。- 紧凑的
Claims
声明格式。Claim
有索赔、声称、要求或者权利要求的含义。
jwt的结构
官网:https://jwt.io/
如果改前面的,最后的签名会改变,就知道被改动过了
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据。
加密算法
-
HSA256
SHA(Secure Hash Algorithm,安全散列算法)是一个密码散列函数家族,由美国国家安全局(NSA)设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准。
- 无论输入多长,都输出
64
个字符,共32字节(byte),256位(bit) - 输出只包含数字
0
`9`和字母`A`F
,大小写不敏感
- 无论输入多长,都输出
-
RSA256
由美国麻 省理工 学院三 位学者 Riv est、Sh amir 及Adleman 研 究发 展出 一套 可实 际使用 的公 开金 钥密码系 统,那 就是RSA(Rivest-Shamir-Adleman)密码系统。
RS256(带有SHA-256的 RSA 签名)是一种非对称算法,它使用公钥/私钥对:身份提供者拥有用于生成签名的私钥(秘密)密钥,而 JWT 的消费者获得公钥验证签名。由于与私钥相反,公钥不需要保持安全,因此大多数身份提供者都可以让消费者轻松获取和使用(通常通过元数据 URL)。
Base64 + 图片编码
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。Base64不是加密算法,只是一种编码方式,可以用Base64来简单的“加密”来保护某些数据,所以每 6 个比特为一个单元,对应某个可打印字符。
“hello” 编码
h—01101000 e —01100101
package com.tianju.redisDemo.jwt;import org.apache.ibatis.logging.stdout.StdOutImpl;import java.util.Base64;public class JwtDemo {public static void main(String[] args) {String s = "h";// hz转成2进制:h-->ASCII值(10进制)-->2进制String bin = "0" + Integer.toBinaryString(s.charAt(0));System.out.println("h转换成2进制:"+bin);System.out.println("截取前6位:"+bin.substring(0, 6));// 进行base64编码String sBase64 = new String(Base64.getEncoder().encode(s.getBytes()));System.out.println(sBase64);}
}
Base64 在线编码解码 | Base64 加密解密 - Base64.us
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<img src="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAABMLAAATCwAAAAAAAAAAAAAsLTD/CwsJ/xoeIf82QUn/Njw5/0ZGRf9LTFH/WVZX/1JVV/9iZmT/ZmNx/1dha/85Qkz/Jyct/w0HBf8OCgf/Cw0P/yEhIv87O0D/RjpI/0tBRv8uLCj/FxsS/x0eGP8bFhn/IyAl/y8xMv9xd3r/eoCJ/0NFVP8gHSX/CAUC/xQXG/8nKC//OjY4/z86Nv8RDRv/AAAZ/wAAGf8UEyL/MyEg/w8AAP8FAAD/DAgH/2hobv+FhZH/MzVA/xUSEv8+QUb/Ly43/ykiK/8BAT3/BARi/wEBaP8BAFj/LCRk/2NCO/80AAH/LAMD/x0BAf8BAAD/PDk+/2pucv82MjX/SUdY/0EvJ/8KCTH/BwWT/wcEpv8CAJr/AAF6/y0ue/+HSEz/XQAA/1EDB/8+Awb/HAEB/wgAAP9nZmr/Yl5l/01OUP8uJiX/AwN5/wMFzP8FCMj/Awa//wAArP8sMJX/lU9R/34AAP96Awb/VwME/y0EAP8XAAD/MCYp/3t6gP9cVk//JSdE/wECyP8ABu7/AQDx/wEA4v8AAMH/LTG4/6NcW/+WAAD/oQAC/5IDAf9UBQD/NwMA/x0EBf92eX7/eWNi/yQkY/8BAu3/AQD9/xwk6P9HUPD/EhPi/y4z1v+tZGD/nAAA/7UDA//RCAb/fgUB/1ICAf8mAAD/goiK/4RuZv8kHnf/AgH9/wAA9v9gce7/9//5/7+++P+Ji+3/qGZq/6MAAP+oAgn/rQcL/4YEBP9aAwH/KAEB/4+Kj/96dm3/IB5X/wYB+P8BAP3/HyXv/8bN9f//29vt/6teZv+dAAH/iwUA/3MCAf9gAwP/PgQD/xcAAP90bnX/Zm5y/yUiNP8GBcT/AgT//wAA/f8vL/L/eHfx/3J37P+tb2v/qwEA/5oBA/9sAwH/UAMD/ywBAf8mEhn/b212/11ugf9aR1D/AQFg/wUG9v8EAvz/BwD6/wAA5f8wNNT/pG1s/54AAP+NBAP/YwQB/zoCAv8NAAD/T01X/05OWP9Nb4T/Xl9f/0M1TP8AAJz/CArl/wIE6f8AALz/ODq0/7Jwbv93AAD/bgQD/0gEAP8WAAD/GxYZ/15aaP8pLDj/X5bB/2iHnP97c3n/KSg4/wMAfv8EAL7/AACc/zczoP+fYWb/YgAA/1EBAf8WAAD/HxMW/2hpdf87R1n/MTM9/zRtnf+Pxev/gpek/2pjZP9gX2z/JCNW/wAATf8bG1X/VT1C/xoAAP8gDg7/PDQ4/1pNVv9QSVr/SEtd/xIRGP9McYj/R3uU/4a+2f9Wbn//SVFQ/2lmXv9oYV//YlVd/2VVVP9eVlf/WFlf/0dKVv8zQUj/Q0xU/yAYIP8CAAT/gABseQAAMyAAAGRlAAAyNAAAdGUAAGluAAAxLAAAdXQAAG50AABBTQAAUFIAAEVTAABSXwAAVkUAADIzgAFSTw=="
style="width: 100px" height="100px">
</body>
</html>
Payload 载荷:比如唯一的用户ID
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
Payload 也使用BASE64编码
Registered claims
这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操作的声明。 其中一些是:
iss
(发行者),exp
(到期时间),sub
(主题),aud
(受众)等
序号 | 名称 | 解释 |
---|---|---|
1 | iss (issuer) | 签发人 |
2 | exp (expiration time) | 过期时间 |
3 | sub (subject) | 主题 |
4 | aud (audience) | 受众(接收jwt的一方) |
5 | nbf (Not Before) | 生效时间 |
6 | iat (Issued At) | 签发时间 |
7 | jti (JWT ID) | jwt的唯一身份标识, 主要用来作为一次性token, 从而回避重放攻击 |
Public claims
这些可以由使用JWT的人员随意定义。
Private claims
这些是为了同意使用它们但是既没有登记,也没有公开声明的各方之间共享信息,而创建的定制声明。
Payload案例
{ "sub": "456781234", "name": "Mr.zhang", "admin": true}
Signature:签名
The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.
Signature 部分是对前两部分的签名,防止数据篡改。
https://jwt.io/
Signature部分由编码后的Header、Payload和自定义的秘钥使用Header中指定的算法(HSA256)进行加密签名;
结构
jwt使用初步
导包
<!-- jwt的依赖--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version></dependency>
生成token
package com.tianju.redisDemo.jwt;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JavaJwtTest {public static void main(String[] args) {// 定义头部headerMap<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 私钥String salt = "pet";String token = JWT.create().withHeader(header).withClaim("username", "tom").withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60)) // 60s之后过期.sign(Algorithm.HMAC256(salt.getBytes()));System.out.println(token);}
}
获得token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODk3NzY0MTUsInVzZXJuYW1lIjoidG9tIn0.kPmgw03j9g3EJAo2LLzXHd1b7H3uF5zF-0EDEaeonMY
解析token
DecodedJWT decode = JWT.decode(token);
获取token的内容
System.out.println("Header: " + decode.getHeader());
System.out.println("Payload: " + decode.getPayload());
System.out.println("Audience: " + decode.getAudience());
System.out.println("Signature: " + decode.getSignature());
验证token
JWTVerifier build = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
获取信息
System.out.println(build.verify(token).getClaim("data"));
{“name”:”tomcat”,”username”:”tom”}
几种token验证的情况
token的过期时间
com.auth0.jwt.exceptions.TokenExpiredException
package com.tianju.redisDemo.jwt;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JavaJwtTest1 {public static void main(String[] args) throws InterruptedException {// 定义头部headerMap<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 私钥String salt = "pet";String token = JWT.create().withHeader(header).withClaim("username", "tom").withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60s之后过期.sign(Algorithm.HMAC256(salt.getBytes()));System.out.println(token);Thread.sleep(1000*5);JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();DecodedJWT verify = jwtVerifier.verify(token);System.out.println(verify.getClaim("username"));}
}
token的内容被改过
com.auth0.jwt.exceptions.JWTDecodeException
package com.tianju.redisDemo.jwt;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JavaJwtTest1 {public static void main(String[] args) throws InterruptedException {// 定义头部headerMap<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 私钥String salt = "pet";String token = JWT.create().withHeader(header).withClaim("username", "tom").withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60s之后过期.sign(Algorithm.HMAC256(salt.getBytes()));System.out.println(token);token = token.replaceAll("[a-zA-Z]","X");JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();DecodedJWT verify = jwtVerifier.verify(token);System.out.println(verify.getClaim("username"));}
}
token的载荷被更改
package com.tianju.redisDemo.jwt;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JavaJwtTest1 {public static void main(String[] args) throws InterruptedException {// 定义头部headerMap<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 私钥String salt = "pet";String token = JWT.create().withHeader(header).withClaim("username", "tom").withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 30)) //.sign(Algorithm.HMAC256(salt.getBytes()));System.out.println("登陆成功产生token:"+token);// token = token.replaceAll("[a-zA-Z]","X");String hackToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODk3Nzk1MjMsInVzZXJuYW1lIjoicGV0In0.laRjGZsM5nq8IYPBMSYCbMdxyFMiXaZXKg9F7WL-n-Q";System.out.println("被改过的token:"+hackToken);JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();DecodedJWT verify = jwtVerifier.verify(hackToken);System.out.println(verify.getClaim("username"));System.out.println("签名:"+ verify.getSignature());}
}
Jwt的应用:登陆验证的流程
整体的流程:
第一步:产生30分钟有效时间的token;
第二步:在redis里面存储token,redis里面有效时间为60分钟;
第三步:token过期,但是Redis里面的token还没有过期,此时进行续期;
第四步:给原有的token续期,需要设置最大续期时间,目前用最大续期次数解决;
用户表
用户名、密码、电话号码、邮箱
user.java实体类
package com.tianju.redisDemo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_owner")
public class User {@TableId(value = "id",type = IdType.AUTO)private Integer id;@TableField("username")private String username;@TableField("realname")private String realName;@TableField("password")private String password;@TableField("tel")private String tel;@TableField("security_key")private String securityKey;@TableField("create_time")private Date createTime;
}
dao数据库
UserMapper.java文件
package com.tianju.redisDemo.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tianju.redisDemo.entity.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper extends BaseMapper<User> {}
service业务
处理登陆业务:抛出异常:
(1)throw new UsernameIsEmptyException(“输入为空异常”);
(2)throw new UsernameNotFoundException(“用户名不存在异常”); 后面用布隆过滤器
(3)throw new UsernameOrPasswordErrorException(“用户名或者密码错误异常”);
package com.tianju.redisDemo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tianju.redisDemo.dao.UserMapper;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.exception.UsernameIsEmptyException;
import com.tianju.redisDemo.exception.UsernameNotFoundException;
import com.tianju.redisDemo.exception.UsernameOrPasswordErrorException;
import com.tianju.redisDemo.service.IUserService;
import com.tianju.redisDemo.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;@Service
@Transactional
@Slf4j
public class UserServiceImpl implements IUserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic List<UserVo> queryAll() {List<UserVo> userVolist = new LinkedList<>();List<User> userList = userMapper.selectList(null);for (User user : userList) {userVolist.add(new UserVo(user.getUsername(), user.getRealName(), user.getCreateTime()));}return userVolist;}/*** 登陆的业务* @param username* @param password*/@Overridepublic User login(String username, String password) {if (username==null || password==null||"".equals(username)||"".equals(password)){// 抛出异常,USERNAME_IS_EMPTY_EXCEPTIONthrow new UsernameIsEmptyException("输入为空异常");}// 判断用户名是否存在LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(User::getUsername,username);User user = userMapper.selectOne(lambdaQueryWrapper);if (Objects.isNull(user)){// 用户名不存在// 抛出异常:USERNAME_NOT_FOUND_EXCEPTIONthrow new UsernameNotFoundException("用户名不存在异常");}if (!user.getPassword().equals(password)){// 密码不对// 抛出异常:USERNAME_OR_PASSWORD_ERROR_EXCEPTIONthrow new UsernameOrPasswordErrorException("用户名或者密码错误异常");}log.debug("用户名{}登陆系统成功",username);return user;}
}
定义异常
package com.tianju.redisDemo.exception;/*** 非法用户*/
public class IllegalUserException extends RuntimeException{public IllegalUserException(String message) {super(message);}
}
package com.tianju.redisDemo.exception;/*** 用户名。或者密码未输入异常*/
public class UsernameIsEmptyException extends RuntimeException{public UsernameIsEmptyException(String message) {super(message);}
}
package com.tianju.redisDemo.exception;/*** 用户未登录*/
public class UserNotLoginException extends RuntimeException{public UserNotLoginException(String message) {super(message);}
}
异常的拦截处理
controller层调用service时,会抛出异常,捕获controller层的异常,进行处理,返回响应
package com.tianju.redisDemo.exception.handle;import com.auth0.jwt.exceptions.TokenExpiredException;
import com.tianju.redisDemo.dto.HttpResp;
import com.tianju.redisDemo.dto.ResultCode;
import com.tianju.redisDemo.exception.*;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.util.Date;/*** 拦截controller层的异常,捕获异常,进行处理;* 用户登陆异常*/
@RestControllerAdvice
public class UserExceptionHandler {@ExceptionHandler(UsernameIsEmptyException.class)public HttpResp usernameIsEmptyHandler(UsernameIsEmptyException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}@ExceptionHandler(UsernameNotFoundException.class)public HttpResp usernameNotFoundHandler(UsernameNotFoundException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}@ExceptionHandler(UsernameOrPasswordErrorException.class)public HttpResp usernameOrPasswordErrorHandler(UsernameOrPasswordErrorException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}@ExceptionHandler(UserNotLoginException.class)public HttpResp userNotLoginHandler(UserNotLoginException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}@ExceptionHandler(IllegalUserException.class)public HttpResp illegalUserHandler(IllegalUserException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}// @ExceptionHandler(TokenExpiredException.class)
// public HttpResp tokenExpiredHandler(TokenExpiredException e){
// return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
// }@ExceptionHandler(UserNeedRenewTokenException.class)public HttpResp userNeedRenewTokenHandler(UserNeedRenewTokenException e){return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());}
}
controller层
UserController.java文件
package com.tianju.redisDemo.controller;import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.tianju.redisDemo.dto.HttpResp;
import com.tianju.redisDemo.dto.ResultCode;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Api(tags = "用户Api")
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate IUserService userService;@Value("${lifespanToken}")private Integer lifespanToken;@Value("${lifespanRedis}")private Integer lifespanRedis;@ApiOperation(value = "login",notes = "用户登陆api")@ApiImplicitParams({@ApiImplicitParam(name = "username",required = true),@ApiImplicitParam(name = "password",required = true)})@PostMapping("/login")public HttpResp login(String username, String password, HttpServletResponse response,HttpServletRequest request){User user = userService.login(username, password);// 这里有可能出现异常// 能走到这一步,说明登陆成功,产生token// 第一步:产生30分钟有效时间的tokenString securityKey = user.getSecurityKey();String token = createToken(username, securityKey);// 存入redis里面,用户名和秘钥,以token为键,所以只要token被改动,就会认为非法用户stringRedisTemplate.opsForHash().put(token, "username", username);stringRedisTemplate.opsForHash().put(token, "securityKey", securityKey);// redis过期晚一点,双token机制// 第二步:在redis里面存储token,redis里面有效时间为60分钟;stringRedisTemplate.expireAt(token, new Date(System.currentTimeMillis()+lifespanRedis));// 3.token要给前端response.addHeader("bm_token",token);// 在session里面存续期的次数request.getSession().setAttribute("renewTokenTimes", 0);return HttpResp.results(ResultCode.USER_LOGIN_SUCCESS,new Date(),token);}/*** 产生token对象* @param username 用户名* @param securityKey 安全码* @return*/private String createToken(String username,String securityKey){Map<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 链式写法return JWT.create().withHeader(header).withClaim("username", username)
// .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60*30// 第一步:产生30分钟有效时间的token.withExpiresAt(new Date(System.currentTimeMillis() + lifespanToken)) // 60*30.sign(Algorithm.HMAC256(securityKey.getBytes()));}@ApiOperation(value = "findAll",notes = "用户登陆api")@GetMapping("/findAll")public HttpResp findAll(){// 使用拦截器处理没有token的情况return HttpResp.results(ResultCode.USER_FIND_SUCCESS,new Date(),userService.queryAll());}
}
swagger测试
使用拦截器统一拦截方式
AuthInterceptor.java文件
TokenExpiredException异常
package com.tianju.redisDemo.interceptor;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tianju.redisDemo.exception.IllegalUserException;
import com.tianju.redisDemo.exception.UserNeedRenewTokenException;
import com.tianju.redisDemo.exception.UserNotLoginException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${lifespanToken}")private Integer lifespanToken;@Value("${lifespanRedis}")private Integer lifespanRedis;@Value("${MAX_RENEW_TOKEN_TIME}")private Integer MAX_RENEW_TOKEN_TIME;// 最大续期次数@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("bm_token");if (token==null || "".equals(token)){throw new UserNotLoginException("用户没有登陆");}// 如何获取tokenString securityKey = (String) stringRedisTemplate.opsForHash().get(token, "securityKey");if (securityKey==null){throw new IllegalUserException("非法用户");}// 过期异常,篡改异常JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(securityKey)).build();try {// 如果token过期,会抛出com.auth0.jwt.exceptions.TokenExpiredExceptionDecodedJWT decodedJWT = jwtVerifier.verify(token);} catch (TokenExpiredException e) { // token过期,redis没有过期// 第三步:token过期,但是Redis里面的token还没有过期,此时进行续期String username = (String) stringRedisTemplate.opsForHash().get(token, "username");// 方案一:产生新的token给前端;
// String newToken = removeTokenMakeNewToken(token, username, securityKey);
// response.addHeader("bm_token",token);// 方案二:给原有的token续期,需要设置最大续期时间Integer renewTokenTimes = (Integer) request.getSession().getAttribute("renewTokenTimes");request.getSession().setAttribute("renewTokenTimes",renewTokenTimes+1);// 产生一个新的token,更新redislog.info( ">>>>>>>>>"+username+"的token过期,即将进行续期,续期次数:"+renewTokenTimes);if (renewTokenTimes<MAX_RENEW_TOKEN_TIME){System.out.println("我输续期");stringRedisTemplate.expireAt(token, new Date(System.currentTimeMillis()+lifespanRedis));}else {System.out.println("不再续期");throw new UserNeedRenewTokenException("达到最大续期次数,需要重新登陆");}}return true;}private String removeTokenMakeNewToken(String token,String username,String securityKey){// 1.产生新的tokenMap<String,Object> header = new HashMap<>();header.put("alg", "HS256");// 链式写法String newToken = JWT.create().withHeader(header).withClaim("username", username)
// .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60*30.withExpiresAt(new Date(System.currentTimeMillis() + lifespanToken)) // 60*30.sign(Algorithm.HMAC256(securityKey.getBytes()));// 2.更新redisstringRedisTemplate.opsForHash().delete(token,"*"); // 删除之前的tokenstringRedisTemplate.opsForHash().put(newToken, "username", username);stringRedisTemplate.opsForHash().put(newToken, "securityKey", securityKey);// redis过期晚一点,双token机制stringRedisTemplate.expireAt(newToken, new Date(System.currentTimeMillis()+lifespanRedis));// 3.新的token还要给前端return newToken;
// response.addHeader("bm_token",token);}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1VGKnaD-1690192090095)(D:\javalearn\思维导图笔记\mdPictures\image-20230724173507503.png)]
配置拦截器 @Configuration
package com.tianju.redisDemo.config;import com.tianju.redisDemo.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class BmMvcConfig implements WebMvcConfigurer {@Resourceprivate AuthInterceptor authInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/user/login");}
}
Vo对象给前端
只给前端部分数据信息
package com.tianju.redisDemo.vo;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVo {private String username;private String name;private Date createTime;
}
DTO对象
日期json问题
@JsonFormat(timezone = “GMT+8”)
HttpResp.java文件
package com.tianju.redisDemo.dto;import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;import java.io.Serializable;
import java.util.Date;/*** 返回给前端的响应* @param <T>*/
@ApiModel("DTO返回数据")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HttpResp<T> implements Serializable {private ResultCode resultCode;@ApiModelProperty("time")@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",timezone = "GMT+8")private Date time;@ApiModelProperty("results")private T result;public static <T> HttpResp <T> results(ResultCode resultCode,Date time,T results){HttpResp httpResp = new HttpResp();httpResp.setResultCode(resultCode);httpResp.setTime(time);httpResp.setResult(results);return httpResp;}
}
枚举类型的json化
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Getter
ResultCode.java枚举类
package com.tianju.redisDemo.dto;import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;/*** 枚举类型,http请求的返回值*/
// 枚举类型的json化,需要有get方法
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Getter
public enum ResultCode {BOOK_RUSH_SUCCESS(20010,"图书抢购成功"),BOOK_RUSH_ERROR(3001,"图书抢购失败"),LUA_SCRIPT_ERROR(3002,"Lua脚本操作失败"),USER_FIND_ERROR(40010,"非法请求,布隆过滤器不通过"),USER_FIND_SUCCESS(20010,"查询用户名成功"),USER_LOGIN_ERROR(40030,"用户登陆失败"),USER_LOGIN_SUCCESS(20020,"用户登陆成功"),;@ApiModelProperty("状态码")private Integer code;@ApiModelProperty("提示信息")private String msg;private ResultCode(Integer code,String msg){this.code =code;this.msg = msg;}
}
application.yml配置文件
server:port: 9099# token的过期时间是30分钟;redis的过期时间是90分钟
lifespanToken: 6000 # 1000*60*30
lifespanRedis: 20000 # 1000*60*90
MAX_RENEW_TOKEN_TIME: 1 # 最大续期次数spring:# redis的相关配置redis:host: localhostport: 6379database: 0# mysql的相关配置datasource:druid:url: jdbc:mysql://127.0.0.1/community?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 123driver-class-name: com.mysql.cj.jdbc.Driver# 日志需要配置一下
logging:level:com.tianju.redisDemo: debug# mybatis-plus的日志
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# knife需要允许
knife4j:enable: true
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.tianju.jwt</groupId><artifactId>jwt_demo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><!-- 起步依赖--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.13</version></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- redis的包--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- 图片验证码--><dependency><groupId>com.oopsguy.kaptcha</groupId><artifactId>kaptcha-spring-boot-starter</artifactId><version>1.0.0-beta-2</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.11</version></dependency><!-- mysql相关的包--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.18</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><!-- jwt的依赖--><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.3.0</version></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.0.0</version></dependency></dependencies></project>
总结
1.maven打包springboot项目,jar包;
2.windows安装java环境,以及运行jar包;
3.Linux安装java环境,以及运行jar包;
4.运行jar包template might not exist报错及解决;
相关文章:

Jwt(Json web token)——从Http协议到session+cookie到Token Jwt介绍 Jwt的应用:登陆验证的流程
目录 引出从Http协议到session&cookie到TokenHTTP协议session & cookiesessioncookie为什么需要session & cookie? JavaEE传统解决长连接方案问题:分布式不适用解决方案:令牌Token Jwt,Json web tokenjwt的结构Header加密算法Ba…...

Java使用 java.util.regex.Pattern 正则表达式校验参数值是否规范
场景: java中我们可以利用 Pattern 注解对某个入参进行规则校验,但有些特殊参数在接口入口处不方便校验,需要在代码中校验 一、使用 Pattern 注解校验 Pattern(regexp "^[a-zA-Z0-9]$", message "xxx号限输入字母、…...

HDFS基本操作命令
这里写目录标题 HDFS Shell CLI客户端说明常用命令hadoop fs -mkdir [-p] <path>hadoop fs -ls [-h] [-R] [<path>...]上传文件到指定目录下方法一:hadoop fs -put [-f] [-p] <localsrc>.....<dst>方法二:hadoop fs -moveFromLocal <loc…...

git 实操
首先有安装好的git,安装好后,会在任一目录下右键出现git bash和git gui两个选项 打开git bash,设置好全局变量,用户名和邮箱,设置方法为: git config -- global user.name "xxx" git config --global user.email "xxxxxx.com" 1.创建版本库 git init 命…...

Visual Studio Code Python 扩展中的包管理
排版:Alan Wang Python 凭借其简单的语法和强大的库,目前已成为最流行的编程语言之一,也是最适合那些刚接触编程的人们的语言。但是,随着项目复杂性和规模的增长,管理依赖项的复杂性也会增加。当新用户不断承接更成熟的…...
spring学习笔记九
数据源对象管理 1、加入pom坐标 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version></dependency><!-- https://mvnrepository.com/artifact/c3p0/c3p0 --><depe…...
java list stream 使用
1、实现List对象集合的简单去重(distinct()) List<User> list list.stream().distinct().collect(Collectors.toList()); 2、实现List集合的根据属性(name)去重 list list.stream().filter(o -> o.getName() ! …...
两个Ubuntu电脑用SSH远程连接
两个Ubuntu电脑用SSH远程连接 1.ssh客户端及服务端的安装: 打开终端后,只需要以下两个命令即可 sudo apt-get install openssh-clientsudo apt-get install openssh-server2.启动ssh服务,执行以下命令: sudo /etc/init.d/ssh …...

讲解 @ServletComponentScan注解
目录: 1、用法介绍2、实例讲解 1、介绍 在SpringBoot项目启动器中添加ServletComponentScan注解后,SpringBoot在启动时会扫描并注册所有带有WebServlet(控制器)、WebFilter(过滤器)、WebListener(监听器&a…...

20款奔驰S350商务型加装原厂前排座椅通风系统,夏天必备的功能
通风座椅的主动通风功能可以迅速将座椅表面温度降至适宜程度,从而确保最佳座椅舒适性。该功能启用后,车内空气透过打孔皮饰座套被吸入座椅内部,持续时间为 8 分钟。然后,风扇会自动改变旋转方向,将更凉爽的环境空气从座…...

Rust vs Go:常用语法对比(十一)
题目来自 Rust Vs Go: Which Language Is Better For Developing High-Performance Applications?[1] 202. Sum of squares Calculate the sum of squares s of data, an array of floating point values. 计算平方和 package mainimport ( "math")func main() { da…...

Spring MVC拦截器和跨域请求
一、拦截器简介 SpringMVC的拦截器(Interceptor)也是AOP思想的一种实现方式。它与Servlet的过滤器(Filter)功能类似,主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用…...

C++初阶--C++入门
目录 前言C关键字命名空间命名空间的定义命名空间的使用加命名空间名称及作用域限定符使用using namespace 命名空间名称引入使用using将命名空间中的成员引入 C的输入与输出缺省参数全缺省半缺省参数 函数重载参数类型不同参数个数不同参数类型顺序不同 引用引用特性 常引用使…...
Matlab实现PID控制仿真(附上30个完整仿真源码+数据)
本文介绍了如何使用Matlab实现PID控制器的仿真。首先,我们将简要介绍PID控制器的原理和控制算法。然后,我们将使用Matlab编写一个简单的PID控制器,并使用仿真环境来验证其性能。最后,我们将通过调整PID控制器的参数来优化控制系统…...
微信小程序:文件下载
目录 第一步 请求资源 第二步 获取资源后写入到微信本地 获取资源 写入资源(wx.getFileSystemManager)writeFile 的api 第三步 读取资源(openDocument与saveImageToPhotosAlbum) 第一步 请求资源 下面是请求接口中的脚本内容 export let baseUrl http://192.168.78.112…...

QString和QByteArray的区别
QString和QByteArray的区别 本质格式转换QString字符串格式化打印长度 本质 QString是对QByteArray的再次封装 QString可以通过char*来构造,也可以通过QByteArray来构造 QByteArray就是char* QString是编码后的char* QString也是封装了字符串, 但是内部的编码为utf…...

Vue3 Vite electron 开发桌面程序
Electron是一个跨平台的桌面应用程序开发框架,它允许开发人员使用Web技术(如HTML、CSS和JavaScript)构建桌面应用程序,这些应用程序可以在Windows、macOS和Linux等操作系统上运行。 Electron的核心是Chromium浏览器内核和Node.js…...

【Nodejs】Express模板使用
1.Express脚手架的安装 安装Express脚手架有两种方式: 使用express-generator安装 使用命令行进入项目目录,依次执行: cnpm i -g express-generator可通过express -h查看命令行的指令含义 express -hUsage: express [options] [dir] Optio…...

【iOS】App仿写--管理系统
文章目录 前言一、账号界面二、功能界面三、添加功能四、删除功能五、更改功能六、查找功能七、排序功能八、退出功能总结 前言 在日常生活中,如果用文字来记述与管理我们数据会十分麻烦,并且人工成本较高,这里笔者给出一种管理系统的模版&a…...
JS实现队列的数据结构
创建queue.ts /*** 队列*/ export default class Queue<T> {private items: object;private count: number;private header: number;constructor() {this.items {};this.count this.header 0;}/*** 入队列* param element* returns 当前队列的数量*/enqueue(element:…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
内存分配函数malloc kmalloc vmalloc
内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...
Java 语言特性(面试系列1)
一、面向对象编程 1. 封装(Encapsulation) 定义:将数据(属性)和操作数据的方法绑定在一起,通过访问控制符(private、protected、public)隐藏内部实现细节。示例: public …...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

MongoDB学习和应用(高效的非关系型数据库)
一丶 MongoDB简介 对于社交类软件的功能,我们需要对它的功能特点进行分析: 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具: mysql:关系型数据库&am…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...

【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...