zoukankan      html  css  js  c++  java
  • JWT

    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清空。

    就是这么简单,你学废了吗?感觉有用的话,给笔者点个赞吧 !
  • 相关阅读:
    Core Data
    scrollViews
    网络通信
    UIView
    textView取消键盘
    AFNetworking转载
    多线程
    css3[转载][菜单导航] 带有记忆功能的多页面跳转导航菜单
    jQuery翻牌或百叶窗效果
    jQuery联动日历(三)完成
  • 原文地址:https://www.cnblogs.com/zys2019/p/13734400.html
Copyright © 2011-2022 走看看