分布式多端登陸token驗(yàn)證的實(shí)現(xiàn)

首先說(shuō)下需求背景:這是一個(gè)分布式微服務(wù)項(xiàng)目。然后現(xiàn)在要實(shí)現(xiàn)的功能是可同時(shí)app,小程序,公眾號(hào)和網(wǎng)頁(yè)端在線。

因?yàn)橛腥苏f(shuō)我這個(gè)像是轉(zhuǎn)載的!?。∥姨匾飧缴鲜之嬎悸穲D?。。?!純手打?。。。。?/p>

token生成和用法思路圖

這里有幾個(gè)細(xì)微化需求:

? ? 1,網(wǎng)頁(yè)端是每次登陸的,而且不涉及跨域問(wèn)題。所有token/session/cookie都可以。但為了統(tǒng)一所以這里也用token。

? ? 2,小程序是每次都用微信授權(quán),所以也不需要做什么特別處理。只要在授權(quán)登陸的時(shí)候生成普通token。

? ? 3,app第一次登陸是賬號(hào)密碼登陸。然后系統(tǒng)會(huì)發(fā)一個(gè)基于設(shè)備碼的token憑證。這個(gè)憑證有效期七天。也就是七天內(nèi)都可以憑借這個(gè)憑證+設(shè)備碼 直接登陸。(同樣每次登陸后這個(gè)時(shí)間重置為7天)

? ? 4,app登陸時(shí)會(huì)返回一個(gè)普通token,然后每個(gè)接口都要驗(yàn)證此token。此token每次訪問(wèn)時(shí)長(zhǎng)都會(huì)變成30min。(也就是說(shuō)30min無(wú)操作會(huì)失效)

token組成:

? ? 1,普通token:key是:pc/app+“-”+用戶id,? ? ?value: 隨機(jī)字符串。返回給前端的是隨機(jī)字符串。? 每次傳給我的:pc/app+“-”+用戶id+字符串

? ? 2,設(shè)備碼token:key:用戶id,value:隨機(jī)字符串+設(shè)備碼。? ? ? ? ?返回給前端的是這個(gè)隨機(jī)字符串。? ?傳給我的:用戶id+字符串(+設(shè)備碼)


然后因?yàn)樾枨蟊容^雜。所以可能看起來(lái)有點(diǎn)不明確。咱們現(xiàn)在一樣一樣做。


1,接口的token驗(yàn)證:

????因?yàn)槲沂莄loud項(xiàng)目,用zuul做的api網(wǎng)關(guān)。所以我這邊是直接在zuul中進(jìn)行登陸攔截和token驗(yàn)證(如果zuul都不會(huì)用的先去了解下zuul吧)。

? ? 這里大致說(shuō)一下,zuul本身提供一個(gè)攔截器。我們只要自己創(chuàng)建一個(gè)攔截器然后繼承并重寫抽象方法即可。代碼如下:

package org.ourtowns.zuul.fifter;

import com.netflix.zuul.ZuulFilter;

public class myFifter extends ZuulFilter{

@Override

public boolean shouldFilter() {

// TODO Auto-generated method stub

return false;

}

@Override

public Object run() {

// TODO Auto-generated method stub

return null;

}

@Override

public String filterType() {

// TODO Auto-generated method stub

return null;

}

@Override

public int filterOrder() {

// TODO Auto-generated method stub

return 0;

}

}

這里大概說(shuō)一下,一共四個(gè)方法。

? ? ? ? -filterType是具體的攔截類型。一般像是權(quán)限驗(yàn)證的應(yīng)該都是在路由之前。所以這里我們”pre“即可。

? ? ? ? -filterOrder代表過(guò)濾器順序。具體我還真沒(méi)太看,反正根據(jù)默認(rèn)是-1。

? ? ? ? -shouldFilter代表這個(gè)過(guò)濾器是否生效。一般不需要驗(yàn)證的比如登陸,注冊(cè)等設(shè)置為不生效。剩下的都生效。true是生效。

? ? ? ? -Run方法:這個(gè)是主要的處理邏輯的地方,我們做權(quán)限控制、日志等都是在這里。

然后下面附上我基于普通token寫的代碼:(我把不需要攔截的都列出來(lái)了。為了排除)

package org.ourtowns.zuul.fifter;

import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.http.HttpStatus;

import org.springframework.stereotype.Component;

import org.springframework.util.StringUtils;

import com.netflix.zuul.ZuulFilter;

import com.netflix.zuul.context.RequestContext;

import net.sf.json.JSONObject;

/**

* 權(quán)限驗(yàn)證 Filter 注冊(cè)和登錄接口不過(guò)濾

*

* 如果沒(méi)有找到token,就會(huì)返回 401 無(wú)權(quán)限,并給與文字提示

*

* @author 變異者

*

*/

@Component

