Zuul网关优化实践之token校验

公司最近要上线一个活动功能,由于后端这边的安全等级非常低,用户编号都是通过接口明文传输,稍微懂点技术的都可以利用请求监控拿到请求的URL和参数,由于之前都是赶业务需求,导致后端整体安全性偏低了,所以趁着这次redis也在应用中,所以决定在zuul中加入一个token授权功能,来缓解一下别人非法模拟其他用户编号来请求刷数据。

利用redis + token来实现单点登录

实现思路

Filter

zuul一共有: prerouteposterror 四种类型的拦截器,分别对应的时机为开始前、执行中、执行后、异常发生。
我们会在postpre两个阶段分别做处理:

  1. post : 登录完成之后,回调这个接口,来判断是否是登录接口,如果是的话生成token并且绑定用户信息。
  2. pre : 访问其他接口之前,先判断是否携带token,携带了则做校验(兼容老的接),如果符合的话则将用户信息加入到header头中,传递到其他业务服务中去。

单点登录这块的话,可以将token和用户编号做各做缓存,如果登录的时候,查看用户id 的key是否有token的值,如果有则删掉已经登录的token的key的信息。

另外,再全局写一个工具类,让其他业务服务调用的时候直接通过该工具类直接获取用户的信息,如果获取不到则直接抛出需要登录的异常。(我们对特定的异常做了包装,只会返回要登录的文本消息,免得其他小伙伴各种判断是否null啊啥的~~)

当然如果还可以做的好一点的话是将所有微服务的路由进行管理,哪些路由是必须要通过token验证的,哪些接口需要做角色校验的、后台统一维护。而且也可以直接做到网关拦截,避免了没有携带token的也能直接过到微服务中去。

代码样本

1. 检测是否是特定的登录接口

/**
 * 登录接口处理器
 * 该类仅仅只是将登记的登录接口进行token绑定
 *
 * @author : liukx
 * @time : 2020/8/19 - 14:23
 */
@Component
public class LoginPostFilter extends ZuulFilter implements InitializingBean {

    private Map<String, String> loginMap = new HashMap<>();

    @Autowired
    private DefaultTokenFactory defaultTokenFactory;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return 100;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        String requestURI = ctx.getRequest().getRequestURI();
        if (loginMap.containsKey(requestURI.replaceAll("//", "/"))) {
            logger.info(" 是登录接口 : " + requestURI);
            return true;
        }
        return false;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        // 这里需要注意一下、如果你没有找到的话可以去ctx.getResponseDataStream()中拿,但是这个需要将流进行转换,我们应用里面在这个filter执行之前已经从ctx.getResponseDataStream()拿过一次并且放到ctx.setResponseBody(responseBody)里面了,所以下面会拿到值.
        String responseBody = ctx.getResponseBody();
        String managerType = loginMap.get(request.getRequestURI().replaceAll("//", "/"));
        try {
            if (responseBody != null) {
                AbstractTokenManager abstractTokenManager = defaultTokenFactory.getTokenManager(managerType);
                JSONObject resultJson = JSON.parseObject(responseBody);
                Boolean success = resultJson.getBoolean("success");
                if (success) {
                    String token = abstractTokenManager.createToken(resultJson);
                    ctx.addZuulResponseHeader(abstractTokenManager.getHeaderName(), token);
                } else {
                    logger.debug(" 登录失败! ");
                }
            }
        } catch (Exception e) {
            logger.error("login filter ", e);
        }
        return null;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 登录路由和token工厂进行关联
        loginMap.put("/user/login/brandXcxLogin", XcxTokenManager.tokenName);
    }
}

2. 验证携带了token的header正确性


/**
 * token校验的拦截器
 *
 * @author : liukx
 * @create : 2018/6/22 14:54
 * @email : liukx@elab-plus.com
 */
@Component
public class TokenFilter extends ZuulFilter {
    private Logger logger = LoggerFactory.getLogger(TokenFilter.class);

    @Autowired
    private DefaultTokenFactory defaultTokenFactory;
    
    /**
     * 拦截器的类型
     *
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * 拦截器的优先级,越小优先级越高
     *
     * @return
     */
    @Override
    public int filterOrder() {
        return 10;
    }

    /**
     * 是否执行该过滤器
     *
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 该拦截器执行的具体逻辑
     *
     * @return
     */
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        HttpServletRequest request = ctx.getRequest();

