JWT结合Springboot+shiro,session、token同时存在来应对不同的业务场景(物联网设备管理及开放api)
一、背景说明
需求是这样滴:对物联网终端设备以及网关设备进行统一的管理,这里需要一个设备管理平台,同时呢,计划开放API,以供应用开发者调用API来管理控制设备。设备管理平台本身的用的是传统的session来管理,设备管理者数量并不多,所以不会有超量的session给服务器造成太大的压力。开放API给第三方应用用户是为了应对第三方用户开发的各种移动端app以及需要自身维护的设备管理。所以用session就不是那么合适,计划采取token的方式。
多年以前我用过token这种方式来开发,那时候似乎还没有jwt这个框架,我记得是根据用户名密码生成token后存在数据库中的,每次token进来是需要从数据库中或者提前缓存的token池中来找到匹配的token以确保不是非法请求。
闲话多了,看看正题。
二、JWT以及JJWT介绍
首先呢,我们可以通过这里来看看JWT是个什么样的东西:JSON Web Token Introduction - jwt.io 官方说的很清楚了,我就用我蹩脚的英文来给大家解释下:
1、什么是JSON Web Token?
JSON Web Token (JWT)是一个开放的标准(RFC 7519),它定义了一种简洁独立的方式,以JSON对象的形式在各方之间安全地传输信息。
2、什么时候使用JWT呢?
授权和信息交换的时候
3、JWT结构介绍
JWT说白了,就是一串字符串,包含三个部分,三部分之间用“.”来分割。三部分分别是:
-
Header
-
Payload
-
Signature
最后形成的字符串就像这样:xxxxx.yyyyy.zzzzz
Header大概就是这样的:
{
"alg": "HS256",
"typ": "JWT"
}
payload就是放内容的,官方叫做claims,这个是啥玩意呢?这玩意是声明一些实体,包括jwt自己已经定义好的特色的声明,还有一些用户加上的声明(我们这些开发者想加上的)以及一些附加数据
这玩意有三种类型,分别是 *registered* , *public* , and *private* claims. Registered Claims就是官方已经定义了的,比如: **iss** (issuer), **exp** (expiration time), **sub** (subject), **aud** (audience) public呢,就是自己可以随意定义了,要注意避免命名空间的冲突,https://www.iana.org/assignments/jwt/jwt.xhtml。 private就是几方之间约定的,没有注册public的claims。感觉说多了自己都晕。
说白了就是一些key value,大概是这个样子的:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
signature是签名喽,就是你要发这些,你签个字再发,大概就是这个样子滴。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
最终形成这么个玩意:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
4、JJWT是啥?
呃,就是Java JSON Web Token。JWT的一个java实现,如果是做Java开发的直接用JJWT得了。
三、和springboot整合
作为一个正常的开发者,和springboot整合这种事情的第一反应就是添加依赖,先把jar之类的搞起来再说,下面这个不用说了吧,spring的pom文件中添加依赖,如果看这个蒙圈的话,请学习springboot……相关内容。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
然后我就在想,我要的效果是:
1)我的客户在我的平台上注册一个账户。
2)然后通过这个账户创建一个APP,平台会根据规则(你自己定)生成一个appEUI(app全球唯一编码)和一个appSecret,把appEUI和appSecret在页面上展示给客户。
3)这个时候告诉客户,你要想访问平台各种设备接口,那么首先用appEUI和appSecret生成token吧!然后访问的时候把这个token放在httpheader里,我服务端收到请求的时候会监控的啦。(在拦截器中)。
嗯……应该就是这样了。
得有个Token的生成和解析的TokenService吧,就是我需要生成token的时候,调用一把这个service,然后把结果给请求者。
@Service
public class TokenService {
/**
* 有效期7天
*/
private static final int EXPIRE_TIME = 7;
/**
* 盐
*/
private static final String signingKey = "secret";
/**
* 创建token
* @param appEUI
* @param appSecret
* @return
*/
public String createToken(String appEUI,String appSecret){
//签发时间
Date iatTime = new Date();
//expire time
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.DATE,7);
Date expireTime = nowTime.getTime();
Claims claims = Jwts.claims();
claims.put("appEUI",appEUI);
claims.put("appSecret",appSecret);
claims.setIssuedAt(iatTime);
String token = Jwts.builder().setClaims(claims).setExpiration(expireTime)
.signWith(SignatureAlgorithm.HS256,signingKey).compact();
return token;
}
/**
* 解析token
* @param token
*/
public void parseToken(String token){
Jws<Claims> jws = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token);
Claims claims = jws.getBody();
Map<String,String> header = jws.getHeader();
System.out.println("parse");
}
}
请求进来后我首先要看看是管理端的还是第三方客户的,如果是第三方客户的,还有看有没有token,如果有token,还要看对不对,如果对,还要看在不在有效期……好烦。好吧,首先得从拦截器入手分析,这又涉及到一个知识点:拦截器,它的作用呢,就是当有个请求来的时候,来判断这个请求是不是合法,比如你想验证session是不是过期,就可以在拦截器中做,如果过期就跳转到登陆页面。在这个项目里呢,我设置了两个拦截器,分别是:
ApiInterceptor:用来拦截所有的第三方用户请求。
UserActionInterceptor:用来拦截所有的管理平台用户请求。
拦截器建立好了后,如果要启用哪个拦截器,就需要在继承了WebMvcConfigurer接口的类中来启用它,就像你买了两个摄像头,需要通电来启用一样。
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
/**
* 保障在spring加载的时候注入拦截器,可以在拦截器中使用业务service。
* @return
*/
@Bean
UserActionInterceptor userActionInterceptor(){
return new UserActionInterceptor();
}
@Bean
ApiInterceptor apiInterceptor(){return new ApiInterceptor();}
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
// 可添加多个
interceptorRegistry.addInterceptor(userActionInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/api/**");
interceptorRegistry.addInterceptor(apiInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/api/getToken");
}
}
可以看到,在userActionInterceptor拦截器中,拦截所有路径,排除以api开头的路径;在apiInterceptor中拦截所有api开头的,但是需要排除生成token的路径。这样通过拦截器把内容用户和外部api接口请求分割开来。
ApiInterceptor核心代码:
public class ApiInterceptor implements HandlerInterceptor {
//可以在这里设置各种规则,取到token后解析,来验证token有效性,有效期等等。这里仅仅验证了是不是token为空。
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
String token = httpServletRequest.getHeader("v-token");//这个就是从http头中取约定好的token的key。
try{
if(token==null||token.trim().equals("")){
throw new SignatureException("token is null");
}
}catch (SignatureException e){
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg","请求参数中找不到Token");
jsonObject.put("code", Code.NO_TOKEN);
createSuccessResponse(jsonObject,httpServletResponse);
return false;
}
return true;
}
ApiController:用来生成token以及得到token之后通过token来请求其他接口。
@Controller
@RequestMapping("/api")
public class ApiController {
@Autowired
TokenService tokenService;
@RequestMapping(value = "/getToken",method = RequestMethod.POST)
@ResponseBody
public ApiResult getToken(String appEUI,String appSecret){
String token = tokenService.createToken(appEUI,appSecret);
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
jsonObject.put("expireTime", Calendar.getInstance().getTime());
ApiResult result = new ApiResult();
result.setCode(Code.SUCCESS);
result.setMsg("操作成功");
result.setData(jsonObject.toJSONString());
return result;
}
@RequestMapping(value = "/addNode",method = RequestMethod.POST)
@ResponseBody
public ApiResult addNode(){
ApiResult result = new ApiResult();
//TODO 各种API接口就在这个类里搞了。
return result;
}
}
Api请求的结果就是通过这个bean ApiResult来返回给接口请求者:
public class ApiResult implements Serializable {
/**
* 状态码
*/
private int code;
/**
* 结果 success,error
*/
private String msg;
/**
* 数据
*/
private String data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
四、排除shiro控制
因为我不需要用shiro来控制第三方用户的授权,所以我在shiro配置中进行排除
filterChainDefinitionMap.put("/api/**","anon");
原文:JWT结合Springboot+shiro,session、token同时存在来应对不同的业务场景(物联网设备管理及开放api) - 惊尘大人的个人空间 - OSCHINA - 中文开源技术交流社区