需求是这样滴:对物联网终端设备以及网关设备进行统一的管理,这里须要一个设备管理平台,同时呢,计划开放API,以供应用开发者调用API来管理控制设备。设备管理平台自己的用的是传统的session来管理,设备管理者数量并很少,因此不会有超量的session给服务器形成太大的压力。开放API给第三方应用用户是为了应对第三方用户开发的各类移动端app以及须要自身维护的设备管理。因此用session就不是那么合适,计划采起token的方式。html
多年之前我用过token这种方式来开发,那时候彷佛尚未jwt这个框架,我记得是根据用户名密码生成token后存在数据库中的,每次token进来是须要从数据库中或者提早缓存的token池中来找到匹配的token以确保不是非法请求。java
闲话多了,看看正题。web
首先呢,咱们能够经过这里来看看JWT是个什么样的东西:https://jwt.io/introduction/ 官方说的很清楚了,我就用我蹩脚的英文来给你们解释下:spring
一、什么是JSON Web Token?数据库
JSON Web Token (JWT)是一个开放的标准(RFC 7519),它定义了一种简洁独立的方式,以JSON对象的形式在各方之间安全地传输信息。json
二、何时使用JWT呢?api
受权和信息交换的时候缓存
三、JWT结构介绍安全
JWT说白了,就是一串字符串,包含三个部分,三部分之间用“.”来分割。三部分分别是:springboot
最后造成的字符串就像这样: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
四、JJWT是啥?
呃,就是Java JSON Web Token。JWT的一个java实现,若是是作Java开发的直接用JJWT得了。
做为一个正常的开发者,和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配置中进行排除
filterChainDefinitionMap.put("/api/**","anon");