JWT

Json web token(JWT)是為了網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開發(fā)標準(RFC 7519)砸紊,該token被設(shè)計為緊湊且安全的,特別適用于分布式站點的單點登陸(SSO)場景凤跑。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認證的用戶身份信息缀遍,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息煮剧,該token也可直接被用于認證闻镶,也可被加密甚脉。

起源

說起JWT,我們應(yīng)該來談一談基于token的認證和傳統(tǒng)的Session認證的區(qū)別儒溉。

傳統(tǒng)的session認證

我們知道宦焦,http協(xié)議本身是一種無狀態(tài)的協(xié)議,而這就意味著如果用戶向我們的應(yīng)用提供了用戶名和密碼來進行用戶認證,那么下一次請求時波闹,用戶還要再一次進行用戶認證才行酝豪,因為根據(jù)http協(xié)議,我們并不能知道是哪個用戶發(fā)送的請求精堕,所以為了讓我們的應(yīng)用能識別是哪個用戶發(fā)出的孵淘,我們只能在服務(wù)器存儲一份用戶登陸的信息,這份登陸信息會在響應(yīng)時傳遞給服務(wù)器歹篓,告訴其保存為cookie瘫证,以便下次請求時發(fā)送給我們的應(yīng)用,這樣我們的英喲個就能識別請求來自哪個用戶了庄撮,這就是傳統(tǒng)的基于sessino認證

但是這種基于session的認證使應(yīng)用本身很難得擴展背捌,隨著不用客戶端的增加,獨立的服務(wù)器已無法承載更多的用戶洞斯,而這個時候基于session認證應(yīng)用的問題就會暴露出來

基于session認證所顯露的問題

Session:每個用戶經(jīng)過我們的應(yīng)用認證之后毡庆,我們的應(yīng)用都要在服務(wù)端做一次記錄,以便用戶下次請求的鑒別烙如,通常而言session都是保存在內(nèi)存中么抗,而隨著認證用戶的增多,服務(wù)端的開銷會明顯增大

擴展性:用戶認證之后亚铁,服務(wù)端做認證記錄蝇刀,如果認證的記錄被保存在內(nèi)存的話,這意味著用戶下次請求還必須要請求在這臺服務(wù)器上徘溢,這樣才能拿到授權(quán)的資源吞琐,這樣在分布式的應(yīng)用上,響應(yīng)的限制了負載均衡器的能力甸昏,也意味著限制了應(yīng)用的擴展性

CSRF:因為是基于cookie來進行用戶識別的顽分,cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊施蜜。

基于token的鑒權(quán)機制

基于token的鑒權(quán)機制類似于http協(xié)議也是無狀態(tài)的,它不需要在服務(wù)端去保留用戶的認證信息或會話信息雌隅。這也就意味著機遇tokent認證機制的應(yīng)用不需要去考慮用戶在哪一臺服務(wù)器登陸了翻默,這就為應(yīng)用的擴展提供了便利

流程是這樣的

  • 用戶使用用戶名密碼請求服務(wù)器

  • 服務(wù)器進行驗證用戶信息

  • 服務(wù)器通過驗證發(fā)送給用戶一個token

  • 客戶端存儲token,并在每次請求時附加這個token值

  • 服務(wù)器驗證token恰起,并返回數(shù)據(jù)

    這個token必須要在每次請求時發(fā)送給服務(wù)器修械,它應(yīng)該保存在請求頭中,另外检盼,服務(wù)器要支持CORS(跨來源資源共享)策略肯污,一般我們在服務(wù)端這么做就可以了 Access-Control-Allow-Origin:*

JWT的構(gòu)成

JWT是由三部分構(gòu)成,將這三段信息文本用鏈接構(gòu)成了JWT字符串。就像這樣

<pre>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U</pre>

第一部分我們稱它為頭部(header)第二部分我們稱其為載荷(payload蹦渣,類似于飛機上承載的物品)哄芜,第三部分是簽證(signature)

header

JWT的頭部承載的兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法柬唯,通常直接使用HMAC SHA256

完整的頭部就像下面這樣的JSON

<pre>{ 'typ':'JWT', 'alg':'HS256' }</pre>

然后將頭部進行base64加密(該加密是可以對稱解密的)认臊,構(gòu)成了第一部分

