浅谈JWT(JSON Web Token )及其应用(登录验签)

1前言
1.1. JWT 介绍

  • JSON Web Token(JWT)是一个开放式标准(RFC 7519),它定义了一种紧凑(Compact)且自包含(Self-contained)的方式,用于在各方之间以JSON对象安全传输信息。 这些信息可以通过数字签名进行验证和信任。 可以使用秘密(使用HMAC算法)或使用RSA的公钥/私钥对对JWT进行签名。

1.2. JWT 特点

  • 由于它们尺寸较小,JWT可以通过URL,POST参数或HTTP标头内发送。 另外,尺寸越小意味着传输速度越快。
  • 有效载荷(Playload)包含有关用户的所有必需信息,避免了多次查询数据库。

2应用场景

  • Authentication(鉴权):这是使用JWT最常见的情况。 一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。 单点登录是当今广泛使用JWT的一项功能,因为它的开销很小,并且能够轻松地跨不同域使用。
  • Information Exchange(信息交换):JSON Web Tokens是在各方之间安全传输信息的好方式。 因为JWT可以签名:例如使用公钥/私钥对,所以可以确定发件人是他们自称的人。 此外,由于使用标头和有效载荷计算签名,因此您还可以验证内容是否未被篡改。

3 JWT的结构:在紧凑的形式中,JWT包含三个由点(.)分隔的部分,它们分别是

  • Header 头
  • Payload 负载
  • Signature 签名
//下面这种形式 x代表头,y代表负载,z代表签名
xxxxx.yyyyy.zzzzz

3.1分别对JWT三部分进行讲解
3.11 Header:通常由两部分组成:令牌的类型,即JWT。和常用的散列算法,如HMAC SHA256或RSA。

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

3.2Payload:这里放声明内容,可以说就是存放沟通讯息的地方,在定义上有3种声明(Claims):

  • Registered claims(注册声明):这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。
  • Public claims(公开声明):这些可以由使用JWT的人员随意定义。 但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含防冲突命名空间的URI。
  • Private claims(私有声明):这些是为了同意使用它们但是既没有登记,也没有公开声明的各方之间共享信息,而创建的定制声明。
//对于已签名的令牌,此信息尽管受到篡改保护,但任何人都可以阅读。 除非加密,否则不要将秘密信息放在JWT的有效内容或标题元素中。所以在负载中不要存放用户的密码等敏感信息。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

3.3Signature:第三部分signature用来验证发送请求者身份,由前两部分加密形成。要创建签名部分,您必须采用编码标头,编码有效载荷,秘钥,标头中指定的算法并签名。
4JWT工作原理:在身份验证中,当用户使用他们的凭证成功登录时,JSON Web Token将被返回并且必须保存在本地(通常在本地存储中,但也可以使用Cookie),而不是在传统方法中创建会话 服务器并返回一个cookie。
执行流程如下图:
浅谈JWT(JSON Web Token )及其应用(登录验签)_第1张图片
5项目开发中的代码编写

5.1代码适用场景:当用户第一次登陆的时候产生token返回给前端,后续访问服务器时前端把token放到Header中,后端进行解析查看是否正确,正确放行,错误返回登陆页面。

5.2引入依赖

