基于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"
}
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的几大特点
- JWT默认是不加密的,但是你也可以进行加密,生成原始的Token以后,可以再使用密钥进行加密
- JWT不加密的情况下,不能讲秘密数据写入JWT
- JWT不仅可以用于认证,也可以用于交换信息,可以有效的使用JWT,可以降低服务器查询数据库的次数,减轻数据库的压力
- JWT最大的缺点是,由于服务端不再保存session状态,因此就无法在使用过程中终止某个Token,或者更改Token的权限,也就是说JWT一旦由服务端签发后,在到期之前就会始终有效,除非服务端中部署了额外的逻辑
- JWT本身包含了认证信息,所以一旦邪路,任何人都可以获得该令牌的所有权限,为了减少盗用,JWT的有效期应该设置得比较短,对于一些比较重要的权限,使用的时候应该对此用户进行二次认证
- 为了减少盗用,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
23 comments
博主你的网站底部私密评论错位了
emmmm,貌似这个版本的handsome就是这样的
哦哦,好的
拦截器的配置呢?
测试
测试
测试
邮件测试
测试
测试
测试
6666
测试
测试
测试
测试回复
CDN测试邮件提示
测试
CDN测试评论
是个大佬,互访
长互访!大佬算不上
在校时就学了个基础,学完感觉不适合程序员方向,就转方向了。不得不说博主还是牛皮的
兄弟过奖了!!还在努力中呢!!