基于SSM框架集成JWT(Json Web Token)实现对RestFul接口进行权限验证

JWT介绍

JWT验证流程

原理

JWT的原理是,服务器认证以后,生成一个JSON对象,发回给用户,就像下面这样:

{
  "userName": "Seale",
  "role": "管理员",
  "end_date": "2099年7月1日0点0分"
}

之后,客户端再与服务端进行通信的时候就会在请求头中带上这一段信息,服务端靠头部的这段Json对象信息来判断你是否具备权限来进行某类操作,同时,服务端在生成这个对象的时候会加上签名

实现的结构

如图所示,这是一个很长的字符串,中间用.来将他们分割成三个部分,分别为Header(头部),Payload(负载),Signature(签名)

也就是如下模式

header.payload.signature

Header(头部)

Header部分是一个JSON对象,通常是这个样子的

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

其中alg属性表示签名的算法,默认为HMAC SHA256(写成HS256),typ属性表示这个令牌的类型,JWT令牌统一写成JWT

Payload(负载)

Payload部分也是一个JSON对象,用来存放实际需要传递的数据,JWT中规定了7个官方字段,仅供选用

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

你也可以在这个部分定义私有字段,下面就是一个例子

{
    "sub":"test",
    "name":"seale",
    "admin":true,
    "date":"2019年4月27日 17:21:20"
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分

Signature(签名)

Signature是对前两个部分的签名,防止数据被篡改

首先需要制定一个密钥,这个密钥只能是服务器才知道,不能邪路给用户,然后使用Header里面指定的签名算法(默认HS256),按照下面的公式产生签名

算出签名以后,把 Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret);

Base64URL

Header和Payload串型化的算法是Base64URL,这个算法跟Base64算法基本类似,但是也有一些小的不同

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如api.example.com/?token=xxx),Base64中有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略,+替换成-,/替换成_这就是Base64URL算法

JWT的食用方式

客户端收到服务器返回的JWT,可以存储到Cookie里面,也可以存储到内存中

此后,客户端每次和服务端进行通信,都要带上这个JWT,你可以将他放在Cookie里面自动发送,但是这样做得劣势就是不能跨域,所以更好的做法就是放在HTTP请求头中的Authorization字段中

Authorization: Bearer <token>

另外一种做法就是,将JWT放在POST请求的数据体里面

JWT的几大特点

  1. JWT默认是不加密的,但是你也可以进行加密,生成原始的Token以后,可以再使用密钥进行加密
  2. JWT不加密的情况下,不能讲秘密数据写入JWT
  3. JWT不仅可以用于认证,也可以用于交换信息,可以有效的使用JWT,可以降低服务器查询数据库的次数,减轻数据库的压力
  4. JWT最大的缺点是,由于服务端不再保存session状态,因此就无法在使用过程中终止某个Token,或者更改Token的权限,也就是说JWT一旦由服务端签发后,在到期之前就会始终有效,除非服务端中部署了额外的逻辑
  5. JWT本身包含了认证信息,所以一旦邪路,任何人都可以获得该令牌的所有权限,为了减少盗用,JWT的有效期应该设置得比较短,对于一些比较重要的权限,使用的时候应该对此用户进行二次认证
  6. 为了减少盗用,JWT不应该使用HTTP协议进行明码传输,应该使用HTTPS协议进行传输

在SSM中集成JWT

Maven引入

在pom.xml文件中加入以下配置

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

实例

我们先看看效果,下面为我通过程序生成的两个Token,一个为永久授权,一个授权为1小时

//授权为一小时
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoidGVzdCIsImlzcyI6IkVlbmR0ZWNoIiwiYXVkIjoiRWVuZHRlY2giLCJpYXQiOjE1NTY0MzkzMzgsImV4cCI6MTU1NjQ0MjkzOH0.xh_t5B86EFzuTbC7QTVEAfHGsXroG3s80FzASUH9WXE
//永久授权
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoidGVzdCIsImlzcyI6IkVlbmR0ZWNoIiwiYXVkIjoiRWVuZHRlY2giLCJpYXQiOjE1NTY0Mzk1Nzl9.rm6HowWMSiB_t_1qtolMfQwpM5rkG0W7pAc0T40QZHM

其对应的解析为

//一小时授权
//header:ALGORITHM & TOKEN TYPE
{
  "typ": "JWT",
  "alg": "HS256"
}
//Payload:DATA
{
  "jti": "1",
  "sub": "test",
  "iss": "Eendtech",
  "aud": "Eendtech",
  "iat": 1556439338,
  "exp": 1556442938
}
//永久授权
//header:ALGORITHM & TOKEN TYPE
{
  "typ": "JWT",
  "alg": "HS256"
}
//Payload:DATA
{
  "jti": "1",
  "sub": "test",
  "iss": "Eendtech",
  "aud": "Eendtech",
  "iat": 1556439579
}

