動手寫個java快速開發(fā)框架-(5)實現(xiàn)統(tǒng)一用戶token校驗

訪問權(quán)限校驗是一個互聯(lián)網(wǎng)系統(tǒng)必備的功能莽龟,今天我們就動手將訪問權(quán)限控制的功能加到框架中陕凹,可以針對特定路徑的接口進(jìn)行訪問權(quán)限校驗,防止出現(xiàn)越權(quán)訪問的情況發(fā)生肯适。

以前我們做權(quán)限控制都是通過用戶登陸后由服務(wù)端生成sessionid次泽,并將這個sessionid與用戶信息關(guān)聯(lián)起來穿仪,客戶端需要保存sessionid在cookie中,在后續(xù)請求交易中上送這個cookie意荤,服務(wù)端從header中獲取到cookie啊片,并從cookie中獲取到sessionid,來判斷該sessionid是否還有效玖像。在分布式紫谷、前后端分離還沒那么火的時候,權(quán)限控制基本都是基于session來實現(xiàn)的,這么做最大的缺點是笤昨,客戶端需要每次都訪問到固定的服務(wù)器節(jié)點才能查詢到sessionid祖驱,不然會導(dǎo)致查詢不到重新登陸的問題,這就需要在負(fù)載均衡F5或者nginx上配置根據(jù)sessionid做hash路由分發(fā)瞒窒,一旦某臺機(jī)器出問題需要通過這臺服務(wù)節(jié)點登陸的所有用戶全部重新登陸捺僻,影響還是很大的。隨著分布式架構(gòu)的快速發(fā)展崇裁,移動互聯(lián)的快速發(fā)展也推動了前后端分離的快速發(fā)展匕坯,最近幾年新建系統(tǒng)基本都是前后端分離分布式架構(gòu)了,這樣的架構(gòu)自然不再適合用session這種機(jī)制了拔稳,oauth2葛峻、分布式token等等授權(quán)概念紛紛推出。一般有一定規(guī)模用戶的公司會使用oauth這樣的認(rèn)證授權(quán)的架構(gòu)巴比,對于不了解oauth協(xié)議的同學(xué)建議先看看阮一峰的這篇文章术奖,寫的非常淺顯易懂,原先筆者在幾家大公司用的也是oauth匿辩,但是用過oauth的同學(xué)應(yīng)該也都知道,oauth雖然能夠在開放的前提下保證安全榛丢,但是還是有點重铲球,除非是對于要建設(shè)開放平臺的系統(tǒng)一定要用oauth,對于一般的互聯(lián)網(wǎng)項目晰赞,其實是沒必要使用oauth的稼病,oauth同樣是一種數(shù)據(jù)集中授權(quán)的模式,那么勢必還是要想辦法保證oauth token集中校驗的性能和高可用掖鱼,不管是從開發(fā)還是從硬件投入上都是一筆不小的開支然走。

所以既然要做一款輕量級的快速開發(fā)框架,那么這里我們使用了更加輕量級的token發(fā)放和驗證方案-jwt https://jwt.io戏挡,其實簡單理解就是jwt按照一定的規(guī)則生成了一個散列值作為token芍瑞,類似下圖左邊是jwt編碼后的token,右邊是原始明文組成要素主要有3部分褐墅,第一部分定義散列算法和生成類型拆檬,第二部分payload是用戶可以自定義一些用戶標(biāo)示要素項,內(nèi)部其實是使用對稱秘鑰進(jìn)行了加密這部分?jǐn)?shù)據(jù)妥凳,第三部分是校驗域用來給服務(wù)端校驗有效性用的:

Screenshot 2018-09-12 10.34.16

并且可以通過該token還原出用戶的標(biāo)示信息竟贯,同時提供了token校驗的功能,并且不需要集中驗證逝钥,任何一臺服務(wù)節(jié)點都可以根據(jù)規(guī)則進(jìn)行校驗屑那,只要保證每臺機(jī)器生成token的秘鑰保持一致就可以。jwt是非常輕量級的一種方案,對于開發(fā)一般的項目作為權(quán)限校驗足夠了持际。

下面我們再來看看客戶端和服務(wù)端的交互流程:

client->server: 登陸
server-->client: jwt生成并返回token
client->server: 攜帶token請求接口
server->server: 通過jwt校驗token有效性
server-->client: 返回接口內(nèi)容

整個流程是不是很清晰沃琅,同時jwt提供了一系列的接口包含了,生成token选酗、注銷token阵难、有效期設(shè)置等等常用的功能。下面我們利用jwt來實現(xiàn)用戶token的生成和校驗功能芒填,這里我們通過全局?jǐn)r截器來實現(xiàn)token的校驗呜叫,這樣可以做到對業(yè)務(wù)功能的無感。

首先我們要定義用戶的注冊和登錄的相關(guān)操作殿衰,文章中就不詳細(xì)講解了朱庆,代碼很簡單,具體大家可以參考源碼的modules.module1模塊中的相關(guān)業(yè)務(wù)代碼闷祥。

接下來就是我們具體集成jwt了娱颊。

pom依賴

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

jwt二次封裝

