微服务权限控制(二)共享Session方式的登录认证

接上一篇的权限控制,再讨论再网关zuul的登录认证实现。

网关使用SpringCloud的zuul,登录认证选择使用自定义共享session的方式,来实现集群的登录验证。保护接口的私密,保证系统安全。

Filter

zuul提供了filter来对请求进行过滤处理,首先,了解网关zuul的filter。

zuul的filter有三种类型的,pre,route,post,error,static。

  • pre,在路由之前的过滤
  • route,是在pre之后,路由过程中过滤
  • post,是在route之后,结果返回之前过滤
  • error,异常处理,在异常之后过滤
  • static,配置一组URL,返回静态资源,不路由到后端服务

因为登录验证在网关接受到请求之后就要做的,所以在prefilter中增加登录验证的逻辑。

请求Header

cookies和特殊的http请求headers

使用共享session的方式,后端服务必然也要使用用户的缓存数据,这就需要将用户标识传递给后端服务,通过http请求Header传递,将用户请求的Cookie或者特殊的http请求headers(取决于自己的需要)传递给网关后面的业务服务。zuul提供了配置zuul.sensitiveHeaders来配置需要传递给后端服务的请求头。如:

zuul:

  sensitive-headers: Cookie,Set-Cookie

登录

要登录验证,首先需要登录,登录如何实现呢。

验证用户登录信息有效性后,生成token,存储用户token到redis中,作为有效token,同时将用户的缓存信息存入redis中。

redis中数据存储结构为两个键值对

  • 键为用户token,值为用户数据,实现token有效性,用户数据缓存功能。
  • 键为用户ID,值为用户token,可以通过用户ID查询用户token,实现立刻失效用户token功能。

样例代码如下:

 
public Map login(String userId, String password) {
    //验证用户登录信息有效性
    Boolean result = validate(userId, password);
    Map map = new HashMap<>();
    //存储两个键值对,一个是token和用户数据的键值对,第二个是用户id和token的键值对,实现通过用户ID找到用户的token,实现即刻失效用户token的功能。根据需求,也可以只存储一个。
    if(result){
        String body = getUser(userId);
        String sessionKey = SESSION_PREFIX+"_"+sessionId;
        String userKey = USER_ID_PREFIX+"_"+userId;
        redisTemplate.opsForValue().set(sessionKey, userId, EXPIRE_SECOND, TimeUnit.SECONDS);
        redisTemplate.opsForValue().set(userKey, sessionId, EXPIRE_SECOND, TimeUnit.SECONDS);
        redisTemplate.opsForValue().set(dataKey, body, EXPIRE_SECOND, TimeUnit.SECONDS);

        map.put("code","0");
        map.put("msg","ok");
        return map;
    }

    map.put("code","0");
    map.put("msg","用户名或密码错误");
    return map;
}

登录验证

考虑到登录验证的时候,还有隐藏的其他可能需要的功能。

  1. 某些接口不需要不进行登录验证,如登录,注册,获取验证码等接口。
  2. 某些接口需要登录验证,但是不需要刷新token有效时间,如客户端轮询请求的接口。
  3. 特定场景下IP黑、白名单。
  4. 处于安全考虑的接口流量控制。

考虑将最基本的第一个和第二个功能加入进来,其他的后续再实现。

增加1,2需求后登录认证的流程如下

微服务权限控制(二)共享Session方式的登录认证_第1张图片

通常需要登录验证的接口列表,不刷新token有效时间的接口列表都通过配置来实现,这里不再赘述。

登录认证代码如下:

@Component
public class PreFilter {

    public static final Logger log = LoggerFactory.getLogger(PreFilter.class);

    @Autowired
    private RedisTemplate redisTemplate;

    @Value("${server.context-path}")
    private String contextPath;

    @Value("${auth.session.expireSecond}")
    private Integer expire;

    @Override
    public String filterType() {

        return "pre";
    }

    @Override
    public int filterOrder() {

        return 1;
    }

    @Override
    public boolean shouldFilter() {

        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 得到Rquest Response
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest req = ctx.getRequest();
        HttpServletResponse res = ctx.getResponse();
        try {
            String uri = req.getRequestURI();
            //得到SessionId
            String sessionId = req.getSession(true).getId();

            //是否登录认证
            Boolean isIgnore = isIgore(uri);
            if(isIgnore){
                ctx.setSendZuulResponse(true);// 对该请求进行路由
                ctx.setResponseStatusCode(200);
                return null;
            }
            //是否刷新session过期时间
            Boolean isRefreshExpire = isRefreshExpire(uri);
            Boolean isLogin = false;
            if(isRefreshExpire){
                // 检查过期时间并刷新
                isLogin = checkLoginWithExpire(sessionId);
            }else{
                // 检查过期时间
                isLogin = checkLoginWithoutExpire(sessionId);
            }

            //认证成功
            if(isLogin){
                ctx.setSendZuulResponse(true);// 对该请求进行路由
                ctx.setResponseStatusCode(200);
                return null;
            }
            //认证失败
            Map result = new HashMap<>();
            result.put("code","1");
            result.put("msg","请登录");
            ctx.getResponse().setContentType("application/json;charset=utf-8");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody(JSON.toJSONString(result));// 返回错误内容
            return null;
        } catch (NoSuchAlgorithmException e) {
            log.error("generatePVID error", e);
        }
        return null;
    }

    /**
     * 是否登录认证
     * @param uri 请求接口标志
     * @return
     */
    private Boolean isIgore(String uri) {
        String s = uri.replaceAll(contextPath,"");
        for(String reg : ZuulInit.getIgnoreUrl()){
            if(s.matches(reg)){
                return true;
            }
        }
        return false;
    }

    /**
     * 是否刷新过期时间
     * @param uri
     * @return
     */
    private Boolean isRefreshExpire(String uri){
        String s = uri.replaceAll(contextPath,"");
        for(String reg : ZuulInit.getExpireUrl()){
            if(s.matches(reg)){
                return true;
            }
        }
        return false;
    }

    /**
     * 检查过期时间,并刷新过期时间
     * @param sessionId
     * @return
     */
    private Boolean checkLoginWithExpire(String sessionId){
        String key = SESSION_PREFIX + domain + "_" + sessionId;
        String userId = (String)redisTemplate.opsForValue().get(key);
        String userKey = USER_PREFIX + "_" + userId;
        String dataKey = DATA_PREFIX + "_" + sessionId;
        if(!StringUtil.isEmpty(userId)){
            Boolean r = redisTemplate.expire(key, expire,TimeUnit.SECONDS);
            //刷新时间没有成功,返回认证不通过
            if(!r){
                return false;
            }
            r = redisTemplate.expire(userKey, expire, TimeUnit.SECONDS);
            if(!r){
                return false;
            }
            r = redisTemplate.expire(dataKey, expire, TimeUnit.SECONDS);
            if(!r){
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * 检查过期时间
     * @param sessionId
     * @return
     */
    private Boolean checkLoginWithoutExpire(String sessionId){
        String key = SESSION_PREFIX + domain + "_" + sessionId;
        String userId = (String)redisTemplate.opsForValue().get(key);
        if(!StringUtil.isEmpty(userId)){
            return true;
        }
        return false;
    }

如上代码只是一个简单的功能实现,还需要在此基础上面向对象的设计、优化等考虑。

(完)

你可能感兴趣的:(springcloud)