- 引入依赖前先多嘴两句:第一句,jwt的框架有很多,第二句,今天介绍一个现在比较流行的
- 参考网址:(https://www.jianshu.com/p/dfa089448348)
//此实验基于springboot2.1.5版本
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>7.1</version>
       </dependency>

5.3登录逻辑


    /**
     * 登录
     *
     * @param dto
     * @author Lys
     * @date: 4:25 下午 2019/5/29
     * 
     * @version: 1.2.6
     */
    @Override
    public SysUserVO login(SysUserLoginDTO dto) {
        SysUserNew sysUserNew = SysUserNew.builder().phoneNumber(dto.getPhoneNumber()).build();
        sysUserNew = userNewMapper.selectOne(sysUserNew);
        SysUserVO sysUserVO = BeanUtils.copyProperties(sysUserNew, SysUserVO.class);
        // 如果为 null,该账号不存在
        if (sysUserVO == null) {
            ExceptionHandler.publish("1204");
        }
        String password = dto.getPassword();
        if (!password.equals(sysUserNew.getPassword())) {
            ExceptionHandler.publish("1206");
        }
        /**
         * 以上是对登录账号的正确性进行逻辑判断
         * 以下是对账号登录正确后生成token的代码
         */
        //assert sysUserVO != null;
        String token = TokenUtils.createToken(sysUserVO.getPhoneNumber());
        sysUserVO.setToken(token);
        TokenCache tokenCache = BeanUtils.copyProperties(sysUserVO, TokenCache.class);
        //ip()是判断是否是同一ip登录,根据功能需要也可以去掉
        tokenCache.setIp(ip());
        log.info("缓存用户信息{}", JSON.toJSONString(tokenCache));
        //存入redis,一手机号为key
        stringRedisTemplate.opsForValue().set(sysUserVO.getPhoneNumber(), JSON.toJSONString(tokenCache));
        //把带有token的对象返回前端
        return sysUserVO;
    }

5.4生成token的通用代码


public class TokenUtils {
 /**
     * Function: Payload负载信息的一些设置,为生成token做准备;不仅可以添加示例代码中的三条,还可以添           加好多条,负载里的属性可以解析出来
     *
     * @return Token
     * @author Zhang
     **/
    public static String createToken(String uid) {
        Map<String, Object> map = new HashMap<>(3);
        //token过期时间;表示now向后推迟1小时
       LocalDateTime now= LocalDateTime.now();
            //当前时间向后推迟1小时
            LocalDateTime localDateTime = now.plusHours(1);
        map.put("uid", uid);
        map.put("startTime", now.getTime());
        map.put("endTime",  exp.getTime());
        try {
            return creatToken(map);
        } catch (JOSEException e) {
            log.error("生成token失败", e);
        }
        return null;
    }

 /**
     * Function: 创建token
     *
     * @return token
     * @author Zhang
     **/
    public static String creatToken(Map<String, Object> payloadMap) throws JOSEException {
        // 先建立一个头部Header
        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);

        // 建立一个载荷Payload
        Payload payload = new Payload(new JSONObject(payloadMap));

        // 将头部和载荷结合在一起
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);

        // 建立一个密匙
        JWSSigner jwsSigner = new MACSigner(getFinalSecret());

        // 签名
        jwsObject.sign(jwsSigner);

        // 生成token
        return jwsObject.serialize();
    }

  /**
     * Function: 根据token解析获取mobile信息
     *
     * @author Zhang
     **/
    public static String validTokenGetMobile(String token) {
        try {
            // 解析token
            JWSObject jwsObject = JWSObject.parse(token);

            // 获取到载荷
            Payload payload = jwsObject.getPayload();

            // 建立一个解锁密匙
            JWSVerifier jwsVerifier = new MACVerifier(getFinalSecret());

            if (jwsObject.verify(jwsVerifier)) {
                JSONObject jsonObject = payload.toJSONObject();
                String uid = jsonObject.get(Constant.UID.getChineseMsg()).toString();
                if (StringUtil.isNotEmpty(uid)) {
                    return uid;
                }
            }
        } catch (ParseException e) {
            logger.error("解析TOKEN出错",e);
        } catch (JOSEException e) {
            logger.error("解析TOKEN出错",e);
        }
        return null;
    }
    
 /**
     * Function: 获取加密后的密钥
     *
     * @return secret
     * @author Zhang
     **/
    private static String getFinalSecret() {
        StringBuffer sb = new StringBuffer(MD5Util.md5(SECRET)).append(MD5Util.md5(MD5Util.md5(SECRET)));
        return sb.toString();
    }
}

