Jwt应用实践

1、概述(what)

1.1 定义
	JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs  can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方法,用于在各方之间安全地将信息作为JSON对象传输。 由于此信息是经过数字签名的,因此可以被验证和信任。 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/专用密钥对对JWT进行签名。
1.2 如何构造
JWT包含了三部分,分别使用'.'分割:
	Header(头信息)
	Payload(载荷,或者说数据)
	Signature(签名,附属信息)
JWT的标准样式:
	xxxxx.yyyyy.zzzzz
1.2.1 header
	The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

头信息包含两部分:
	(1) token,也就是 JWT,是Base64编码的Url	
	(2) 加密算法(如 HMAC SHA256 or RSA)
举例:
	{
      "alg": "HS256",
      "typ": "JWT"
	}
1.2.2 payload
	The second part of the token is the payload, which contains theclaims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.

这部分主要是声明,内容如下:
	标准的实体信息,如用户
	附加信息
声明分为三种:
	注册式声明
	公开声明
	私有声明
	
示例:
	{
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }
 Base64 编码
 注意:
	  请注意,对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的有效负载或报头元素中。输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
  • Registered claims
	These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims. Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and others.

	这些是一组非强制性的但建议使用的预定义要求,以提供一组有用的,可互操作的要求。 其中一些是:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。
  • Public claims
	These can be defined at will by those using JWTs. But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.
	JWT使用者可以随意定义声明。 但是为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含抗冲突名称空间的URI。
  • Private claims
	These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.
这些是自定义声明,旨在使用者之间共享信息,既不是注册声明也不是公共声明。
1.2.3 signature
	To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
	要创建签名部分,您必须获取编码的标头,编码的有效载荷,机密,标头中指定
的算法,并对其进行签名。
如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

	签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。

2、工作机理(why)

	在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。 由于令牌是凭据,因此必须格外小心以防止安全问题。 通常,令牌的保留时间不应超过要求的时间。
	由于缺乏安全性,您也不应该将敏感的会话数据存储在浏览器中。
	每当用户想要访问受保护的路由或资源时,用户代理通常应使用承载模式在授权标头
中发送JWT。 标头的内容应如下所示:
Authorization: Bearer <token>
2.1 特点
 - 优势:

(1)让我们谈谈与简单Web令牌(SWT)和安全性声明标记语言令牌(SAML)相比,JSON Web令牌(JWT)的好处。	
(2)由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。 这使得JWT是在HTML和HTTP环境中传递的不错的选择。
(3)在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。 但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。 与签名JSON的简单性相比,使用XML数字签名对XML进行签名而不引入模糊的安全漏洞是非常困难的。
(4)JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。 相反,XML没有自然的文档到对象映射。 与SAML断言相比,这使使用JWT更加容易。
(5)关于用法,JWT是在Internet规模上使用的。 这强调了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。

3、应用场景(who\where\when)

JWT的使用场景

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

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

4、实战(how)

4.1、引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
4.2、给application.yml 添加配置
jwt:
  config:
    key: saas_hrm
    ttl: 3600000
    secret-key: secretKey	
4.3、添加配置类 JwtUtils
   import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.Date;
    import java.util.Map;
    
    /**
     * @author 三多
     * @Time 2020/3/24
     */
    @Getter
    @Setter
    @ConfigurationProperties(prefix = "jwt.config")
    @Configuration
    public class JwtUtils {
        /**签名私钥*/
        private String key;
        /**签名的失效时间*/
        private Long ttl;
        /**签名的失效时间*/
        private String secretKey;
    
        /**
         * 解析token获取claims
         */
        public  Claims parseJwt(String token) {
            Jws<Claims> parseClaimsJws = Jwts.parser()
                    //私钥
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            Claims claims = parseClaimsJws.getBody();
            return claims;
        }
    
        /**
         * 创建token
         *      id:登录用户id
         *      subject:登录用户名
         *  步骤:
         *      1. 设置失效时间
         *      2. 创建jwtBuilder
         *      3. 根据map 设置claims
         *      4.创建token
         */
        public String createJwt(String id,String name,Map<String,Object> params) {
            //1. 设置失效时间
            Long  now  = System.currentTimeMillis();
            long  exp = now + ttl;
            //4.创建token
            return Jwts.builder().setId(id)
                    //2. 创建jwtBuilder
                    .setSubject(name)
                    //签发时间
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(exp))
                    .signWith(SignatureAlgorithm.HS512, secretKey)
                    //3. 根据map 设置claims
                    .addClaims(params)
                    .compact();
        }
    
    
    }
4.4、工具包
4.4.1、ResultCode
 import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    /**
     * 公共返回码
     *
     * @author 三多
     * @Time 2020/3/10
     */
    @Getter
    @AllArgsConstructor
    public enum ResultCode {
        SUCCESS(true, 10000, "操作成功!"),
        /************************系统错误返回码:1XXX***************************/
        FAIL(false, 10001, "操作失败!"),
        UN_AUTHENTICATED(false, 10002, "您还未登录!"),
        UN_AUTHORISE(false, 10003, "没有访问权限!"),
        SERVER_ERROR(false, 99999, "抱歉系统繁忙,请稍后重试!"),
        /************************用户操作返回码:2XXX***************************/
        MOBILE_ERROR_OR_PASSWORD_ERROR(false, 20001, "用户名或者密码错误!");
        /************************企业操作返回码:3XXX***************************/
        /************************权限操作返回码: 4XXX***************************/
        /************************其他操作返回码: 5XXX***************************/
    
        /**
         * 操作是否成功
         */
        boolean success;
        /**
         * 操作代码
         */
        int code;
        /**
         * 提示信息
         */
        String message;
    
    }
    
