JWT简介
什么是JWT
JWT(Json Web Token)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于在各方面之间以JSON对象安全地传输信息,此信息可以验证和信任,因为它是数字签名的,jwt可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名
简单理解: JWT简称Json Web Token, 也就是通过JSON形式作为Web应用中的令牌, 用于在各方之间安全地将信息作为对象传输, 在数据传输过程中还可以完成数据加密、签名等相关处理
JWT能做什么
授权: 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
**信息交换: ** 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
JWT的认证流程
-
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程- -般是一 个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议) ,从而避免敏感信息被嗅探。
-
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload (负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同11. zzz. xxx的字符串。token head . payload . singurater
-
后端将JWT字符串作为登录成功的返回结果返回给前端。 前端可以将返回的结果保存在localStorage或sessionStorage上, 退出登录时前端删除保存的JWT即可。
-
前端在每次请求时将JWT放入HTTP Header中的Authorization位。 (解决XSS和XSRF问题)
-
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)
-
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
JWT的优势
-
简洁(Compact): 可以通过URL,POST参 数或者在HTTP header发送,因为数据量小,传输速度也很快
-
**自包含(Self-contained): **因为Token是 以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
-
因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
-
不需要在服务端保存会话信息,特别适用于分布式微服务
JWT的结构是什么
⚔️ 令牌组成
1. **标头(Header)**
2. **有效载荷(Payload)**
3. **签名(Signature)**
因此,JWT通常如下所示: xxxx.yyyy.zzzz Header.payload.signature
⚔️ Header
标头通常由两部分组成:令牌的类型(即JWT) 和所使用的签名算法,例如HMAC SHA256或RSA。 它会使用Base64 编码组成JWT 结构的第一部分。
注意:Base64是一 种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
{
"alg" : "HS256" //使用的加密算法
"typ" : "JWT" //类型
}
⚔️ Payload
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64 编码组成JWT结构的第二部分(不要在payload里放敏感信息 如:用户密码等)
{
"sub" : "HS256"
"name" : "wang"
"admin" : "true"
}
⚔️ signature
-
前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256) 进行签名。签名的作用是保证JWT没有被篡改过
HMACSHA256 (base64Ur1Encode(header) + "." + base64Ur1Encode(payload) , secret);
⚔️ 签名的目的
- 最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负葳的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
JWT适合用于向Web应用传递一些非敏感信息, JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登陆
原始数据未加密
输出: 是由三个点分隔的Base64-URL字符串, 可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑
- 简洁(Compact): 可以通过URL,POST参 数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained): 负载中包含了多有用户所需要的信息, 避免了多次查询数据库
JWT实战
-
导入依赖
<!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
-
编写测试类进行测试
@Test public void test(){ //HashMap<String, Object> map = new HashMap<>(); Calendar instance = Calendar.getInstance();//日历类 instance.add(Calendar.SECOND,100); String token = JWT.create() //.withHeader(map) //header(可以不写默认就是这个) .withClaim("userId",314) //payload .withClaim("username","ahui") .withExpiresAt(instance.getTime()) //指定令牌的过期时间 .sign(Algorithm.HMAC256("onlylmf")); //签名 ("密钥") System.out.println(token); } @Test public void test1(){ //创建验证对象 JWTVerifier onlylmf = JWT.require(Algorithm.HMAC256("onlylmf")).build(); DecodedJWT verify = onlylmf.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTUyNTI5ODYsInVzZXJJZCI6MzE0LCJ1c2VybmFtZSI6ImFodWkifQ.nIib7kjGvqOEZ6fqWOypkKEd_Hy02ReQfgaLbtnaKEw"); System.out.println(verify.getClaims().get("userId").asInt()); System.out.println(verify.getClaims().get("username").asString()); System.out.println(verify.getExpiresAt()); //过期时间 }
**注: ** 存入Int类型数据时不要以0开头的数据,解密出来不一样
常见的异常信息
- SignatureVerificationException: 签名不一致异常
- TokenExpiredException: 令牌过期异常
- AlgorithmMismatchException: 算法不匹配异常
- InvalidClaimException: 失效的payload异常
封装工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static final String SIGNATURE = "onlylmf"; //密钥
/**
* 生成token header.plyload.signature
*/
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);
});
//signature
String token = builder.withExpiresAt(instance.getTime())//指定令牌的过期时间
.sign(Algorithm.HMAC256(SIGNATURE)); //签名 ("密钥")
return token;
}
/**
* 验证token 验证合法性
*/
public static void verify(String token){
//创建验证对象
JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
/**
* 获取token信息方法
*/
public static DecodedJWT getTokenInfo(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
return verify;
}
}
整合Springboot
引入依赖
<!--Springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--Druid数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.3</version>
</dependency>
创建数据库表
用于用的是之前项目所以自行创建数据库 (用的之前ems_vue项目的emp中的用户表)
-
新建实体类
-
开发DAO和Mapper.xml
User findByUserName(String username); ------------------------------------------- <!--findByUserName--> <select id="findByUserName" parameterType="String" resultType="User"> select id,username,realname,password,sex,status,regsterTime from t_user where username=#{username} </select>
-
开发Service和ServiceImpl
//用户登录 User login(User user); -------------------------- @Override public User login(User user) { //1. 根据用户输入的用户名查询 User byUserName = userDao.findByUserName(user.getUsername()); //可以用工具类判断 if (!ObjectUtils.isEmpty(byUserName)){ //2. 比较密码 if (user.getPassword().equals(byUserName.getPassword())){ return byUserName; }else { throw new RuntimeException("密码输入错误"); } }else { throw new RuntimeException("账号输入错误"); } }
-
创建Controller
/** * 用来处理用户登录 */ @PostMapping("login") public Map<String,Object> login(@RequestBody User user){ log.info("当前用户信息为: [{}]",user.toString()); Map<String,Object> map = new HashMap<>(); try { User userDB = userService.login(user); //生成payload 将想要加入的数据put进map集合 HashMap<String, String> payload = new HashMap<>(); payload.put("id",userDB.getId()); payload.put("username",userDB.getUsername()); //生成JWT令牌 String token = JWTUtils.getToken(payload); map.put("state",true); map.put("msg","验证成功,登录完成"); map.put("user",userDB); map.put("token",token); } catch (Exception e) { e.printStackTrace(); map.put("state",false); map.put("msg",e.getMessage()); } return map; } @PostMapping("/user/test") public Map<String,Object> test(String token){ Map<String,Object> map = new HashMap<>(); log.info("当前token为:[{}]",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","token算法不一致"); }catch (Exception e){ e.printStackTrace(); map.put("msg","token无效"); } map.put("state",false); return map; }
**问题: ** 使用上述方式每次都要传递token数据,每个方法都需要验证token冗余,不够灵活,应该如何优化
Web项目应该使用拦截器进行优化
-
创建interceptors包下的JWTInterceptor类
public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HashMap<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","token算法不一致"); }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; } }
-
在config包下创建拦截器InterceptorConfig
//拦截器 @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**") //拦截所有路径 .excludePathPatterns("/user/**"); //排除用户相关路径 } }
-
然后之前写在Controller中的方法就可以不用了
/** * 用来处理用户登录 */ @PostMapping("login") public Map<String,Object> login(@RequestBody User user){ log.info("当前用户信息为: [{}]",user.toString()); Map<String,Object> map = new HashMap<>(); try { User userDB = userService.login(user); //生成payload 将想要加入的数据put进map集合 Map<String, String> payload = new HashMap<>(); payload.put("id",userDB.getId()); payload.put("username",userDB.getUsername()); //生成JWT令牌 String token = JWTUtils.getToken(payload); map.put("state",true); map.put("msg","验证成功,登录完成"); map.put("user",userDB); map.put("token",token); } catch (Exception e) { e.printStackTrace(); map.put("state",false); map.put("msg",e.getMessage()); } return map; } @PostMapping("/user/test") public Map<String,Object> test(HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //处理自己的业务逻辑 String token = request.getHeader("token"); DecodedJWT verify = JWTUtils.verify(token); log.info("用户id: [{}]",verify.getClaim("id").asString()); log.info("用户username: [{}]",verify.getClaim("username").asString()); map.put("state", true); map.put("msg", "请求成功!"); return map; } ``