JWT
1、什么是JWT
本教程参考:B站视频:“JWT认证原理、流程整合springboot实战应用,前后端分离认证的解决方案!”
网址:https://www.bilibili.com/video/BV1i54y1m7cP?p=3
官网:https://jwt.io/introduction/
JsonWebToken(JWT)是一个开放的标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以Json对象安全的传输信息,此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密(使用HMAC算法)或者使用RSA或ECDSA的公钥/私钥对进行签名。
通俗来说,通过JSON方式作为WEB应用中的令牌,用于在各方之间用JSON对象安全的传输信息。传输过程中,还能进行加密,签名等操作。
2、JWT能做什么
- 授权:这是使用JWT的最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用
- 信息交换:JWT是各方之间传输信息保证安全的好方法,因为它可以进行数据加密,签名等操作。此外,由于签名是使用标头和有效负载计算的,因此你还可以验证签名是否被篡改。
3、为什么是JWT
基于传统的Seesion认证
1、认证方式
我们知道,http协议本身是一种无状态的协议,这意味着如果用户向我们的应用提供了用户名和密码进行用户认证,那么下一次请求,用户还需要重新提交一次用户名与密码,因为根据http协议,我们并不知道是哪个用户发出的请求,为了让我们记住用户的登录信息且能识别出是哪一个用户,我们需要在服务器存放一份用户登录的信息。这份信息会在响应时传递给浏览器,告诉它保存为cookie,以便下一次请求时浏览器将cookie携带上,服务器再通过cookie识别出请求来自哪个用户。这便是传统的Seesion认证。
2、认证流程

3、暴露问题
(1)每个用户经过应用认证之后,我们的应用都要在服务器做一次记录,以便下次方便识别用户。通常Session信息存放在内存中,随着用户访问量越来越大,开销也越来越大。
(2)存在内存中的Session信息,只能被内存所在的服务器读取。这样在分布式的应用上,相应的限制了负载均衡的能力,也限制了应用的扩展能力。
(3)cookie有被截获的风险,服务器有可能会遭受到他人请求伪造的攻击。
(4)在前后端系统中,会更加的痛苦。
前后端系统解耦的同时,增加了部署的复杂性。通常用户的一次请求就会被转发多次。如果采用传统Session方式,除了会出现前面三点提到的问题以外,还有就是SessionID就是一个特征值,表达的信息不够丰富,不容易扩展,同时若果后端应用的事多节点部署,还必须实现Session共享机制。
基于JWT认证