5.5做请求拦截器

package com.xm.hardwaremanagement.interception;


import com.alibaba.fastjson.JSONObject;
import com.xm.hardwaremanagement.exception.ExceptionHandler;

import com.xm.hardwaremanagement.util.StringUtil;
import com.xm.hardwaremanagement.util.ThreadLocalUtils;
import com.xm.hardwaremanagement.util.dto.SysUserDTO;
import com.xm.hardwaremanagement.util.redis.RedisService;
import com.xm.hardwaremanagement.util.tkmybatis.BeanUtils;
import com.xm.hardwaremanagement.util.tokenUtil.ITokenVerification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.util.*;

import static com.xm.hardwaremanagement.common.RedisKey.token;

/**
 * 登录拦截器
 *
 * @author csw
 * @date 2020-05-026
 */

@Component
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    @Resource
    private RedisService redisService;

    @Resource
    private ITokenVerification iTokenVerification;

    @Override

    @SuppressWarnings("unchecked")
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //获取请求头中token数据
        String token = request.getHeader("Authorization");
        if (StringUtil.isEmpty(token)) {
            //非平台端发起请求不做校验
            return true;
        }
        //解析token数据中手机号
        String phoneNumber = TokenUtils.validTokenGetMobile(token);
        if (StringUtil.isEmpty(phoneNumber)) {
            ExceptionHandler.publish("1012");
        }
        String object = redisService.get(phoneNumber);
        log.info("object:{}", object);
        SysUserDTO sysUserDTO = JSONObject.parseObject(object, SysUserDTO.class);
        //获取ip(根据功能需求要求取舍)
        String ip = getIpAddress(request);
        if (!ip.equals(sysUserDTO.getIp())) {
            //请求ip与登录ip不一致 直接拒绝访问
            ExceptionHandler.publish("1010");
        }
        if (!token.equals(sysUserDTO.getToken())) {
            ExceptionHandler.publish("1010");
        }
        return true;
    }


    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {

    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
       
    }


}


5.6 配置拦截规则

@Configuration
public class WebAppConfig extends WebMvcConfigurationSupport {
@Autowired
    LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      
        List<String> patterns = new ArrayList<>();
        patterns.add("/userLogin/getAuthenticationCode");
        patterns.add("/userLogin/register");
        patterns.add("/userLogin/forgetPassword");
        patterns.add("/userLogin/login");
        patterns.add("/equipmentRemote/**");
        patterns.add("/deviceManagement/thirdDeleteEquipment");
        patterns.add("/deviceManagement/settingFaceCapacity");
        patterns.add("/deviceManagement/judgeEquipmentStatus");
        patterns.add("/deviceManagement/selectEquipmentCount");
        patterns.add("/app-release.apk");
        patterns.add("/employee/**");
        patterns.add("/thirdPlatform/**");
        patterns.add("/thirdParty/bindThirdParty");
        patterns.add("/swagger-resources/**");
        patterns.add("/swagger-ui.html/**");
        
         // addInterceptor 使用自定义拦截器的某一个
         // addPathPatterns 用于添加拦截规则
        // excludePathPatterns 用户排除拦截
        
        registry.addInterceptor(logInterceptor).addPathPatterns(patterns)
        .excludePathPatterns("/wheatSunshine/auth");
       
       //拦截配置生效
        super.addInterceptors(registry);
    }
}

5.7附加一个类:获取请求ip地址

package com.xm.hardwaremanagement.base;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.function.Function;

public abstract class Base{

    /**
     * 得到request对象
     */
    public static HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     * 参考文章: http://developer.51cto.com/art/201111/305181.htm
     * 

* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢? * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。 *

* 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, * 192.168.1.100 *

* 用户真实IP为: 192.168.1.110 */ public static String getIpAddress() { HttpServletRequest request=getRequest(); String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }

  • 完结

你可能感兴趣的:(功能开发记录,代码模板,其他,java)