這里我們對jwt做了簡單的封裝,主要封裝了token生成方法凯砍,我們這里的payload只有userid箱硕,大家可以可以根據(jù)自己項目需要定制payload,這個payload在后續(xù)會使用到悟衩,用來從線程中獲取userid來判斷用戶權(quán)限剧罩。還封裝了獲取token的Claims對象的方法,Claims大家可以理解為jwt明文的報文體結(jié)構(gòu)座泳,在驗證的時候可以通過獲取到當(dāng)前請求的用戶payload信息惠昔。

@ConfigurationProperties(prefix = "mk.jwt")
@Component
public class JwtUtils {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private String secret;
    private long expire;
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //過期時間
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            logger.debug("validate is token error ", e);
            return null;
        }
    }

    /**
     * token是否過期
     * @return  true:過期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

    //...省略setter、getter
}

定義攔截器

最后就是定義攔截器了挑势,讓自定義攔截器繼承自HandlerInterceptorAdapter這個攔截器適配器類镇防,當(dāng)然你也可以直接實現(xiàn)HandlerInterceptor接口,使用適配器類的話只需要實現(xiàn)自己關(guān)心的preHandle還是postHandle就可以了潮饱,這里我們只需要實現(xiàn)prehandle来氧,在controller和servlet處理之前攔截就可以了。

代碼注釋比較完善香拉,大家看注釋就可以了饲漾。

@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private JwtUtils jwtUtils;

    public static final String USER_KEY = "userId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Login annotation;
        if(handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
        }else{
            return true;
        }

        if(annotation == null){
            return true;
        }

        //獲取用戶憑證
        String token = request.getHeader(jwtUtils.getHeader());
        if(StringUtils.isBlank(token)){
            token = request.getParameter(jwtUtils.getHeader());
        }

        //憑證為空
        if(StringUtils.isBlank(token)){
            throw new MkException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.toString());
        }

        Claims claims = jwtUtils.getClaimByToken(token);
        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
            throw new MkException(jwtUtils.getHeader() + "失效,請重新登錄", HttpStatus.UNAUTHORIZED.toString());
        }

        //設(shè)置userId到request里缕溉,后續(xù)根據(jù)userId考传,獲取用戶信息
        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));

        return true;
    }
}

定義好了攔截器,我們還需要在啟動的時候注冊上攔截器证鸥,我們還是在之前定義WebMvcConfig中進(jìn)行注冊攔截器僚楞,如果對指定的path進(jìn)行攔截勤晚,那么也可以在這里配置。

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private DesParamsHandlerMethodArgumentResolver desParamsHandlerMethodArgumentResolver;
    @Autowired
   private AuthorizationInterceptor authorizationInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**");
   }
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(desParamsHandlerMethodArgumentResolver);
    }
}

驗證

驗證就比較簡單了泉褐,注冊赐写、登錄的controller方法都在TestUserController中,大家可以先訪問register注冊一個用戶膜赃,再調(diào)用login獲取到該用戶的token挺邀,再將這個token放到queryUser接口的header中去訪問就可以獲取到該用戶的信息了,如果在header中沒有token或者token錯誤都會報錯跳座,并且也獲取不到別人的信息端铛,只能獲取到這個token的用戶信息。

總結(jié)

本框架中用了jwt來實現(xiàn)token校驗疲眷,當(dāng)然大家也可以通過使用oauth來實現(xiàn)token的相關(guān)功能禾蚕,如果實現(xiàn)oauth的相關(guān)功能,大家可以使喲過spring security或者shiro狂丝,token數(shù)據(jù)可以緩存在redis集群里來保證性能和高可用换淆。

本文對應(yīng)的github tag為v0.5,可以通過連接下載https://github.com/feiweiwei/MkFramework4java/releases/tag/v0.5几颜,也可以通過git clone -b v0.5 https://github.com/feiweiwei/MkFramework4java.git

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末倍试,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蛋哭,更是在濱河造成了極大的恐慌县习,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件具壮,死亡現(xiàn)場離奇詭異准颓,居然都是意外死亡哈蝇,警方通過查閱死者的電腦和手機(jī)棺妓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炮赦,“玉大人怜跑,你說我怎么就攤上這事》涂保” “怎么了性芬?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長剧防。 經(jīng)常有香客問我植锉,道長,這世上最難降的妖魔是什么峭拘? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任俊庇,我火速辦了婚禮狮暑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辉饱。我一直安慰自己搬男,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布彭沼。 她就那樣靜靜地躺著缔逛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姓惑。 梳的紋絲不亂的頭發(fā)上褐奴,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機(jī)與錄音挺益,去河邊找鬼歉糜。 笑死,一個胖子當(dāng)著我的面吹牛望众,可吹牛的內(nèi)容都是我干的匪补。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼烂翰,長吁一口氣:“原來是場噩夢啊……” “哼夯缺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甘耿,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤踊兜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后佳恬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捏境,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年毁葱,在試婚紗的時候發(fā)現(xiàn)自己被綠了垫言。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡倾剿,死狀恐怖筷频,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情前痘,我是刑警寧澤凛捏,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站芹缔,受9級特大地震影響坯癣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜最欠,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一示罗、第九天 我趴在偏房一處隱蔽的房頂上張望蓬网。 院中可真熱鬧,春花似錦鹉勒、人聲如沸帆锋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锯厢。三九已至,卻和暖如春脯倒,著一層夾襖步出監(jiān)牢的瞬間实辑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工藻丢, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留剪撬,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓悠反,卻偏偏與公主長得像残黑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子斋否,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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