<pre>eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9</pre>

plyload

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品锄奢,這些有效信息包含三個部分

  • 標準中注冊的聲明

  • 公共的聲明

  • 私有的聲明

    標注中注冊的聲明(建議不強制使用)

  • iss:jwt簽發(fā)者

  • sub:jwt所面向的用戶

  • aud:接收jwt的一方

  • exp:jwt的過期時間失晴,這個過期時間必須大于簽發(fā)時間

  • nbf:定義在什么時間之前,該jwt都是不可用的

  • iat:jwt的簽發(fā)時間

  • jti:jwt的唯一身份標識拘央,主要用來作為一次性token涂屁,從而回避重放攻擊

    公共的聲明:

    公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其它業(yè)務(wù)需要的必要信息灰伟,但不建議添加敏感信息胯陋,因為該部分在客戶端可解密;

    私有的聲明

    私有的聲明是提供者和消費者功能定義的聲明袱箱,一般不建議存放敏感信息遏乔,因為base64是對稱解密的,意味著該部分信息可以歸類為名文信息发笔。

    定義一個payload

<pre>{ "sub": "1234567890", "name": "John Doe", "admin": true }</pre>

然后將其base64加密盟萨,得到j(luò)wt的一部分

<pre>eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9</pre>

Signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header(base64后的)

  • payload(base64后的)

  • secred

    這個部分需要base64加密后的header和base64加密后的payload使用“.”連接組成的字符串了讨,然后通過header中聲明的加密方式進行加secret組合加密捻激,然后就構(gòu)成了jwt的第三部分

<pre>var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ</pre>

將這三部分用“.”連接成一個完整的字符串,構(gòu)成了最終的jwt:

<pre>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ</pre>

注意:secret是保存在服務(wù)器端的前计,jwt的簽發(fā)也是在服務(wù)端的胞谭,secret就是用來進行jwt的簽發(fā)和jwt的驗證,所以它就是你服務(wù)端的私鑰男杈,在任何場景都不應(yīng)該流露出去丈屹,一旦客戶端得知這個secret,那就意味著客戶端可以自我簽發(fā)jwt了

應(yīng)用

一般是在請求頭里加入Authorization伶棒,并加上Bearer標注:

<pre>fetch('api/user/1', {
headers: { 'Authorization': 'Bearer ' + token
}
})</pre>

服務(wù)端會驗證token旺垒,如果驗證通過就會返回相應(yīng)的資源,整個流程就是這樣

image

總結(jié)

優(yōu)點:

  • 因為json的通用性肤无,所以JWT是可以跨語言支持的先蒋,像C#,JavaScript宛渐,NodeJS竞漾,PHP等許多語言都可以使用

  • 因為由了payload部分都哭,所以JWT可以在自身存儲一些其它業(yè)務(wù)邏輯所必要的非敏感信息

  • 便于傳輸匕累,jwt的構(gòu)成非常簡單,字節(jié)占用很小,所以它是非常便于傳輸?shù)?/p>

  • 它不需要在服務(wù)端保存會話信息簿废,所以它易于應(yīng)用的擴展

    安全相關(guān)

  • 不應(yīng)該在jwt的payload部分存儲敏感信息暑竟,因為該部分是客戶端可解密的部分

  • 保護好secret私鑰床蜘。該私鑰非常重要

  • 如果可以晰洒,請使用https協(xié)議

代碼實現(xiàn)

下面來進行SpringBoot和JWT的集成

引入JWT依賴,由于是基于Java,所以需要的是java-jwt

<dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.4.0</version>
</dependency>

需要自定義兩個注解

用來跳過驗證的PassToken

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

需要登錄才能進行操作的注解UserLoginToken

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

@Target:注解的作用目標

@Target(ElementType.TYPE)——接口糊闽、類梳玫、枚舉、注解
@Target(ElementType.FIELD)——字段右犹、枚舉的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法參數(shù)
@Target(ElementType.CONSTRUCTOR) ——構(gòu)造函數(shù)
@Target(ElementType.LOCAL_VARIABLE)——局部變量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包

@Retention:注解的保留位置