public class AuthFilter extends ZuulFilter {

@Autowired

StringRedisTemplate stringRedisTemplate;

// 排除過(guò)濾的 uri

// 個(gè)人用戶手機(jī)登陸

private static final String LOGIN_TEL = "/person/telLogin";

// 個(gè)人用戶微信登陸

private static final String LOGIN_WECHAT = "/person/wechatLogin";

// 個(gè)人用戶手機(jī)注冊(cè)

private static final String TEL_ADD = "/person/userReg";


// 無(wú)權(quán)限時(shí)的提示語(yǔ)

private static final String INVALID_TOKEN = "invalid token";

@Override

public Object run(){

RequestContext requestContext = RequestContext.getCurrentContext();

HttpServletRequest request = requestContext.getRequest();

// 從 header 中讀取token

String headerToken = request.getHeader("token");

if (StringUtils.isEmpty(headerToken)) {

//如果消息頭中沒(méi)有token則直接返回?zé)o權(quán)限

setUnauthorizedResponse(requestContext, INVALID_TOKEN);

} else {

//如果有token則驗(yàn)證token

verifyToken(requestContext, request, headerToken);

}

requestContext.setSendZuulResponse(true);?

requestContext.setResponseStatusCode(200);

return null;

}

/**

* 設(shè)置 401 無(wú)權(quán)限狀態(tài)

*/

private void setUnauthorizedResponse(RequestContext requestContext, String msg) {

requestContext.setSendZuulResponse(false);

requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());

JSONObject result = JSONObject.fromObject(Tools.result(401, msg , null, false));

requestContext.setResponseBody(result.toString());

}

/**

* 從Redis中校驗(yàn)token

*/

private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) {

// 需要從header 中取出 userId 來(lái)校驗(yàn) token 的有效性,因?yàn)槊總€(gè)用戶對(duì)應(yīng)一個(gè)token,在Redis中是以u(píng)serId 為鍵的

String userId = request.getHeader("userId");

if (StringUtils.isEmpty(userId)) {

setUnauthorizedResponse(requestContext, INVALID_TOKEN);

} else {

String redisToken = stringRedisTemplate.opsForValue().get(userId);

if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {

setUnauthorizedResponse(requestContext, INVALID_TOKEN);

}

//能走到這里說(shuō)明token驗(yàn)證通過(guò)了。這時(shí)候?qū)oken存在時(shí)長(zhǎng)重置為30分鐘

stringRedisTemplate.opsForValue().set(userId, redisToken, 30, TimeUnit.MINUTES);

}

}

@Override

public boolean shouldFilter() {

RequestContext requestContext = RequestContext.getCurrentContext();

HttpServletRequest request = requestContext.getRequest();

// 不需要攔截的在此列出并直接返回false。

if (LOGIN_TEL.equals(request.getRequestURI()) || LOGIN_WECHAT.equals(request.getRequestURI()) ||TEL_ADD.equals(request.getRequestURI())) {

return false;

}

return true;

}

@Override

public int filterOrder() {

return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;

}

@Override

public String filterType() {

return FilterConstants.PRE_TYPE;

}

}

這樣,一個(gè)簡(jiǎn)單的token驗(yàn)證是否登陸就做完了。需要注意的點(diǎn)有幾個(gè):

? ? 1,我這個(gè)把不需要攔截的接口列為常量了,你可以這樣也可以直接排除。但是我覺(jué)得像我更容易看懂和維護(hù)。

? ? 2,我這里被攔截的返回值是一個(gè)自己定義的對(duì)象,你如過(guò)直接復(fù)制粘貼應(yīng)該會(huì)報(bào)錯(cuò)。自己定義吧。反正返回值只要知道是被攔截的就行。

? ? 3,我定義是常量和代碼中使用的可能不一樣。原因是實(shí)際代碼里我定義了十幾個(gè)不需要攔截的接口。貼在這里我覺(jué)得太多了沒(méi)必要,所以只剩下兩個(gè)。

? ? 4,其實(shí)我寫的比較簡(jiǎn)單。只要被攔截了都只返回一個(gè)提示。如果你們有必要可以根據(jù)不同的情況定義不同的返回值。比如用戶id無(wú)效。token過(guò)期,token不正確等等。。。

? ? 5,我這里設(shè)置的是token在每次使用時(shí)有效時(shí)間都重置為30分鐘。你們可以根據(jù)具體的業(yè)務(wù)邏輯和需求來(lái)定義。甚至說(shuō)用不用重置啥的都要結(jié)合實(shí)際情況。


2,app端設(shè)備碼token驗(yàn)證:

這里再解釋下這個(gè)是設(shè)備碼token的用處:在使用app完成登陸/注冊(cè)后發(fā)放mtoken。此mtoken是存在本地的。也就是脫離app存在的。在每次app完全退出再進(jìn)入后,先查詢本地如果有這個(gè)mtoken,則可以用這個(gè)“mtoken當(dāng)消息頭,參數(shù)是設(shè)備碼”即可登錄到用戶首頁(yè)。(出于要返回用戶信息和普通時(shí)效token等,所以我這個(gè)沒(méi)在攔截器進(jìn)行判斷。而且在login中進(jìn)行的判斷。因?yàn)閿r截器里無(wú)法獲取用戶最新信息。如果大神們有更好的辦法歡迎指點(diǎn)~~萬(wàn)分感謝)。

