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字符串
java
{
"alg":"HS256",
"typ":"JWT"
}
使用Base64编译为 xxx 这种格式,作为JWT的第一部分
4.2 Payload
有效负载,其中包含声明,声明是有关信息,也会使用Base64转为xxx,作为JWT的第二部分
java
{
"id":"sad854dsfewr1",
"name":"zhangsan"
}
需要注意的是,不要在这里放入敏感信息,例如密码,因为Base64是可逆的
4.3 Signature
签名,对头部及负载内容进行签名,保证JWT没有被篡改
5.JWT的使用
-
引入依赖
java<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
-
创建令牌
java@Test void contextLoads() { String secretKey="asdf1213$1f.sad"; //密钥 //设置时间20s Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND,20); String token = JWT.create() //.withHeader(map) 不设置头的话,默认就是alg:HMAC256 typ:JWT .withClaim("id", "asd123456") //设置id .withClaim("name", "zhangsan") //设置用户名 .withExpiresAt(instance.getTime()) //设置过期时间 .sign(Algorithm.HMAC256(secretKey));//使用密钥设置签名 System.out.println(token); }
输出结果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6ImFzZDEyMzQ1NiIsImV4cCI6MTY5Nzc2OTMxNn0.MwxzzpvvCmCXB6JYWWVsMnRffo4RkpW413aDRndkbCc
-
验证令牌
这里密钥要和加密的密钥一致,否则签名验证不通过
java@Test void test(){ String secretKey="asdf1213$1f.sad"; //密钥 //创建验证对象 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secretKey)).build(); DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJleHAiOjE2OTc3NzEyMTB9.Ro9YSdTmuqdrisQhXRVDxPT-pv4FZLb5nnQkNRUrMjc"); //获取负载中name的值,存什么类型的值,就要使用对应的asInt或者asString System.out.println(verify.getClaim("name").asString()); //获取过期时间 System.out.println(verify.getExpiresAt()); }
-
常见异常
- SignatureVerificationException 签名不一致
- TokenExpiredException 令牌过期
- AlgorithmMismatchException 算法不匹配
- InvalidClaimException 失效的payload
6.封装JWT工具类
java
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 builder
JWTCreator.Builder builder = JWT.create();
//存储payload
map.forEach((key, value) -> builder.withClaim(key, value));
//sign
String 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
-
引入依赖
xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--JWT依赖--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.0</version> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <!--druid依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.19</version> </dependency> <!--mysql依赖--> <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>
-
配置文件
xmlserver.port=8999 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/aaa?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root mybatis.type-aliases-package=com.jwt.entity mybatis.mapper-locations=classpath:com/jwt/mapper/*.xml
-
实体
javapackage 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
javapackage com.jwt.dao; import com.jwt.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserDao { User login(User user); }
-
UserDaoMapper.xml
xml<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//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>
-
-
Service
javapackage 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; @Service public class UserServiceImpl { @Autowired private UsersDao usersDao; public Users login(User user){ User user = userDao.login(user); if(ObjectUtils.isEmpty(user)){ throw new RuntimeException("登录失败"); } return user; } }
-
controller
javapackage 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 @Slf4j public class UserController { @Autowired private 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中
javapackage 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 { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 http 请求头中取出 token String 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转为json String json = new ObjectMapper().writeValueAsString(map); response.setContentType("application/json; charset=utf-8"); response.getWriter().print(json); return false; } }
-
配置拦截器
javapackage 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; @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTinterceptors()) .addPathPatterns("/user/verify") //拦截测试验证接口 .excludePathPatterns("/user/login"); //放行登录接口 } }
-
修改7.1中的代码,
-
修改验证接口
/user/verify
,去除手动验证的方法,因为拦截器已经实现了java@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