RetentionPolicy.SOURCE:這種類型的Annotations只在源代碼級別保留,編譯時就會被忽略,在class字節(jié)碼文件中不包含提澎。
RetentionPolicy.CLASS:這種類型的Annotations編譯時被保留,默認的保留策略,在class文件中存在,但JVM將會忽略,運行時無法獲得。
RetentionPolicy.RUNTIME:這種類型的Annotations將被JVM保留,所以他們能在運行時被JVM或其他使用反射機制的代碼所讀取和使用念链。

@Document:說明該注解將被包含在javadoc
@Inherited:說明子類可以繼承父類中的該注解

簡單自定義一個實體類User,使用lombok簡化實體類的編寫

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String Id;
    String username;
    String password;
}

需要寫token的生成方法

public String getToken(User user) {
        String token="";
        token= JWT.create().withAudience(user.getId())
                .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }

Algorithm.HMAC256():使用HS256生成token,密鑰則是用戶的密碼盼忌,唯一密鑰的話可以保存在服務(wù)端。
withAudience()存入需要保存在token的信息掂墓,這里我把用戶ID存入token

接下來需要寫一個攔截器去獲取token并驗證token

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 從 http 請求頭中取出 token
        // 如果不是映射到方法直接通過
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //檢查是否有passtoken注釋谦纱,有則跳過認證
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //檢查有沒有需要用戶權(quán)限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 執(zhí)行認證
                if (token == null) {
                    throw new RuntimeException("無token,請重新登錄");
                }
                // 獲取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("401");
                }
                User user = userService.findUserById(userId);
                if (user == null) {
                    throw new RuntimeException("用戶不存在君编,請重新登錄");
                }
                // 驗證 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @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 {
    }

實現(xiàn)一個攔截器就需要實現(xiàn)HandlerInterceptor接口

HandlerInterceptor接口主要定義了三個方法
1.boolean preHandle ()
預(yù)處理回調(diào)方法,實現(xiàn)處理器的預(yù)處理跨嘉,第三個參數(shù)為響應(yīng)的處理器,自定義Controller,返回值為true表示繼續(xù)流程(如調(diào)用下一個攔截器或處理器)或者接著執(zhí)行postHandle()afterCompletion()false表示流程中斷吃嘿,不會繼續(xù)調(diào)用其他的攔截器或處理器祠乃,中斷執(zhí)行。

2.void postHandle()
后處理回調(diào)方法兑燥,實現(xiàn)處理器的后處理(DispatcherServlet進行視圖返回渲染之前進行調(diào)用)亮瓷,此時我們可以通過modelAndView(模型和視圖對象)對模型數(shù)據(jù)進行處理或?qū)σ晥D進行處理,modelAndView也可能為null降瞳。

3.void afterCompletion():
整個請求處理完畢回調(diào)方法,該方法也是需要當前對應(yīng)的InterceptorpreHandle()的返回值為true時才會執(zhí)行嘱支,也就是在DispatcherServlet渲染了對應(yīng)的視圖之后執(zhí)行。用于進行資源清理力崇。整個請求處理完畢回調(diào)方法斗塘。如性能監(jiān)控中我們可以在此記錄結(jié)束時間并輸出消耗時間,還可以進行一些資源清理亮靴,類似于try-catch-finally中的finally,但僅調(diào)用處理器執(zhí)行鏈中

主要流程:

1.從 http 請求頭中取出 token于置,
2.判斷是否映射到方法
3.檢查是否有passtoken注釋茧吊,有則跳過認證
4.檢查有沒有需要用戶登錄的注解贞岭,有則需要取出并驗證
5.認證通過則可以訪問,不通過會報相關(guān)錯誤信息

配置攔截器

在配置類上添加了注解@Configuration搓侄,標明了該類是一個配置類并且會將該類作為一個SpringBean添加到IOC容器內(nèi)

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    // 攔截所有請求瞄桨,通過判斷是否有 @LoginRequired 注解 決定是否需要登錄
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

WebMvcConfigurerAdapter該抽象類其實里面沒有任何的方法實現(xiàn),只是空實現(xiàn)了接口
WebMvcConfigurer內(nèi)的全部方法讶踪,并沒有給出任何的業(yè)務(wù)邏輯處理芯侥,這一點設(shè)計恰到好處的讓我們不必去實現(xiàn)那些我們不用的方法,都交由WebMvcConfigurerAdapter抽象類空實現(xiàn),如果我們需要針對具體的某一個方法做出邏輯處理,僅僅需要在WebMvcConfigurerAdapter子類中@Override對應(yīng)方法就可以了乳讥。

