菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
487
0

JWT

原创
05/13 14:22
阅读数 23062

1.概述

1.1定义

jwt(json web token)也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

1.2主要功能

1)授权

这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

2)信息交换
JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对)所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

2.基本原理

2.1jwt的认证流程

1)首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
2)后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)形成的JWT就是一个形同l11.Zzz.xxx的字符串。
3)后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
4)前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)HEADER
5)后端检查是否存在,如存在验证JWT的有效性。
6)验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

2.2令牌组成

header.payload .singnature。组成的是一个字符串。

1)标题(Header)

由两部分组成∶令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
格式如下,是固定的。

{
"alg":"HS256",
"typ":"JWT"
}

2)有效载荷(Payload)

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。它也会使用Base64编码组成JWT结构的第二部分。

3)签名(Singnature)
signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过

3.项目实战

源码:https://github.com/zhongyushi-git/springboot-jwt.git

3.1项目准备

本项目是在springboot的基础上开发的,结合了redis。简单起见,登录并没有使用数据库来验证用户信息。

1)首先,新建一个springboot项目,导入redis、fastjson等坐标以及redis工具类。

2)然后编写登录接口LoginController,实现简单的登录功能。

3)再开发一个接口UserController,用于后面测试。

3.2实战演练

1)导入坐标

<!--jwt-->
<dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.4.0</version>
</dependency>

2)编写配置信息

#忽略的url,以逗号分隔
system.IgnoreUrl=/login/login

#jwt相关配置
#配置请求头中token名称
jwt.config.header=token
#加密的秘钥
jwt.config.secret=asdfghjkl123..
#token有效时长,单位是分钟
jwt.config.expire=30

3)编写工具类

package com.zys.springbootjwt.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zhongyushi
 * @date 2020/9/22 0022
 * @dec 描述
 */
@Component
public class JWTUtil {

    @Value("${jwt.config.secret}")
    private String SING;

    @Value("${jwt.config.expire}")
    private Integer expire;

    @Value("${jwt.config.header}")
    private String header;

    /**
     * 生成token
     * @param map
     * @return
     */
    public String createToken(Map<String,String> map){

        //创建jwt构建器
        JWTCreator.Builder builder= JWT.create();

        //设置token的过期时间
        Calendar calendar=Calendar.getInstance();
        //默认是30分钟
        calendar.add(Calendar.MINUTE,expire);
        builder.withExpiresAt(calendar.getTime());

        //设置payload,存储需要的一些参数
        map.forEach((key,value)->{
            builder.withClaim(key,value);
        });

        //加密后生成token
        String token = builder.sign(Algorithm.HMAC256(SING));
        return token;
    }

    /**
     * 验证token,验证通过返回参数信息
     * @param token
     * @return
     */
    public DecodedJWT verifyToken(String token){
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SING)).build();
        //验证token,如果验证失败会抛出异常
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT;
    }

    /**
     * 获取登录的用户信息
     * @param request
     * @return
     */
    public Map<String,Object> getLoginUser(HttpServletRequest request){
        //获取请求头信息
        String token = request.getHeader(header);
        DecodedJWT decodedJWT = verifyToken(token);
        //获取payload中设置的参数
        Map<String, Claim> claims = decodedJWT.getClaims();
        Map<String,Object> result=new HashMap<>();
        result.put("username",claims.get("username").asString());
        return result;
    }
}

4)编写拦截器

package com.zys.springbootjwt.config;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.exceptions.*;
import com.auth0.jwt.interfaces.Claim;
import com.zys.springbootjwt.util.JWTUtil;
import com.zys.springbootjwt.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @author zhongyushi
 * @date 2020/9/22 0022
 * @dec jwt拦截器,验证token
 */
@Component
public class JWTInterceptor implements HandlerInterceptor {

    @Value("${jwt.config.header}")
    private String header;

    @Autowired
    private JWTUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    //在请求之前进行拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头信息
        String token = request.getHeader(header);
        //从redis获取token副本,解决退出问题。当用户退出后,清空redis的token
        boolean keyIsExists = redisUtil.keyIsExists(header);
        if (keyIsExists) {
            String redisToken = redisUtil.getValue(header);
            //判断副本和原token是否相同
            if (!redisToken.equals(token)) {
                token = null;
            }
        } else {
            token = null;
        }
        Map<String, Object> result = new HashMap<>();
        //如果token为空直接返回错误信息
        if (StringUtils.isEmpty(token)) {
            result.put("msg", "未授权,无法访问资源!");
        } else {
            try {
                //验证token
                jwtUtil.verifyToken(token);
                //验证通过就放行
                return true;
            } catch (SignatureVerificationException e) {
                //签名不一致
                result.put("msg", "抱歉,签名不一致!");
            } catch (TokenExpiredException e) {
                //token过期
                e.printStackTrace();
                result.put("msg", "抱歉,token已过期!");
            } catch (AlgorithmMismatchException e) {
                //验证算法不一致
                e.printStackTrace();
                result.put("msg", "抱歉,验证算法不一致!");
            } catch (InvalidClaimException e) {
                //payload失效
                e.printStackTrace();
                result.put("msg", "抱歉,token已失效!");
            } catch (Exception e) {
                //其他异常
                e.printStackTrace();
                result.put("msg", "认证失败,无法访问资源!");
            }
        }

        result.put("status", false);
        //验证不通过,就给浏览器返回错误信息
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(result));
        return false;
    }
}

5)配置拦截器

package com.zys.springbootjwt.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author zhongyushi
 * @date 2020/9/22 0022
 * @dec 拦截器配置
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private JWTInterceptor interceptor;

    //从配置文件读取忽略的url,不拦截这些请求
    @Value("${system.IgnoreUrl}")
    private String ignoreUrl;


    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //addPathPatterns表示拦截所有请求,excludePathPatterns表示不拦截的请求
        registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(ignoreUrl);
    }
}

6)修改登录接口,加入jwt

 @PostMapping("/login")
    public JSONObject login(User user) {
        boolean u = loginService.login(user);
        JSONObject json = new JSONObject();
        if (u) {
            //设置payload中存储的参数,方便在后台获取
            Map<String,String> params=new HashMap<>();
            params.put("username",user.getUsername());
            //生成token并返回
            String token = jwtUtil.createToken(params);
            //保存token副本到redis
            redisUtil.setValue(header,token);
            json.put("msg","登录成功");
            json.put("status",true);
            json.put("token",token);
        }else{
            json.put("msg","用户名或密码错误");
            json.put("status",false);
        }
        return json;
    }

7)在登录中添加退出登录接口

@GetMapping("/logout")
    public JSONObject logout(){
        //删除缓存信息
        redisUtil.deleteKey(header);
        JSONObject json = new JSONObject();
        json.put("msg","退出成功");
        json.put("status",true);
        return json;
    }

8)测试

启动项目,使用postman进行测试。

第一,使用get请求访问http://localhost:8080/api/user/name会返回认证失败,原因就是没登录不能直接访问。

第二,使用post方式访问http://localhost:8080/login/login,需要携带用户名和密码参数。正确后会返回一个token。

第三,在请求头携带token使用get请求去访问http://localhost:8080/api/user/name会返回正确的数据。

第四,携带token使用get请求去访问http://localhost:8080/login/logout会返回退出成功。

第五,再次携带token使用get请求去访问http://localhost:8080/api/user/name会返回认证失败,原因是用户已退出。

测试都是模拟进行的,如果是前端直接请求后台,也是类似的,登录之后把token放到请求头中,才能去访问资源。退出时,不仅仅要请求后台,还要把前端的token清空。

发表评论

0/200
487 点赞
0 评论
收藏