這里直接上代碼,再聲明一點(diǎn)。我是在攔截器里放過(guò)這個(gè)uri,然后在接口里寫的邏輯:

@Override

public ResultBean loginMtoken(HttpServletRequest request,String mid) {

try {

String headerToken = request.getHeader("mtoken");

String userId = request.getHeader("userId");

//先從消息頭中取出token和userId,如果沒(méi)有則直接報(bào)無(wú)權(quán)限

if(StringUtils.isEmpty(userId) && StringUtils.isEmpty(headerToken)) {

return Tools.result(401, "invalid token", null, false);

}

String token = stringRedisTemplate.opsForValue().get(userId);

//如果該userId對(duì)應(yīng)的token不存在或者與傳來(lái)的不等則報(bào)token無(wú)效

if (StringUtils.isEmpty(token) || !headerToken.equals(token)) {

return Tools.result(401, "invalid token", null, false);

}

//走到這里說(shuō)明token有效。所以返回該用戶信息并且更換token

String[] newToken = changeToken(userId, mid);//這個(gè)是我封裝好的一個(gè)更換token的方法。一會(huì)兒會(huì)貼在下面

UserPerson userPerson = userPersonRepository.findById(userId);

userPerson.setToken(newToken[0]);

userPerson.setmToken(newToken[1]);

return Tools.result(200, "token登陸成功", userPerson, true);

} catch (Exception e) {

logger.info("token登陸失敗"+e.getMessage(), e);

return Tools.result(500, "token登陸失敗", null, false);

}

}

到這里根據(jù)mtoken登陸已經(jīng)實(shí)現(xiàn)了。然后其中生成token的方法是我自己封裝的。有兩個(gè)。一個(gè)是生成,一個(gè)是更換。

3,token生成(因?yàn)槲覀冞@個(gè)項(xiàng)目涉及到app會(huì)產(chǎn)生兩個(gè)token。比較復(fù)雜而且墨跡還惡心。如果是只生成一個(gè)的會(huì)簡(jiǎn)潔方便多了):

因?yàn)槲覀兝习逡髏oken要雙方加密。所以返回前端的是一個(gè)隨機(jī)串,然后我這邊把隨機(jī)串用md5加密后存入redis。前端同樣接到隨機(jī)串要用md5加密然后放到header。。大家不用做的我們這么惡心。

//這里是一個(gè)生成兩個(gè)token的方法

public String[] getToken(String channel,String mid,String id) throws Exception{

String uuid = Tools.UUID();

String muuid = Tools.UUID();

String token = Tools.MD(uuid);

//普通token(key:pc/app+“-”+用戶id,? ? value: 隨機(jī)字符串),有效時(shí)長(zhǎng)是30分鐘。

stringRedisTemplate.opsForValue().set(channel+id, token, 30, TimeUnit.MINUTES);

//如果用戶是手機(jī)app登陸才會(huì)生成基于驗(yàn)證碼的token(判斷此處是否有設(shè)備碼,如沒(méi)有不生成設(shè)備碼token)

if("app".equals(channel) && "".equals(mid)==false) {

//基于設(shè)備碼的token(key:用戶id,value:隨機(jī)字符串+設(shè)備碼。)

String mtoken = Tools.MD(muuid+mid);

//設(shè)備碼token有效時(shí)間7天

stringRedisTemplate.opsForValue().set(id, mtoken, 7, TimeUnit.DAYS);

}

return new String[]{uuid,muuid};

}

要求是每次基于設(shè)備碼token登陸后要更換token。所以這里有個(gè) token登陸和更換的方法:

//這是換取mtoken和生成普通token的方法(能走這個(gè)接口說(shuō)明肯定是app端)

public String[] changeToken(String id,String mid) throws Exception{

String uuid = Tools.UUID();

String muuid = Tools.UUID();

String token = Tools.MD(uuid);

String mtoken = Tools.MD(muuid+mid);

stringRedisTemplate.opsForValue().set("app"+id, token, 30, TimeUnit.MINUTES);

stringRedisTemplate.opsForValue().set(id, mtoken, 7, TimeUnit.DAYS);

return new String[]{uuid,muuid};

}


好的。幾乎全部配置就這樣。下午和前端也試過(guò)了~全部跑通,沒(méi)有什么問(wèn)題。然后可能有的地方設(shè)計(jì)冗余且不合理。不過(guò)水平有限,起碼功能實(shí)現(xiàn)了。親們有好的建議可以留言哈~~

然后如果有什么疑問(wèn)或者覺(jué)得我說(shuō)的哪里有問(wèn)題或者哪里不懂的歡迎留言或者私信指出。

全文手打~~這么不容易的寫個(gè)文~~如果你覺(jué)得用到了理解了~留個(gè)言點(diǎn)個(gè)贊轉(zhuǎn)個(gè)發(fā)什么的啊~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容