我们可以清晰的看到,两者之间的不同为exp字段,那么这个字段是做什么的呢?我们回顾一下上文

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

exp字段为过期时间,将它格式化日期之后过期时间为 Apr 28 2019 17:15:38 那么这就可以对应,刚好一小时

在客户端请求中,通过将Token放在请求头Header中来交给服务端完成权限验证,下面我们通过PostMan来模拟一次Token签发请求和一次操作请求

我们通过Post来提交登录请求

我们可以看到,在登录帐号成功之后,服务端会返回data,其中就为服务端签发的Token令牌,之后我们可以根据场景的不同,如果在PC或者H5下将这个令牌保存到浏览器的Cookies中方便随时使用,如果在APP等应用程序中,可以直接保存在内存

之后我们通过PostMan进行一些数据库请求操作,我们在Header中带上Authorization,根据JWT规范,官方建议我们采用如下方式进行保存Token令牌

Authorization: Bearer <token>

下面一种情况是Token令牌出错,我们更改中间部分Payload,删除其中几个字符,之后再提交操作请求,那么验证Token会出现异常,返回如下内容

代码实现

构建工具类

package eendtech.utils;

import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import eendtech.utils.Constants.TokenConstant;
import io.jsonwebtoken.*;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;

/**
 * @ author Seale
 * @ Description:JWT工具类JSON WEB TOKEN
 * @ QQ:1801157108
 * @ Date 2019/4/27 18:51
 */
public class JwtUtils {
    /**
     * 签发JWT
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id , String subject , long ttlMillis){
        //加密算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //密钥
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setSubject(subject) //主题
                .setIssuer(TokenConstant.ISSUER)//签发者
                .setAudience(TokenConstant.ISSUER)//受众(接收方)
                .setIssuedAt(now)//签发时间
                .signWith(signatureAlgorithm,secretKey)//签名算法和密钥
                .setHeaderParam("typ","JWT");

        if (ttlMillis > 0){
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate);//设置过期时间
        }
        return builder.compact();
    }

    /**
     * 验证JWT
     * @param jwt
     * @return
     */
    public static CheckResult validateJWT(String jwt) {
        //生成返回类
        CheckResult checkResult = new CheckResult();
        Claims claims = null;
        try {
            claims = parseJWT(jwt);
            if (claims == null){
                checkResult.setSuccess(false);
                checkResult.setErrCode(TokenConstant.JWT_ERRCODE_FAIL);
            } else if (claims.getAudience().equals(TokenConstant.ISSUER)){
                checkResult.setSuccess(false);
                checkResult.setErrCode(TokenConstant.JWT_ERRCODE_FAIL);
            }else if (claims.getIssuer().equals(TokenConstant.ISSUER)){
                checkResult.setSuccess(false);
                checkResult.setErrCode(TokenConstant.JWT_ERRCODE_FAIL);
            }
            checkResult.setSuccess(true);
            checkResult.setClaims(claims);
        }catch (ExpiredJwtException e){
            //JWT过期
            checkResult.setSuccess(false);
            checkResult.setErrCode(TokenConstant.JWT_ERRCODE_EXPIRE);
        }catch (SignatureException e){
            //签名错误
            checkResult.setSuccess(false);
            checkResult.setErrCode(TokenConstant.JWT_ERRCODE_FAIL);
        }

        return checkResult;
    }

    /**
     * 构建密钥
     * @return SecretKey
     */
    public static SecretKey generalKey()  {
        //密钥解密
        byte[] encodedKey = Base64.decode(TokenConstant.JWT_SECERT);
        //生成密钥
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt){
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();

    }
}
package eendtech.utils;

/**
 * @ author Seale
 * @ Description:
 * @ QQ:1801157108
 * @ Date 2019/4/28 10:26
 */
public enum ResultCode {
    SUCCESS(200),//成功
    WRONG_UP(201),//帐号密码不存在
    FAIL(400),//失败
    TIME_OUT(401),//超时
    UNAUTHORIZED(401),//未认证(签名错误)
    NOT_FOUND(404),//不存在
    INTERNAL_SERVER_ERROR(500),//服务器内部错误
    TOKEN_ERROR(701),//token错误
    TOKEN_MISS(702);//token失效

    private final int code ;
    ResultCode (int code){this.code = code ; }
    public int code (){
        return code ;
    }

}
package eendtech.utils;

import lombok.Getter;
import lombok.Setter;

