首先說下需求背景:這是一個分布式微服務(wù)項(xiàng)目。然后現(xiàn)在要實(shí)現(xiàn)的功能是可同時app,小程序,公眾號和網(wǎng)頁端在線。
因?yàn)橛腥苏f我這個像是轉(zhuǎn)載的!??!我特意附上手畫思路圖?。。。〖兪执颍。。。?!

這里有幾個細(xì)微化需求:
? ? 1,網(wǎng)頁端是每次登陸的,而且不涉及跨域問題。所有token/session/cookie都可以。但為了統(tǒng)一所以這里也用token。
? ? 2,小程序是每次都用微信授權(quán),所以也不需要做什么特別處理。只要在授權(quán)登陸的時候生成普通token。
? ? 3,app第一次登陸是賬號密碼登陸。然后系統(tǒng)會發(fā)一個基于設(shè)備碼的token憑證。這個憑證有效期七天。也就是七天內(nèi)都可以憑借這個憑證+設(shè)備碼 直接登陸。(同樣每次登陸后這個時間重置為7天)
? ? 4,app登陸時會返回一個普通token,然后每個接口都要驗(yàn)證此token。此token每次訪問時長都會變成30min。(也就是說30min無操作會失效)
token組成:
? ? 1,普通token:key是:pc/app+“-”+用戶id,? ? ?value: 隨機(jī)字符串。返回給前端的是隨機(jī)字符串。? 每次傳給我的:pc/app+“-”+用戶id+字符串
? ? 2,設(shè)備碼token:key:用戶id,value:隨機(jī)字符串+設(shè)備碼。? ? ? ? ?返回給前端的是這個隨機(jī)字符串。? ?傳給我的:用戶id+字符串(+設(shè)備碼)
然后因?yàn)樾枨蟊容^雜。所以可能看起來有點(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都不會用的先去了解下zuul吧)。
? ? 這里大致說一下,zuul本身提供一個攔截器。我們只要自己創(chuàng)建一個攔截器然后繼承并重寫抽象方法即可。代碼如下:
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;
}
}
這里大概說一下,一共四個方法。
? ? ? ? -filterType是具體的攔截類型。一般像是權(quán)限驗(yàn)證的應(yīng)該都是在路由之前。所以這里我們”pre“即可。
? ? ? ? -filterOrder代表過濾器順序。具體我還真沒太看,反正根據(jù)默認(rèn)是-1。
? ? ? ? -shouldFilter代表這個過濾器是否生效。一般不需要驗(yàn)證的比如登陸,注冊等設(shè)置為不生效。剩下的都生效。true是生效。
? ? ? ? -Run方法:這個是主要的處理邏輯的地方,我們做權(quán)限控制、日志等都是在這里。
然后下面附上我基于普通token寫的代碼:(我把不需要攔截的都列出來了。為了排除)
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 注冊和登錄接口不過濾
*
* 如果沒有找到token,就會返回 401 無權(quán)限,并給與文字提示
*
* @author 變異者
*
*/
@Component
public class AuthFilter extends ZuulFilter {
@Autowired
StringRedisTemplate stringRedisTemplate;
// 排除過濾的 uri
// 個人用戶手機(jī)登陸
private static final String LOGIN_TEL = "/person/telLogin";
// 個人用戶微信登陸
private static final String LOGIN_WECHAT = "/person/wechatLogin";
// 個人用戶手機(jī)注冊
private static final String TEL_ADD = "/person/userReg";
// 無權(quán)限時的提示語
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)) {
//如果消息頭中沒有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 無權(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 來校驗(yàn) token 的有效性,因?yàn)槊總€用戶對應(yīng)一個token,在Redis中是以userId 為鍵的
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);
}
//能走到這里說明token驗(yàn)證通過了。這時候?qū)oken存在時長重置為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;
}
}
這樣,一個簡單的token驗(yàn)證是否登陸就做完了。需要注意的點(diǎn)有幾個:
? ? 1,我這個把不需要攔截的接口列為常量了,你可以這樣也可以直接排除。但是我覺得像我更容易看懂和維護(hù)。
? ? 2,我這里被攔截的返回值是一個自己定義的對象,你如過直接復(fù)制粘貼應(yīng)該會報錯。自己定義吧。反正返回值只要知道是被攔截的就行。
? ? 3,我定義是常量和代碼中使用的可能不一樣。原因是實(shí)際代碼里我定義了十幾個不需要攔截的接口。貼在這里我覺得太多了沒必要,所以只剩下兩個。
? ? 4,其實(shí)我寫的比較簡單。只要被攔截了都只返回一個提示。如果你們有必要可以根據(jù)不同的情況定義不同的返回值。比如用戶id無效。token過期,token不正確等等。。。
? ? 5,我這里設(shè)置的是token在每次使用時有效時間都重置為30分鐘。你們可以根據(jù)具體的業(yè)務(wù)邏輯和需求來定義。甚至說用不用重置啥的都要結(jié)合實(shí)際情況。
2,app端設(shè)備碼token驗(yàn)證:
這里再解釋下這個是設(shè)備碼token的用處:在使用app完成登陸/注冊后發(fā)放mtoken。此mtoken是存在本地的。也就是脫離app存在的。在每次app完全退出再進(jìn)入后,先查詢本地如果有這個mtoken,則可以用這個“mtoken當(dāng)消息頭,參數(shù)是設(shè)備碼”即可登錄到用戶首頁。(出于要返回用戶信息和普通時效token等,所以我這個沒在攔截器進(jìn)行判斷。而且在login中進(jìn)行的判斷。因?yàn)閿r截器里無法獲取用戶最新信息。如果大神們有更好的辦法歡迎指點(diǎn)~~萬分感謝)。
這里直接上代碼,再聲明一點(diǎn)。我是在攔截器里放過這個uri,然后在接口里寫的邏輯:
@Override
public ResultBean loginMtoken(HttpServletRequest request,String mid) {
try {
String headerToken = request.getHeader("mtoken");
String userId = request.getHeader("userId");
//先從消息頭中取出token和userId,如果沒有則直接報無權(quán)限
if(StringUtils.isEmpty(userId) && StringUtils.isEmpty(headerToken)) {
return Tools.result(401, "invalid token", null, false);
}
String token = stringRedisTemplate.opsForValue().get(userId);
//如果該userId對應(yīng)的token不存在或者與傳來的不等則報token無效
if (StringUtils.isEmpty(token) || !headerToken.equals(token)) {
return Tools.result(401, "invalid token", null, false);
}
//走到這里說明token有效。所以返回該用戶信息并且更換token
String[] newToken = changeToken(userId, mid);//這個是我封裝好的一個更換token的方法。一會兒會貼在下面
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的方法是我自己封裝的。有兩個。一個是生成,一個是更換。
3,token生成(因?yàn)槲覀冞@個項(xiàng)目涉及到app會產(chǎn)生兩個token。比較復(fù)雜而且墨跡還惡心。如果是只生成一個的會簡潔方便多了):
因?yàn)槲覀兝习逡髏oken要雙方加密。所以返回前端的是一個隨機(jī)串,然后我這邊把隨機(jī)串用md5加密后存入redis。前端同樣接到隨機(jī)串要用md5加密然后放到header。。大家不用做的我們這么惡心。
//這里是一個生成兩個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ī)字符串),有效時長是30分鐘。
stringRedisTemplate.opsForValue().set(channel+id, token, 30, TimeUnit.MINUTES);
//如果用戶是手機(jī)app登陸才會生成基于驗(yàn)證碼的token(判斷此處是否有設(shè)備碼,如沒有不生成設(shè)備碼token)
if("app".equals(channel) && "".equals(mid)==false) {
//基于設(shè)備碼的token(key:用戶id,value:隨機(jī)字符串+設(shè)備碼。)
String mtoken = Tools.MD(muuid+mid);
//設(shè)備碼token有效時間7天
stringRedisTemplate.opsForValue().set(id, mtoken, 7, TimeUnit.DAYS);
}
return new String[]{uuid,muuid};
}
要求是每次基于設(shè)備碼token登陸后要更換token。所以這里有個 token登陸和更換的方法:
//這是換取mtoken和生成普通token的方法(能走這個接口說明肯定是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};
}
好的。幾乎全部配置就這樣。下午和前端也試過了~全部跑通,沒有什么問題。然后可能有的地方設(shè)計冗余且不合理。不過水平有限,起碼功能實(shí)現(xiàn)了。親們有好的建議可以留言哈~~
然后如果有什么疑問或者覺得我說的哪里有問題或者哪里不懂的歡迎留言或者私信指出。
全文手打~~這么不容易的寫個文~~如果你覺得用到了理解了~留個言點(diǎn)個贊轉(zhuǎn)個發(fā)什么的啊~