1.认证流程
- 前端通过post请求发送用户名与信息,建议通过SSL加密的传输(https协议),以免敏感信息被嗅探。
- 后端对用户名密码验证成功后,将用户id等其他信息作为JWT Payload(负载),将其头部分别进行Base64编码拼接后签名,形成一个JWT,形成的JWT就是一个形同lll.zzz.xxx的字符串。
- 后端将JWT字符串作为登录成功后的结果返回前端,前端可以将结果保存在localStorage或者SessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时,将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT有效性,例如检查签名是否正确,token是否过期,检查token接收方是否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果
2、jwt优势
- 简洁(Compact):通过UPL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
- 自包含(self-contained):负载中包含了所有用户需要的信息,避免了多次查询数据库
- 因为token以加密的形式保存在客户端的,所以JWT是跨语言的,理论上各种web形式都支持
- 用计算换内存,jwt不像传统Session需要在服务器中保存客户信息,适合分布式。
3、劣势
- 不能主动吊销令牌,只能等令牌过期
- 令牌长度与信息量正相关,携带信息越多,传输开销越大
JWT的组成
JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,编码之后的JWT看起来是这样的一串字符:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
通用格式:XXX.XXX.XXX 对应部分:Header.Playload.Signature
(1)头部(Hader)
jwt头部承载两部分信息:
- 声明类型:这里是JWT
- 声明加密的算法:通常使用的事HMAC SHA256
完整的头部信息如下:
//一段json格式
{
'typ' : 'JWT',
'alg' : 'HS256'
}
头部信息最终会同伙base64加密(该加密可对称解密),构成第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
(2)载荷(payload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明(建议但不强制使用):
- iss: 该JWT的签发者,一般是服务器,是否使用是可选的;
- iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
- exp(expires): 什么时候过期,这里是一个Unix时间戳,这个过期时间必须要大于签发时间,是否使用是可选的;
- aud: 接收该JWT的一方,是否使用是可选的;
- sub: 该JWT所面向的用户,userid,是否使用是可选的;
- nbf:定义在什么时间之前,该JWT都是不可用的;
- jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击;
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个playload:
// 包括需要传递的用户信息;
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.gusibi.com",
"sub": "uid",
"nickname": "goodspeed",
"username": "goodspeed",
"scopes": [ "admin", "user" ]
}
将上面的JSON对象进行Base64编码可以得到JWT的字符串,也就是上面三段式字符串的第二段,我们将它称作JWT的 Payload
eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0
信息会暴露:由于这里用的是可逆的base64 编码,所以第二部分的数据实际上是明文的。我们应该避免在这里存放不能公开的隐私信息。
(3)签名(signature)
// 根据alg算法与私有秘钥进行加密得到的签名字串;
// 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECREATE_KEY
)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
签名的目的:签名实际上是对头部以及载荷内容进行签名。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的密钥的话,得出来的签名也一定会是不一样的。这样就能保证token不会被篡改。
安全相关
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
4、JWT的第一个程序
1、引入依赖
<!--引入JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2、生成token
public static void main(String[] args) {
//生成令牌
HashMap<String,Object> map=new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,20);
String token = JWT.create()
.withHeader(map)//头部
.withClaim("userid",21)//payload
.withClaim("username","废熊")
.withExpiresAt(instance.getTime())//令牌过期时间,使用Calendar生成赋值
.sign(Algorithm.HMAC256("!@#123qwe"));//签名
System.out.println(token);
}
结果:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc1ODQ5OTksInVzZXJpZCI6MjEsInVzZXJuYW1lIjoi5bqf54aKIn0.IcYJQ6I1bFfwXdBaJnOFUwFsCoFYVvyB02IRhmF4S1k
3、认证token
/创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2W#E$RW")).build();
//获得解码后的对象
DecodedJWT verify = jwtVerifier.verify(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDM4NTI0ODUsInVzZXJJZCI6NywidXNlcm5hbWUiOiJsb3VycSJ9.8DFnHQLA1XLlGxJjdwPWapW55OXQzELyncRQUPrapco");
System.out.println(verify.getClaim("userId").asInt());
System.out.println(verify.getClaim("username").asString());
System.out.println("过期时间:"+verify.getExpiresAt());
4、常见异常信息
- SignatureVerificationException:签名不一致异常
- TokenExpiredException:令牌过期异常
- AlgorithmMismatchException:算法不匹配异常
- InvalidClainException:失效的payload异常
5、jwt工具类的封装
为了方便后续开发,将token的两个方法封装起来。第一个方法是将用户传入的信息生成token,第二个方法是验证token。
在写方法之前,首先要将密钥定义为静态变量 SIGN,因为它始终不变,这样写方便方法中使用。
private static final String SIGN = "!Q2W#E$RW";
工具类:
/**
* 生成token header.payload.sign
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7); //默认7天过期
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime())//指定令牌的过期时间
.sign(Algorithm.HMAC256(SIGN));//sign
return token;
}
/**
* 验证token合法性
* 验证成功:返回解码后的对象
* 验证失败:抛出异常
*/
public static DecodedJWT verify(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
6、Springboot整合JWT
需要提供两个接口,用户认证接口和访问前检验token接口。
流程:
- 用户传入username和password(以“zhangsan" "123456"为例),拿着这两个信息去查数据库作认证。若认证通过,基于想要的用户信息生成一个token,然后把token响应给用户。
- 日后用户在请求其他接口的时候,用户都要携带着token请求。若携带的token验证通过,则服务器允许访问;若验证不通过,则服务器拒绝访问。
1、引入依赖,同上,省略
2、建立数据库以及user表,常如一行自定义测试数据。

3、在配置文件上配置mysql和mybatis
4、建立实体类
UserAdmin.java 使用了lombok插件
@Data
public class UserAdmin {
private String id;
private String name;
private String password;
}
5、DAO接口和mapper.xml
@Mapper
public interface UserAdminMapper{
UserAdmin login(UserAdmin userAdmin);
mapper文件
<mapper namespace="com.xxx.gamejump.mapper.UserAdminMapper">
<select id="login" parameterType="UserAdmin" resultType="UserAdmin">
select * from g_admin where name =#{name} and password =#{password}
</select>
</mapper>
6、Service接口以及实现类
@Service
public interface UserAdminService {
/*
管理员登录接口
*/
UserAdmin login(UserAdmin userAdmin);
@Service
@Transactional
public class UserAdminServiceImpl implements UserAdminService{
@Autowired
private UserAdminMapper userAdminMapper;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public UserAdmin login(UserAdmin userAdmin) {
//根据接收的用户名和密码查询数据库
UserAdmin admin = userAdminMapper.login(userAdmin);
if(admin != null){
return admin;
}
throw new RuntimeException("登陆失败--");
}
}
7、controller类
login方法:验证用户名密码,生成token令牌
@RestController
@RequestMapping("/admin")
@CrossOrigin(origins = "*")
public class UserAdminController {
private Logger logger = LoggerFactory.getLogger(UserAdminController.class);
@Autowired
private UserAdminService userAdminService;
@GetMapping("/login")
public Map<String,Object> login(UserAdmin userAdmin){
logger.info("用户名:[{}]",userAdmin.getName());
logger.info("密码:[{}]",userAdmin.getPassword());
Map<String,Object> map = new HashMap<>();
try{
UserAdmin useradminDB = userAdminService.login(userAdmin);
Map<String,String> payload = new HashMap<>();
payload.put("id",useradminDB.getId());
payload.put("name",useradminDB.getName());
//生成JWT令牌,项目中引用前面提到的JWTutil工具类
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);//响应token
}catch(Exception e){
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
首先运行程序,然后用Postman工具测试login方法。
测试1:认证成功示例
GET方式访问:http://localhost:8089/admin/login?name=lrq&password=12345
传参:
name:lrq
password:12345
数据库存在该数据,所以是成功的

8、验证类
只有第一次访问需要从数据库获取信息,在token失效前,用户后面的访问都会携带token,我们需要对其进行验证。
@PostMapping("/test")
public Map<String,Object> test(String token) {
Map<String, Object> map = new HashMap<>();
logger.info("当前token为:[{}]", token);
//System.out.println("当前token为");
try {
DecodedJWT verify = JWTUtils.verify(token); //验证令牌
map.put("state",true);
map.put("msg","请求成功!");
return map;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名!");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期!");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","算法不一致!");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","token无效!");
}
map.put("state",false);
return map;
}
注意:官方建议token隐藏在header请求头中进行传参
8、增加拦截器
问题:
假设应该受保护的接口有10个,那这10个接口除了自己本身的参数之外每次还需要额外传token数据作为参数,每个方法都需要验证token。这样就造成了大量的代码冗余,且不够灵活。
优化/解决方法:
若是JavaWeb项目,可以将jwt验证放在拦截器里。
若是springcloud分布式项目,可以将jwt验证放在网关里
8.1 拦截器
public class JWTInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
//获取请求头中的令牌
String token = request.getHeader("token");
try{
JWTUtils.verify(token);
return true;//放行请求
} catch (SignatureVerificationException e){
e.printStackTrace();
map.put("msg","无效签名!");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期!");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","算法不一致!");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","token无效!");
}
map.put("state",false);//设置状态
//将map 转化为json jackson
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
- 集成HandleInterceptor接口
- 重写preHandle预处理方法
- 官方不建议Jwt以参数传入,而是放入请求头里传入
- 若验证成功,不需要返回携带成功信息的map,返回true即可方形。但是失败,需要返回false和将map以json的方式返回前端。
8.2、拦截器配置类
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/admin/test") //其他接口token验证
.excludePathPatterns("/admin/login"); //所有管理员用户都放行
}
拦截路径:所有接口路径,需要排除用户登录验证用户名密码的方法路径,这个接口是用来验证用户并生成token的,不能拦截。因为是demo,所以这里写的具体一点,拦截test方法,不拦截login方法。
8.3、Controller类
@PostMapping("/test")
public Map<String,Object> test() {
Map<String, Object> map = new HashMap<>();
//处理自己业务逻辑
map.put("state",true);
map.put("msg","请求成功!");
return map;

浙公网安备 33010602011771号