        List<AbstractTokenManager> tokenManagerList = defaultTokenFactory.getTokenManagerList();
        for (AbstractTokenManager abstractTokenManager : tokenManagerList) {
            String headerName = abstractTokenManager.getHeaderName();
            String tokenValue = request.getHeader(headerName);
            if (StringUtils.isNotEmpty(tokenValue)) {
                Object tokenValueObject = abstractTokenManager.checkToken(tokenValue);
                if (tokenValueObject != null) {
                    ctx.addZuulRequestHeader(headerName, JSON.toJSONString(tokenValueObject));
                    // 可以告诉后端是从哪端登录的.
                    ctx.addZuulRequestHeader("tokenType", headerName);
                    logger.info("鉴权通过 : " + abstractTokenManager.toString());
                    break;
                } else {
                    // 这里可以抛异常来告诉前端 token过期了
                    logger.debug(" token 过期或者不存在 ! ");
                }
            }
        }
        
        return null;
    }

}

DefaultTokenFactory
这个工厂是用来管理不同token规则的,因为实际业务中可能有来自APP、小程序、WEB端的用户登录,他们返回的数据结构、token的生成和校验规则、有效时间都不同。

/**
 * 默认的token创建工厂
 *
 * @author : liukx
 * @time : 2020/8/19 - 17:17
 */
@Component
public class DefaultTokenFactory implements InitializingBean {

    @Autowired
    private List<AbstractTokenManager> tokenManagerList;

    private Map<String, AbstractTokenManager> tokenManagerMap = new HashMap<>();

    public AbstractTokenManager getTokenManager(String managerType) {
        return tokenManagerMap.get(managerType);
    }

    public List<AbstractTokenManager> getTokenManagerList() {
        return tokenManagerList;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        for (AbstractTokenManager abstractTokenManager : tokenManagerList) {
            tokenManagerMap.put(abstractTokenManager.getHeaderName(), abstractTokenManager);
        }
    }
}
/**
 * 抽象token管理类,子类可以根据自己的规则重写相应的方法
 *
 * @author : liukx
 * @time : 2020/8/19 - 16:37
 */
public abstract class AbstractTokenManager<T> {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    protected CacheTemplate cacheTemplate;

    public abstract String getHeaderName();

    /**
     * 校验token
     *
     * @param token
     * @return
     */
    public T checkToken(String token) {
        String key = RedisKeyEnums.LOGIN_XCX_TOKEN.key(token);
        T o = (T) cacheTemplate.string().get(key);
        if (o != null) {
            // 刷新token有效时间
            cacheTemplate.getRedisTemplate().expire(key, RedisConstants.defaultExpire, TimeUnit.SECONDS);
            return o;
        }
        return null;
    }

    /**
     * 获取对象
     *
     * @param result
     * @return
     */
    public abstract T getObject(JSONObject result);

    /**
     * 生成tokenkey
     *
     * @return
     */
    protected String generateToken(JSONObject result) {
        String key = RandomUtils.randomString(3) + "_" + System.currentTimeMillis();
        String token = MD5Utils.encode(key);
        logger.debug("默认生成的token:" + token);
        return token;
    }

    /**
     * 默认的有效时间,如果业务有需要变更可重写
     *
     * @return
     */
    protected Integer defaultExpire() {
        return RedisConstants
                .defaultExpire;
    }
    /**
     * 创建token并加入到缓存中
     */
    public String createToken(JSONObject result) {
        // 创建对象
        T object = getObject(result);
        // 生成key的规则
        String token = generateToken(result);
        // 加入到缓存中
        cacheTemplate.string().set(RedisKeyEnums.LOGIN_XCX_TOKEN.key(token), object, defaultExpire(), TimeUnit.SECONDS);
        return token;
    }
}
/**
 * 小程序token管理器
 *
 * @author : liukx
 * @time : 2020/8/19 - 16:42
 */
@Component
public class XcxTokenManager extends AbstractTokenManager<BrandXcxUserResponse> {
    /**
     * 处理小程序的token的header名称
     */
    public static final String tokenName = HeaderConstants.HEADER_TOKEN_XCX;

    @Override
    public String getHeaderName() {
        return tokenName;
    }

    @Override
    public BrandXcxUserResponse getObject(JSONObject result) {
        return result.getObject("single", BrandXcxUserResponse.class);
    }
}

CacheTemplate 是针对RedisTemplate包装了下。

后续还需要优化的地方:

  1. 小程序动不动就杀死进程退出重新登录了,如果给它设置了失效时间过长,则会一个用户产生过多的token。因为每次重新登录,token都会重新生成,之前的可能还没有过期。不过用单点登录的思路可以解决。

思考中…

有好的想法或者思路可以探讨。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值