/**
 * @ author Seale
 * @ Description: 统一API响应结果工具包
 * @ QQ:1801157108
 * @ Date 2019/4/28 10:29
 */
@Getter@Setter
public class ResultKit<T> {
    private int code ;
    private String message ;
    private T data ;

}
package eendtech.utils.Constants;

/**
 * @ author Seale
 * @ Description:Token常量集
 * @ QQ:1801157108
 * @ Date 2019/4/27 19:05
 */
public class TokenConstant {
    //密钥
    //原始码 eendtech_asgduiashduiAASDWEFHTFGsdhioasd89498411894914648987_%^&&^*^()*()&5201314
    public static final String JWT_SECERT = "ZWVuZHRlY2hfYXNnZHVpYXNoZHVpQUFTRFdFRkhURkdzZGhpb2FzZDg5NDk4NDExODk0OTE0NjQ4OTg3XyUyNSU1RSUyNiUyNiU1RSolNUUlMjglMjkqJTI4JTI5JTI2NTIwMTMxNA==";
    //签发者
    public static final String ISSUER = "Eendtech";
    /****************************************************************/
    //JWT过期错误
    public static final Integer JWT_ERRCODE_EXPIRE = 85201;
    //JWT验证失败
    public static final Integer JWT_ERRCODE_FAIL = 85202;
    /****************************************************************/
    //请求头
    public static final String HEADER_TOKEN = "Authorization";
    //请求头内容前缀
    public static final String HEADER_TOKEN_PREFIX = "Bearer ";
}

之后是控制器

package eendtech.controller;

import eendtech.dao.UserDao;
import eendtech.po.User;
import eendtech.utils.JwtUtils;
import eendtech.utils.ResultCode;
import eendtech.utils.ResultKit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;

/**
 * @ author Seale
 * @ Description:
 * @ QQ:1801157108
 * @ Date 2019/4/28 10:20
 */
@Controller
@RequestMapping("/login")
public class LoginController {
    @Autowired
    UserDao userDao ;

    @ResponseBody
    @PostMapping("/login")
    public ResultKit<String> login(String username , String password , HttpServletResponse response){
        User user = this.userDao.findUser(username,password);
        if (user != null){
                String JWT = JwtUtils.createJWT("1",username,0);
                ResultKit<String> resultKit = new ResultKit<>();
                resultKit.setCode(ResultCode.SUCCESS.code());
                resultKit.setData(JWT);
                resultKit.setMessage("登录成功");
                return resultKit;

        }else{
            ResultKit<String> resultKit = new ResultKit<>();
            resultKit.setCode(ResultCode.WRONG_UP.code());
            resultKit.setMessage("失败");
            return resultKit;
        }
    }

}

这里是数据操作

 @PostMapping(value = "/info")
    @ResponseBody
    public ResultKit info (@RequestBody User user, HttpServletRequest request){
        //进行验证
        String Token = request.getHeader(TokenConstant.HEADER_TOKEN);
        if (Token != null){
            int subLength = TokenConstant.HEADER_TOKEN_PREFIX.length();
            Token = Token.substring(subLength);
            CheckResult result = JwtUtils.validateJWT(Token);
            if (result.isSuccess()){
                User user_info = userService.findUser(user.getUser_code(),user.getUser_password());
                ResultKit<User> resultKit =  new ResultKit<>();
                resultKit.setCode(ResultCode.SUCCESS.code());
                resultKit.setMessage("成功获取信息");
                resultKit.setData(user_info);
                return resultKit;
            }else {
                ResultKit<String> resultKit =  new ResultKit<>();
                resultKit.setCode(ResultCode.UNAUTHORIZED.code());
                resultKit.setMessage("错误:Token授权失败,或许是授权过期,或许是Token错误");
                resultKit.setData("没有权限进行操作");
                return resultKit;
            }

        }else{
            ResultKit<String> resultKit =  new ResultKit<>();
            resultKit.setCode(ResultCode.FAIL.code());
            resultKit.setMessage("错误:没有检测到Token");
            resultKit.setData("没有权限进行操作");
            return resultKit;
        }
    }

以上便简单的构建了服务端Token授权和验证机制,当然在实际项目中需要更进一步的优化

参考

JSON Web Token 入门教程 BY:阮一峰

Java JWT: JSON Web Token for Java and Android from Github BY:Github JWT

How to Create and verify JWTs in Java

JWT


本作品采用知识共享署名 4.0 国际许可协议进行许可。

如果可以的话,请给我钱请给我点赞赏,小小心意即可!

Last modification:February 7, 2020
If you think my article is useful to you, please feel free to appreciate