注:
SpringBoot2.0Spring 5.0WebMvcConfigurerAdapter已被廢棄
網(wǎng)上有說改為繼承WebMvcConfigurationSupport()柱查,不過試了下,還是過期的

解決方法:

直接實現(xiàn)WebMvcConfigurer (官方推薦)

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");   
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

InterceptorRegistry內(nèi)的addInterceptor需要一個實現(xiàn)HandlerInterceptor接口的攔截器實例云石,addPathPatterns方法用于設(shè)置攔截器的過濾路徑規(guī)則唉工。
這里我攔截所有請求,通過判斷是否有@LoginRequired注解 決定是否需要登錄

在數(shù)據(jù)訪問接口中加入登錄操作注解

@RestController
@RequestMapping("api")
public class UserApi {
    @Autowired
    UserService userService;
    @Autowired
    TokenService tokenService;
    //登錄
    @PostMapping("/login")
    public Object login(@RequestBody User user){
        JSONObject jsonObject=new JSONObject();
        User userForBase=userService.findByUsername(user);
        if(userForBase==null){
            jsonObject.put("message","登錄失敗,用戶不存在");
            return jsonObject;
        }else {
            if (!userForBase.getPassword().equals(user.getPassword())){
                jsonObject.put("message","登錄失敗,密碼錯誤");
                return jsonObject;
            }else {
                String token = tokenService.getToken(userForBase);
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }
    @UserLoginToken
    @GetMapping("/getMessage")
    public String getMessage(){
        return "你已通過驗證";
    }
}

不加注解的話默認不驗證汹忠,登錄接口一般是不驗證的淋硝。在getMessage()中我加上了登錄注解,說明該接口必須登錄獲取token后宽菜,在請求頭中加上token并通過驗證才可以訪問

下面進行測試谣膳,啟動項目,使用postman測試接口

在沒token的情況下訪問api/getMessage接口

image

我這里使用了統(tǒng)一異常處理铅乡,所以只看到錯誤message

下面進行登錄继谚,從而獲取token

image

登錄操作我沒加驗證注解,所以可以直接訪問

token加在請求頭中隆判,再次訪問api/getMessage接口

image

注意:這里的key一定不能錯犬庇,因為在攔截器中是取關(guān)鍵字token的值
String token = httpServletRequest.getHeader("token");
加上token之后就可以順利通過驗證和進行接口訪問了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市侨嘀,隨后出現(xiàn)的幾起案子臭挽,更是在濱河造成了極大的恐慌,老刑警劉巖咬腕,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件欢峰,死亡現(xiàn)場離奇詭異,居然都是意外死亡涨共,警方通過查閱死者的電腦和手機纽帖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來举反,“玉大人懊直,你說我怎么就攤上這事』鸨牵” “怎么了室囊?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵雕崩,是天一觀的道長。 經(jīng)常有香客問我融撞,道長盼铁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任尝偎,我火速辦了婚禮饶火,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘致扯。我一直安慰自己肤寝,他們只是感情好,可當我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布急前。 她就那樣靜靜地躺著醒陆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裆针。 梳的紋絲不亂的頭發(fā)上刨摩,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天,我揣著相機與錄音世吨,去河邊找鬼澡刹。 笑死,一個胖子當著我的面吹牛耘婚,可吹牛的內(nèi)容都是我干的罢浇。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼沐祷,長吁一口氣:“原來是場噩夢啊……” “哼嚷闭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赖临,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤胞锰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后兢榨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗅榕,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年吵聪,在試婚紗的時候發(fā)現(xiàn)自己被綠了凌那。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡吟逝,死狀恐怖帽蝶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情块攒,我是刑警寧澤嘲碱,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布金砍,位于F島的核電站局蚀,受9級特大地震影響麦锯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜琅绅,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一扶欣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧千扶,春花似錦料祠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妆绞,卻和暖如春顺呕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背括饶。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工株茶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人图焰。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓启盛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親技羔。 傳聞我的和親對象是個殘疾皇子僵闯,可洞房花燭夜當晚...
    茶點故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容