分布式多端登陆token验证的实现
首先说下需求背景:这是一个分布式微服务项目。然后现在要实现的功能是可同时app,小程序,公众号和网页端在线。
这里有几个细微化需求:
1,网页端是每次登陆的,而且不涉及跨域问题。所有token/session/cookie都可以。但为了统一所以这里也用token。
2,小程序是每次都用微信授权,所以也不需要做什么特别处理。只要在授权登陆的时候生成普通token。
3,app第一次登陆是账号密码登陆。然后系统会发一个基于设备码的token凭证。这个凭证有效期七天。也就是七天内都可以凭借这个凭证+设备码 直接登陆。(同样每次登陆后这个时间重置为7天)
4,app登陆时会返回一个普通token,然后每个接口都要验证此token。此token每次访问时长都会变成30min。(也就是说30min无操作会失效)
token组成:
1,普通token:key是:pc/app+“-”+用户id, value: 随机字符串。返回给前端的是随机字符串。 每次传给我的:pc/app+“-”+用户id+字符串
2,设备码token:key:用户id,value:随机字符串+设备码。 返回给前端的是这个随机字符串。 传给我的:用户id+字符串(+设备码)
然后因为需求比较杂。所以可能看起来有点不明确。咱们现在一样一样做。
1,接口的token验证:
因为我是cloud项目,用zuul做的api网关。所以我这边是直接在zuul中进行登陆拦截和token验证(如果zuul都不会用的先去了解下zuul吧)。
这里大致说一下,zuul本身提供一个拦截器。我们只要自己创建一个拦截器然后继承并重写抽象方法即可。代码如下:
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是具体的拦截类型。一般像是权限验证的应该都是在路由之前。所以这里我们”pre“即可。
-filterOrder代表过滤器顺序。具体我还真没太看,反正根据默认是-1。
-shouldFilter代表这个过滤器是否生效。一般不需要验证的比如登陆,注册等设置为不生效。剩下的都生效。true是生效。
-Run方法:这个是主要的处理逻辑的地方,我们做权限控制、日志等都是在这里。
然后下面附上我基于普通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;
/**
* 权限验证 Filter 注册和登录接口不过滤
*
* 在两个地方都没有找到token,就会返回 401 无权限,并给与文字提示
*
* @author 变异者
*
*/
@Component
public class AuthFilter extends ZuulFilter {
@Autowired
StringRedisTemplate stringRedisTemplate;
// 排除过滤的 uri
// 个人用户手机登陆
private static final String LOGIN_TEL = "/person/telLogin";
// 个人用户微信登陆
private static final String LOGIN_WECHAT = "/person/wechatLogin";
// 个人用户手机注册
private static final String TEL_ADD = "/person/userReg";
// 无权限时的提示语
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则直接返回无权限
setUnauthorizedResponse(requestContext, INVALID_TOKEN);
} else {
//如果有token则验证token
verifyToken(requestContext, request, headerToken);
}
return null;
}
/**
* 设置 401 无权限状态
*/
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中校验token
*/
private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) {
// 需要从header 中取出 userId 来校验 token 的有效性,因为每个用户对应一个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验证通过了。这时候将token存在时长重置为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()) || COMMADMIN_LOGIN.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验证是否登陆就做完了。
至于app设备码验证还没做。等我做完了补上。
然后如果有什么疑问或者觉得我说的哪里有问题或者哪里不懂的欢迎留言或者私信指出。
全文手打~~这么不容易的写个文~~如果你觉得用到了理解了~留个言点个赞转个发什么的啊~