微服务权限控制(二)共享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<String, Object> login(String userId, String password) {
    //验证用户登录信息有效性
    Boolean result = validate(userId, password);
    Map<String,Object> 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需求后登录认证的流程如下

通常需要登录验证的接口列表,不刷新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<String,Object> 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;
    }

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

(完)

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微服务架构下的应用通常会面临共享会话的问题。由于微服务的特性,每个微服务都是独立部署和运行的,因此无法直接共享会话状态。然而,有几种方法可以在微服务架构中实现会话共享: 1. 使用无状态会话:将会话状态保存在客户端,每个请求都包含会话信息。微服务通过解析请求中的会话信息来获取会话状态。这种方式不需要在微服务之间共享会话状态,但会增加每个请求的负载大小。 2. 使用共享数据库:将所有微服务的会话状态存储在共享数据库中,例如Redis或数据库。每个微服务都可以访问和更新该数据库中的会话状态。这种方式需要确保对共享数据库的访问是高效和可靠的。 3. 使用分布式缓存:使用像Redis这样的分布式缓存来存储会话状态。每个微服务可以从缓存中获取会话状态,并将更新后的状态写回缓存。这种方式可以减轻对共享数据库的负载,并提供更好的性能。 4. 使用JWT(JSON Web Tokens):将会话信息编码为JWT,并将其传递给每个微服务。每个微服务可以验证和解码JWT来获取会话信息。这种方式不需要在微服务之间共享会话状态,但需要确保JWT的安全性和完整性。 需要注意的是,无论选择哪种方法,都需要确保会话状态的一致性和安全性。同时,根据具体的应用场景和需求,选择适合的方法来共享会话。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值