4.4.2、BusinessException
 import com.hrm.common.entity.ResultCode;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.http.HttpStatus;
    
    import java.io.Serializable;
    
    /**
     * @author 三多
     * @Time 2020/3/13
     */
    public class BusinessException extends RuntimeException implements Serializable {
    
        @Setter
        @Getter
        private int code=HttpStatus.INTERNAL_SERVER_ERROR.value();
    
        @Setter
        @Getter
        private String message;
        public BusinessException(String message) {
            super(message);
            this.message = message;
        }
        public BusinessException(ResultCode resultCode) {
            super(resultCode.getMessage());
            this.message = resultCode.getMessage();
        }
        public BusinessException(String message,Throwable throwable) {
            super(message,throwable);
            this.message = message;
        }
    
        /**
         *
         * @param message 消息
         * @param code 状态码
         */
        public BusinessException(String message, int code) {
            super(message);
            this.code = code;
            this.message = message;
        }
        public  BusinessException(int code,String message ,Throwable throwable){
            super(message,throwable);
            this.code = code;
        }
    }
    
4.5、自定义拦截器
 import com.hrm.common.entity.ResultCode;
    import com.hrm.common.exception.BusinessException;
    import com.hrm.common.utils.JwtUtils;
    import io.jsonwebtoken.Claims;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Objects;
    
    /**
     * 自定义拦截器:
     *     继承:
     *     HandlerInterceptorAdapter
     *      preHandle:进入到控制器之前执行的内容
     *          boolean:
     *              true:可以继续执行控制器方法
     *              false:拦截控制器方法
     *      postHandle:执行控制器方法之后执行的内容
     *      afterCompletion:响应结束之后执行的内容
     * @author 三多
     * @Time 2020/3/26
     */
     @Component
    public class JwtIntercept extends HandlerInterceptorAdapter {
    
        @Autowired
        private JwtUtils jwtUtils;
    
        /**
         * 1、通过拦截器获取token
         *      统一的用户权限校验(是否登录)
         *         a. 通过request获取token信息
         *         b. 从token中获取claims
         *         c. 将claims 绑定到request域中
         * 2、判断当前用户是否具有当前访问接口的权限
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //a. 通过request获取token信息
            String authorization = request.getHeader("Authorization");
            //判断请求头是否为空,或者以Bearer开头
            if(! StringUtils.isEmpty(authorization)&& authorization.startsWith("Bearer")){
                //获取token
                String token = authorization.replace("Bearer ", "");
                //解析token 获取 claims
                Claims claims = jwtUtils.parseJwt(token);
                if(Objects.nonNull(claims)){
                    /**
                     * 获取当前用户可访问的API权限字符串,
                     *      a. 获取handlerMethod
                     *      b. 获取注解
                     *      c. 获取注解的名称
                     *      d. 判断是否包含
                     *          包含就赋权
                     *          否则提示没有权限
                     */
                    //获取当前用户可访问的API权限字符串
                    String apis = String.valueOf(claims.get("apis"));
                    if(handler instanceof HandlerMethod) {
                        HandlerMethod handlerMethod = (HandlerMethod) handler;
                        DeleteMapping methodAnnotation = handlerMethod.getMethodAnnotation(DeleteMapping.class);
                        String name = methodAnnotation.name();
                        if(apis.contains(name)){
                            request.setAttribute("user_claims",claims);
                            return true;
                        }else{
                            throw new BusinessException(ResultCode.UN_AUTHORISE);
                        }
                    }
                }
            }
            throw new BusinessException(ResultCode.UN_AUTHENTICATED);
    
        }
    
    }
4.6、注册拦截器
package com.hrm.system.config;

import com.hrm.common.intercept.JwtIntercept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 三多
 * @Time 2020/3/27
 */
@Configuration
public class SysConfig implements WebMvcConfigurer {
    @Autowired
    private JwtIntercept jwtIntercept;

    /**
     * 添加拦截器配置
     *      1. 添加自定义拦截器
     *      2. 指定拦截器的URL地址
     *      3. 指定不拦截器的URL地址
     *  造成问题:
     *      延迟加载no session
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //1. 添加自定义拦截器
        registry.addInterceptor(jwtIntercept)
                //2. 指定拦截器的URL地址
                .addPathPatterns("/**")
                //3. 指定不拦截器的URL地址
                .excludePathPatterns("/system/login","/frame/register/**");
    }

    /**
     * jpa EntityManager 懒加载:解决no session
     * 防止 session失效
     *
     * 延迟关闭session到 view 层
     * @return
     */
    @Bean
    public OpenEntityManagerInViewFilter openEntityManagerInViewFilter(){
        return new OpenEntityManagerInViewFilter();
    }
}
4.7、源码地址

github源码地址

5、参考

  1. JWT官网参考文档
  2. 10分钟了解JSON Web令牌(JWT)
  3. RFC 定义
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