需求是這樣滴:對物聯網終端設備以及網關設備進行統一的管理,這裏須要一個設備管理平臺,同時呢,計劃開放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");