目录
1、JWT的结构
JWT 最后的形式就是个字符串,它由头部、载荷与签名这三部分组成,中间以「.」分隔。像下面这样:
Header:
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
Payload:
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
Signature:
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
如:
第三部分内容:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),私钥);
签名目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
如果JWT的前2部分的内容被修改了,服务器接收到token后,会先获取前两部分的内容+私钥加密后和token中的第三部分的签名进行对比。如果一致就是合法的
信息安全问题:
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏 感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第 三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
2、使用JWT
1、在用户登录网站的时候,需要输入用户名、密码或者短信验证的方式登录,登录请求到达服务端的时候,服务端对账号、密码进行验证,然后计算出 JWT 字符串,返回给客户端。
2、客户端拿到这个 JWT 字符串后,存储到 cookie 或者 浏览器的 LocalStorage 中。
3、再次发送请求,比如请求用户设置页面的时候,在 HTTP 请求头中加入 JWT 字符串,或者直接放到请求主体中。
4、服务端拿到这串 JWT 字符串后,使用 base64的头部和 base64 的载荷部分加上私钥,通过HMACSHA256
算法计算签名部分,比较计算结果和传来的签名部分是否一致,如果一致,说明此次请求没有问题,如果不一致,说明请求过期或者是非法请求。
保证安全性的关键就是 私钥和HMACSHA256
或者与它同类型的加密算法,因为加密过程是不可逆的,所以不能根据传到前端的 JWT 传反解到密钥信息。另外,不同的头部和载荷加密之后得到的签名都是不同的,所以,如果有人改了载荷部分的信息,那最后加密出的结果肯定就和改之前的不一样的,所以,最后验证的结果就是不合法的请求。
私钥如果不小心泄露会怎么样?
如果单纯的依靠 JSON Web Token 解决用户认证的所有问题,那么系统的安全性将是脆弱的。由于 JWT令牌存储于客户端中,一旦客户端存储的令牌发生泄露事件或者被攻击,攻击者就可以轻而易举的伪造用户身份去修改/删除系统资源,按 JWT 自带过期时间,但在过期之前,攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是 JWT 的优势,同时也是最大的弊端——它太过于依赖算法。
为了更大程度上防止被强盗盗取,应该使用 HTTPS 协议而不是 HTTP 协议,这样可以有效的防止一些中间劫持攻击行为。
3、封装工具类
3.1、引入依赖
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
3.2、生成token
@Test
public void makeToken(){
//1、日历类
Calendar calendar=Calendar.getInstance();
//2、设置时间为 1小时过期
calendar.add(Calendar.SECOND,60);
Map<String,Object> map=new HashMap<>();
//3、生成令牌
String token = JWT.create()
.withHeader(map)//第一部分 头使用默认值 签名算法:HS256 type:jwt
.withClaim("username", "rk")//第二部分
.withClaim("age", 20)
.withIssuedAt(new Date(System.currentTimeMillis()))//设置令牌发放时间
.withExpiresAt(calendar.getTime())//设置过期时间
.sign(Algorithm.HMAC256("LUOLIN!@$%#@"));//第三部分 设置签名,LUOLIN!@$%#@为秘钥
//4、输出令牌
System.out.println(token);
}
3.3、解析token
//解析token
@Test
public void parseToken(){
try {
String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTYzNzk5OTAsImlhdCI6MTY1NjM3OTkzMCwiYWdlIjoyMCwidXNlcm5hbWUiOiJyayJ9.-Pcm-dox8IG2ZT_gve7ApVHCOvM1M9mHDicgDBm8H0k";
//1、创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("LUOLIN!@$%#@")).build();
//2、校验签名(校验token是否合法)
DecodedJWT jwt = jwtVerifier.verify(token);
System.out.println("用户名:"+jwt.getClaim("username").asString());
System.out.println("年龄:"+jwt.getClaim("age").asInt());
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("令牌发放时间:"+sdf.format(jwt.getIssuedAt()));
System.out.println("令牌过期时间:"+sdf.format(jwt.getExpiresAt()));
}
catch (TokenExpiredException e){
System.out.println("token已过期!");
}
catch (JWTVerificationException e) {
System.out.println("token不合法!");
}
}
常见异常:
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
3.4、封装工具类
public class JWTUtils {
private static String TOKEN = "token!Q@W3e4r";
/**
* 生成token
* @param map //传入payload
* @return 返回token
*/
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
}
/**
* 验证token
* @param token
* @return
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
}
4、整合pringboot
登录Controller:
@GetMapping("/user/login")
public Map<String,Object> login(User user){
Map<String, Object> map = new HashMap<>();
try{
//通过用户名和密码查找用户
User userDB = userService.login(user);
Map<String,String> payload = new HashMap<>();
payload.put("id",userDB.getId());
payload.put("name",userDB.getName());
//生成JWT的令牌
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;
}
登录Service:
@Autowired
private UserDAO userDAO;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
//根据接收用户名密码查询数据库
User userDB = userDAO.login(user);
if(userDB!=null){
return userDB;
}
throw new RuntimeException("登录失败");
}
拦截器:
拦截器配置:
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())//将jwt拦截器添加进去
.excludePathPatterns("/user/login")//放行登录接口
.addPathPatterns("/**"); //拦截接口
}
}
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","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;
}
}
拦截器的作用主要就是校验token 是否合法,每次发起请求前都会先通过拦截器校验token。
测试接口:
@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("用户name: [{}]",verify.getClaim("name").asString());
map.put("state",true);
map.put("msg","请求成功!");
return map;
}
拦截器用来校验token,其他接口就只需要获取token后来解析其中的值,处理自己的业务逻辑就行了
未登录的情况下访问其它接口会被拦截器拦截。
登录成功后返回token给前端界面。
登录成功后携带token继续访问其它接口,拦截器校验通过。test接口也获取解析出来了token中携带的值。
5、前端页面解析token
//jwt的token解码方式:
//首先拿到token码然后以点为分隔符转为数组
let token=localStorage.getItem(‘token’).split(".");
console.log(token);
//拿到第二段token也就是负载的那段 进行window.atob方法的 base64的解算,
然后再用decodeURIComponent字符串解码方法 解析出字符串 然后再转成JSON对象
由于atob()方法解码无法对中文解析 所以要再用escape()方法对其重新编码
然后再用decodeURI解码方式解析出来
let str=token[1];
let user=JSON.parse(decodeURIComponent(escape(window.atob(str))));
console.log(user.username);