JWT是一种用户双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT(Json Web Token)作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法用于通信双方之间以Json对象的形式进行安全性信息传递,传递时有数字签名所以信息时安全的,JWT使用RSA公钥密钥的形式进行签名。
JWT组成
JWT格式的输出是以.分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。(形式:xxxxx.yyy.zzz):
1、Header:头部
2、Payload:负载
3、Signature:签名
Header
在header中通常包含了两部分,Token类型以及采用加密的算法
Payload
Token的第二部分是负载,它包含了Claim,Claim是一些实体(一般都是用户)的状态和额外的数据组成。
Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
JWT工作流程图
JWT客户端发送请求到服务器端整体流程:
引入依赖
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
初始化信息
DROP TABLE IF EXISTS `api_token_infos`;
CREATE TABLE `api_token_infos` (
`ati_id` int(255) NOT NULL AUTO_INCREMENT,
`ati_app_id` varchar(100) DEFAULT NULL,
`ati_token` blob,
`ati_build_time` varchar(20) DEFAULT NULL COMMENT '生成token时间(秒单位)',
PRIMARY KEY (`ati_id`),
KEY `ati_app_id` (`ati_app_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8 COMMENT='api请求token信息表,根据appId保存';
-- ----------------------------
-- Records of api_token_infos
-- ----------------------------
INSERT INTO `api_token_infos` VALUES ('29', 'c2R4bWtqX21vYmlsZTQ4QTkxMzAwMTkyQzJFMTgzODc0N0NFMzk4MTREM0ZG', 0x65794A68624763694F694A49557A49314E694A392E65794A7A645749694F694A6A4D6C4930596C6430635667794D585A5A6257787A576C52524E4646556133684E656B46335456527265564636536B5A4E564764365430526A4D453477546B5A4E656D73305456525352553077576B63694C434A70595851694F6A45304F5449344E7A51784F545573496D6C7A63794936496B397562476C755A53425A515856306143424364576C735A475679496977695A586877496A6F784E446B794F4467784D7A6B3166512E30464251735536635A53796F46695371316F6F775737634D345358756A3944644439795544324956736B34, '1492874196023');
-- ----------------------------
-- Table structure for api_user_infos
-- ----------------------------
DROP TABLE IF EXISTS `api_user_infos`;
CREATE TABLE `api_user_infos` (
`aui_app_id` varchar(100) NOT NULL COMMENT '授权唯一标识',
`aui_app_secret` blob NOT NULL COMMENT '授权密钥',
`aui_status` char(1) NOT NULL DEFAULT '1' COMMENT '用户状态,1:正常,0:无效',
`aui_day_request_count` int(11) NOT NULL COMMENT '日请求量',
`aui_ajax_bind_ip` varchar(100) DEFAULT NULL COMMENT '绑定IP地址多个使用“,”隔开',
`aui_mark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`aui_app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='api平台用户信息表';
-- ----------------------------
-- Records of api_user_infos
-- ----------------------------
INSERT INTO `api_user_infos` VALUES ('c2R4bWtqX21vYmlsZTQ4QTkxMzAwMTkyQzJFMTgzODc0N0NFMzk4MTREM0ZG', 0x6D4B6B377237494A6B632B52566A765068334D34504736564947556C6744376A4F6F3356776B484A4B534F4C395179643742573159496E764A582F4E324D4B52584A412F626B33524A3532444E376E4B41376464393251642B2B75712B33775342355359303871492F4F787038524979445635513679614B7541786E304767374566792F4374587562537A4A496D394748675878676D3079523135615573386358434B414C62544D34734E5632694F73544E616C3138395843395363457A5A323042576C4D4E4742637A676D4C7A3368464B71624F6E2F46384465535A3043395A43664137322B4A6B6A5A72723168775765537868465A473071706D6666355A4631324C736843383639682B374F707A6238466359655469716B5A7245596E71666663367A557659303533505368584C37644D38466E4546414F4749393457644A5041547936786C456C62664D492F6954412B4371513D3D, '1', '0', '*', '测试用户');
Model
model、JpaRepository、Controller省略。。。。
public class TokenResult implements Serializable{
//状态
private boolean flag = true;
//返回消息内容
private String msg = "";
//返回token值
private String token ="";
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
生成Token:
生成Token方法的内容大致是,检查appId以及appSecret-->检查是否存在该appId的对应Token-->根据存在与否、过期与否执行更新或者写入操作-->返回用户请求。
在createNewToken方法中是JWT生成Token的方法,我们默认了过期时间为7200秒,上面是毫秒单位,我们生成token需要指定subject也就是我们的用户对象,设置过期时间、生成时间、还有签名生成规则等。token生成方法已经编写完成,下面我们需要在除了获取token的路径排除在外拦截所有的路径,验证路径是否存在header包含token,并且验证token是否正确,jwt会自动给我们验证过期,如果过期会抛出对应的异常。
@RestController
@RequestMapping(value = "/jwt")
public class TokenController
{
@Autowired
private TokenJPA tokenJPA;
@Autowired
private UserInfoJPA userInfoJPA;
/**
* 获取token,更新token
* @param appId 用户编号
* @param appSecret 用户密码
* @return
*/
@RequestMapping(value = "/token", method = {RequestMethod.POST,RequestMethod.GET})
public TokenResult token
(
@RequestParam String appId,
@RequestParam String appSecret
)
{
TokenResult token = new TokenResult();
//appId is null
if(appId == null || appId.trim() == "")
{
token.setFlag(false);
token.setMsg("appId is not found!");
}
//appSecret is null
else if(appSecret == null || appSecret.trim() == "")
{
token.setFlag(false);
token.setMsg("appSecret is not found!");
}
else
{
//根据appId查询用户实体
UserInfoEntity userDbInfo = userInfoJPA.findOne(new Specification<UserInfoEntity>() {
@Override
public Predicate toPredicate(Root<UserInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), appId));
return null;
}
});
//如果不存在
if (userDbInfo == null)
{
token.setFlag(false);
token.setMsg("appId : " + appId + ", is not found!");
}
//验证appSecret是否存在
else if (!new String(userDbInfo.getAppSecret()).equals(appSecret.replace(" ","+")))
{
token.setFlag(false);
token.setMsg("appSecret is not effective!");
}
else
{
//检测数据库是否存在该appId的token值
TokenInfoEntity tokenDBEntity = tokenJPA.findOne(new Specification<TokenInfoEntity>() {
@Override
public Predicate toPredicate(Root<TokenInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), appId));
return null;
}
});
//返回token值
String tokenStr = null;
//tokenDBEntity == null -> 生成newToken -> 保存数据库 -> 写入内存 -> 返回newToken
if(tokenDBEntity == null)
{
//生成jwt,Token
tokenStr = createNewToken(appId);
//将token保持到数据库
tokenDBEntity = new TokenInfoEntity();
tokenDBEntity.setAppId(userDbInfo.getAppId());
tokenDBEntity.setBuildTime(String.valueOf(System.currentTimeMillis()));
tokenDBEntity.setToken(tokenStr.getBytes());
tokenJPA.save(tokenDBEntity);
}
//tokenDBEntity != null -> 验证是否超时 ->
//不超时 -> 直接返回dbToken
//超时 -> 生成newToken -> 更新dbToken -> 更新内存Token -> 返回newToken
else
{
//判断数据库中token是否过期,如果没有过期不需要更新直接返回数据库中的token即可
//数据库中生成时间
long dbBuildTime = Long.valueOf(tokenDBEntity.getBuildTime());
//当前时间
long currentTime = System.currentTimeMillis();
//如果当前时间 - 数据库中生成时间 < 7200 证明可以正常使用
long second = TimeUnit.MILLISECONDS.toSeconds(currentTime - dbBuildTime);
if (second > 0 && second < 7200) {
tokenStr = new String(tokenDBEntity.getToken());
}
//超时
else{
//生成newToken
tokenStr = createNewToken(appId);
//更新token
tokenDBEntity.setToken(tokenStr.getBytes());
//更新生成时间
tokenDBEntity.setBuildTime(String.valueOf(System.currentTimeMillis()));
//执行更新
tokenJPA.save(tokenDBEntity);
}
}
//设置返回token
token.setToken(tokenStr);
}
}
return token;
}
/**
* 创建新token
* @param appId
* @return
*/
private String createNewToken(String appId){
//获取当前时间
Date now = new Date(System.currentTimeMillis());
//过期时间
Date expiration = new Date(now.getTime() + 7200000);
return Jwts
.builder()
.setSubject(appId)
//.claim(YAuthConstants.Y_AUTH_ROLES, userDbInfo.getRoles())
.setIssuedAt(now)
.setIssuer("Online YAuth Builder")
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS256, "HengYuAuthv1.0.0")
.compact();
}
}
configuration
@Configuration
public class JWTConfiguration extends WebMvcConfigurerAdapter
{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtTokenInterceptor()).addPathPatterns("/api/**");
}
}
interceptor
Claims就是我们生成Token是的对象,我们把传递的头信息token通过JWT可以逆转成Claims对象,并且通过getSubject可以获取到我们用户的appId。
public class JwtTokenInterceptor implements HandlerInterceptor
{
/**
* 请求之前
* @param request 请求对象
* @param response 返回对象
* @param o
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
//自动排除生成token的路径,并且如果是options请求是cors跨域预请求,设置allow对应头信息
if(request.getRequestURI().equals("/token") || RequestMethod.OPTIONS.toString().equals(request.getMethod()))
{
return true;
}
//其他请求获取头信息
final String authHeader = request.getHeader("X-YAuth-Token");
try {
//如果没有header信息
if (authHeader == null || authHeader.trim() == "") {
throw new SignatureException("not found X-YAuth-Token.");
}
//获取jwt实体对象接口实例
final Claims claims = Jwts.parser().setSigningKey("HengYuAuthv1.0.0")
.parseClaimsJws(authHeader).getBody();
//从数据库中获取token
TokenInfoEntity token = getDAO(TokenJPA.class,request).findOne(new Specification<TokenInfoEntity>() {
@Override
public Predicate toPredicate(Root<TokenInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), claims.getSubject()));
return null;
}
});
//数据库中的token值
String tokenval = new String(token.getToken());
//如果内存中不存在,提示客户端获取token
if(tokenval == null || tokenval.trim() == "") {
throw new SignatureException("not found token info, please get token agin.");
}
//判断内存中的token是否与客户端传来的一致
if(!tokenval.equals(authHeader))
{
throw new SignatureException("not found token info, please get token agin.");
}
}
//验证异常处理
catch (SignatureException | ExpiredJwtException e)
{
//输出对象
PrintWriter writer = response.getWriter();
//输出error消息
writer.write("need refresh token");
writer.close();
return false;
}
//出现异常时
catch (final Exception e)
{
//输出对象
PrintWriter writer = response.getWriter();
//输出error消息
writer.write(e.getMessage());
writer.close();
return false;
}
return true;
}
/**
* 根据传入的类型获取spring管理的对应dao
* @param clazz 类型
* @param request 请求对象
* @param <T>
* @return
*/
private <T> T getDAO(Class<T> clazz,HttpServletRequest request)
{
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
return factory.getBean(clazz);
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
验证
我们在拦截器中配置的无论是不存在token还是token需要刷新都是返回"need refresh token"错误信息
可以看到我们将之前获取的token作为请求header(X-YAuth-Token)的值进行传递,再次访问127.0.0.1:8080/api/index,就可以成功的获取接口返回的数据。
注意:如果Token过期,再次访问/jwt/token地址传入对应的appId以及appSecret就可以获取一条新的token,也会对应的更新数据库token信息表的内容。