1.什么是JWT
JWT(JSON WEB TOKEN)是一种标准,用来在前后端或者系统间以JSON对象安全的传输信息,该信息是可以被验证和信任的,因为它是数字签名的。可以使用HMAC或RSA或ECDSA的公钥/私钥进行签名。
2.JWT能做什么
- 授权
一旦登录,每个后续请求都包括JWT,用户就可以访问该令牌允许的路由、服务和资源 - 信息交换
可以对JWT签名,确保收件人是对的人,还可以验证内容是否篡改
3.为什么用JWT
3.1 传统登录基于session认证
- 如何实现的
- 客户端发送账号密码到后端
- 账号密码验证通过,用户登录成功以后,返回一个sessionId给客户端,客户端每次请求时候,都携带这个sessionId,就表示它已经登陆过了,不需要再输入用户名密码。
- 存在问题
- 每个用户都要在服务端保存一个sessionId来保存它的登录状态,一般session都是保存在内存中,用户越多,服务器压力越大。
- 用户认证之后,如果session存在对应服务器的内存中,下次用户访问必须还是这台服务器,但是现在分布式的应用,负载均衡后没法确定是访问哪台服务器。
- 基于cookie来识别用户,如果被篡改,容易收到跨站请求伪造攻击
3.2 基于JWT认证
- 如何实现的
- 客户端发送账号密码到后端,
- 账号密码验证通过后,将用户的id等其他信息作为Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token),格式如xxx.xxx.xxx,由
.
分割,三部分组成head,payload,singurater - 客户端接收到JWT后保存在本地,退出登录时,就删除本地的JWT
- 前端每次请求将JWT放在HTTP Header的Atuthorization中,解决XSS和XSRF。
- 后端收到请求,就验证JWT的有效性,是否过期,签名是否正确等。
- 验证通过后,后端使用JWT中包含的用户信息进行对应操作。
- 优势
- 可以通过URL、post参数或header发送,数据量小,传输速度快。
- 负载中包含了用户信息,避免多次查询数据库
- token以JSON加密的形式保存在客户端,所以可以跨语言,原则上任何web形式都支持。
- 不保存在服务端,适合分布式微服务。
4.JWT的结构
令牌的组成
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
格式如xxx.xxx.xxx,由.
分割,三部分组成
4.1 Header
标头,标头通常由两部分组成,令牌的类型(JWT)和所使用的签名算法,例如HMAC、SHA256或RAS。
明文信息,json字符串
{"alg":"HS256","typ":"JWT"}
使用Base64编译为 xxx 这种格式,作为JWT的第一部分
4.2 Payload
有效负载,其中包含声明,声明是有关信息,也会使用Base64转为xxx,作为JWT的第二部分
{"id":"sad854dsfewr1","name":"zhangsan"}
需要注意的是,不要在这里放入敏感信息,例如密码,因为Base64是可逆的
4.3 Signature
签名,对头部及负载内容进行签名,保证JWT没有被篡改
5.JWT的使用
引入依赖
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version></dependency>
创建令牌
@Testvoid contextLoads() {String secretKey="asdf1213$1f.sad"; //密钥//设置时间20sCalendar instance = Calendar.getInstance();instance.add(Calendar.SECOND,20);String token = JWT.create()//.withHeader(map)不设置头的话,默认就是alg:HMAC256typ:JWT.withClaim("id", "asd123456") //设置id.withClaim("name", "zhangsan")//设置用户名.withExpiresAt(instance.getTime())//设置过期时间.sign(Algorithm.HMAC256(secretKey));//使用密钥设置签名System.out.println(token);}
输出结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6ImFzZDEyMzQ1NiIsImV4cCI6MTY5Nzc2OTMxNn0.MwxzzpvvCmCXB6JYWWVsMnRffo4RkpW413aDRndkbCc
验证令牌
这里密钥要和加密的密钥一致,否则签名验证不通过@Testvoid test(){String secretKey="asdf1213$1f.sad"; //密钥//创建验证对象JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secretKey)).build();DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJleHAiOjE2OTc3NzEyMTB9.Ro9YSdTmuqdrisQhXRVDxPT-pv4FZLb5nnQkNRUrMjc");//获取负载中name的值,存什么类型的值,就要使用对应的asInt或者asStringSystem.out.println(verify.getClaim("name").asString());//获取过期时间System.out.println(verify.getExpiresAt());}
常见异常
- SignatureVerificationException 签名不一致
- TokenExpiredException 令牌过期
- AlgorithmMismatchException 算法不匹配
- InvalidClaimException 失效的payload
6.封装JWT工具类
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 SECRET_KEY = "asdf1213$1f.sad";/** * 生成token * @param map 负载中存储的值 * @return生成的token值 */public static String getToken(Map<String, String> map) {//设置过期时间Calendar instance = Calendar.getInstance();instance.add(Calendar.DATE, 7);//默认7天//创建JWT builderJWTCreator.Builder builder = JWT.create();//存储payloadmap.forEach((key, value) -> builder.withClaim(key, value));//signString token = builder.withExpiresAt(instance.getTime())//设置过期时间.sign(Algorithm.HMAC256(SECRET_KEY));//使用密钥设置签名return token;}/** * 验证token * @param token * @return */public static DecodedJWT verify(String token){//创建验证对象return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token);}}
7.SpringBoot整合JWT
目录结构
7.1 搭建环境
springboot+jwt+mybatis
引入依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.19</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
配置文件
server.port=8999spring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/aaa" />package com.jwt.entity;import lombok.Data;import lombok.experimental.Accessors;@Data@Accessors(chain = true)public class User {private String username;private String password;}
Dao和Mapper
- UserDao
package com.jwt.dao;import com.jwt.entity.User;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface UserDao {User login(User user);}
- UserDaoMapper.xml
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.jwt.dao.UserDao"><select id="login" parameterType="User" resultType="User">select * from user where username=#{username} and password=#{password}</select></mapper>
- UserDao
Service
package com.jwt.service;import com.jwt.dao.UserDao;import com.jwt.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.util.ObjectUtils;@Servicepublic class UserServiceImpl {@Autowiredprivate UsersDao usersDao;public Users login(User user){User user = userDao.login(user);if(ObjectUtils.isEmpty(user)){throw new RuntimeException("登录失败");}return user;}}
controller
package com.jwt.controller;import com.auth0.jwt.exceptions.AlgorithmMismatchException;import com.auth0.jwt.exceptions.SignatureVerificationException;import com.auth0.jwt.exceptions.TokenExpiredException;import com.auth0.jwt.interfaces.DecodedJWT;import com.jwt.entity.Users;import com.jwt.service.UserServiceImpl;import com.jwt.utils.JWTUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@Slf4jpublic class UserController {@Autowiredprivate UserServiceImpl userService;@RequestMapping("/user/login")public Map<String,Object> login(Users user){System.out.println(user.getUsername());log.info("username:[{}]",user.getUsername());log.info("password:[{}]",user.getPassword());Map<String,Object> map=new HashMap<>();try {Users userDb = userService.login(user);Map<String,String> payload = new HashMap<>();payload.put("username",userDb.getUsername());payload.put("password",userDb.getPassword());String token = JWTUtils.getToken(payload);map.put("state",true);map.put("msg","登录成功");map.put("token",token);} catch (Exception e) {map.put("state",false);map.put("msg",e.getMessage());}return map;}@RequestMapping("/user/verify")public Map<String,Object> verify(String token){Map<String,Object> map = new HashMap<String,Object>();log.info("token:{}",token);try {DecodedJWT decodedJWT = 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;}}
访问/user/login
,首先从数据库查询,user对象是否存在,如果存在,就生成token,返回给前端,
然后再访问/user/verify
,验证这个token是否有效
7.2 拦截器验证token
如果对每个接口都做token认证,代码冗余太高,单体应用的话,可以使用拦截器验证,分布式应用可以使用网关验证
创建拦截器
token放在请求头header中package com.jwt.interceptors;import com.auth0.jwt.exceptions.AlgorithmMismatchException;import com.auth0.jwt.exceptions.SignatureVerificationException;import com.auth0.jwt.exceptions.TokenExpiredException;import com.auth0.jwt.interfaces.DecodedJWT;import com.fasterxml.jackson.databind.ObjectMapper;import com.jwt.utils.JWTUtils;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.HashMap;import java.util.Map;public class JWTinterceptors implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从 http 请求头中取出 tokenString token = request.getHeader("token");Map<String,Object> map = new HashMap<String,Object>();try {DecodedJWT decodedJWT = JWTUtils.verify(token); //验证令牌map.put("state",true);map.put("msg","验证成功");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转为jsonString json = new ObjectMapper().writeValueAsString(map);response.setContentType("application/json; charset=utf-8");response.getWriter().print(json);return false;}}
配置拦截器
package com.jwt.config;import com.jwt.interceptors.JWTinterceptors;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Component;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTinterceptors()).addPathPatterns("/user/verify") //拦截测试验证接口.excludePathPatterns("/user/login"); //放行登录接口}}
修改7.1中的代码,
- 修改验证接口
/user/verify
,去除手动验证的方法,因为拦截器已经实现了@RequestMapping("/user/verify")public Map<String, Object> verify(String token) {Map<String, Object> map = new HashMap<String, Object>();map.put("state", true);map.put("msg", "请求成功");return map;}
- 修改验证接口
验证
先访问登录接口,登录成功
再访问验证接口,现在token就不是以参数的形式传进去了,需要放在请求头header中,
先验证个没带token的,因为controller的代码已经改了,这个token无效提示不是controller的了,是拦截器中的提示
再验证个正确的token