JWT結合Springboot+shiro,session、token同時存在來應對不一樣的業務場景(物聯網設備管理及開放api)

1、背景說明

    需求是這樣滴:對物聯網終端設備以及網關設備進行統一的管理,這裏須要一個設備管理平臺,同時呢,計劃開放API,以供應用開發者調用API來管理控制設備。設備管理平臺自己的用的是傳統的session來管理,設備管理者數量並很少,因此不會有超量的session給服務器形成太大的壓力。開放API給第三方應用用戶是爲了應對第三方用戶開發的各類移動端app以及須要自身維護的設備管理。因此用session就不是那麼合適,計劃採起token的方式。html

    多年之前我用過token這種方式來開發,那時候彷佛尚未jwt這個框架,我記得是根據用戶名密碼生成token後存在數據庫中的,每次token進來是須要從數據庫中或者提早緩存的token池中來找到匹配的token以確保不是非法請求。java

    閒話多了,看看正題。web

2、JWT以及JJWT介紹

首先呢,咱們能夠經過這裏來看看JWT是個什麼樣的東西:https://jwt.io/introduction/  官方說的很清楚了,我就用我蹩腳的英文來給你們解釋下:spring

一、什麼是JSON Web Token?數據庫

    JSON Web Token (JWT)是一個開放的標準(RFC 7519),它定義了一種簡潔獨立的方式,以JSON對象的形式在各方之間安全地傳輸信息。json

二、何時使用JWT呢?api

    受權和信息交換的時候緩存

三、JWT結構介紹安全

    JWT說白了,就是一串字符串,包含三個部分,三部分之間用「.」來分割。三部分分別是:springboot

  • Header
  • Payload
  • Signature

    最後造成的字符串就像這樣:xxxxx.yyyyy.zzzzz

    Header大概就是這樣的:

{
  "alg": "HS256",
  "typ": "JWT"
}

    payload就是放內容的,官方叫作claims,這個是啥玩意呢?這玩意是聲明一些實體,包括jwt本身已經定義好的特點的聲明,還有一些用戶加上的聲明(咱們這些開發者想加上的)以及一些附加數據

    這玩意有三種類型,分別是 registeredpublic, 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得了。

3、和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;
    }
}

4、排除shiro控制

由於我不須要用shiro來控制第三方用戶的受權,因此我在shiro配置中進行排除

filterChainDefinitionMap.put("/api/**","anon");
相關文章
相關標籤/搜索