可扩展的登录——第三方登录

原创 2016年08月31日 09:41:56

   设计一个可扩展的登录系统,分为三步,首先要设计好数据表,其次拿到登录用户的cookie,然后再判断cookie是否有效

一、设计数据表

    常用的第三方登录有qq、微信、微博等。以微博登录为例,由于微博使用OAuth2协议登录,所以,一个登录用户会包含他的微博身份的ID,一个Access Token用于代表该用户访问微博的API和一个过期时间。只要添加另外一个表就可以解决这个问题,把这个表和用户表关联起来。每一种X-Auth表都存储了用户的登录认证信息,并通过user_id关联到Users表。这样一来,不但登录过程简化了,而且一个用户可以使用多种方式登录。只要登录成功,拿到了user_id,最后读取Users表是为了获得用户的Profile,这样读出来的数据也更安全,因为Users表不包含用户口令,不会因为暴露API而不小心把口令给泄露出去。

二、用户认证生成cookie

每个已认证用户的信息都必须通过Cookie来传递,服务器的session也无非是靠一个特殊名称的Cookie来识别而已,只不过由服务器本身帮你完成了解析Cookie、在session中查找User的过程,而代价却是内存占用高,单台服务器变成有状态,无法简单扩展成集群。遇到不懂事的年轻人,什么都敢往session里扔,很快就把服务器搞死了。

确认用户身份,我们需要一个统一的Authenticator接口

public interface Authenticator {
    // 认证成功返回User,认证失败抛出异常,无认证信息返回null:
    User authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException;
}
接下来,对于每一种类型的认证,我们都编写一个对应的Authenticator的实现类。例如,针对表单登录后的Cookie,需要一个LocalCookieAuthenticator

public LocalCookieAuthenticator implements Authenticator {
    public User authenticate(HttpServletRequest request, HttpServletResponse response) {
        String cookie = getCookieFromRequest(request, 'cookieName');
        if (cookie == null) {
            return null;
        }
        return getUserByCookie(cookie);
    }
}

对于直接用Basic认证的Authorization Header,我们需要一个BasicAuthenticator
public BasicAuthenticator implements Authenticator {
    public User authenticate(HttpServletRequest request, HttpServletResponse response) {
        String auth = getHeaderFromRequest(request, "Authorization");
        if (auth == null) {
            return null;
        }
        String username = parseUsernameFromAuthorizationHeader(auth);
        String password = parsePasswordFromAuthorizationHeader(auth);
        return authenticateUserByPassword(username, password);
    }
}

对于用API Token认证的方式,同样编写一个APIAuthenticator

public APIAuthenticator implements Authenticator {
    public User authenticate(HttpServletRequest request, HttpServletResponse response) {
        String token = getHeaderFromRequest(request, "X-API-Token");
        if (token == null) {
            return null;
        }
        return authenticateUserByAPIToken(token);
    }
}
然后在一个统一的入口处,例如Filter里面,把这些Authenticator全部串起来,让它们依次自己去尝试认证:

public class GlobalFilter implements Filter {
    // 所有的Authenticator都在这里:
    Authenticator[] authenticators = initAuthenticators();

    // 每个页面都会执行的代码:
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        User user = null;
        for (Authenticator auth : this.authenticators) {
            user = auth.authenticate(request, response);
            if (user != null) {
                break;
            }
        }
        // user放哪?
        chain.doFilter(request, response);
    }
}
认证成功后的User对象放到一个与业务逻辑相关的地方了,比如UserContext
public class GlobalFilter implements Filter {
    Authenticator[] authenticators = initAuthenticators();

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 链式认证获得User:
        User user = tryGetAuthenticatedUser(request, response);
        // 把User绑定到UserContext中:
        try (UserContext ctx = new UserContext(user)) {
            chain.doFilter(request, response);
        }
    }
}
任何地方需要获得当前User

User user = UserContext.getCurrentUser();
三、生成cookie和认证cookie

防止伪造用单向函数,MD5就是单向函数。方法是计算hash的时候,不仅只包含用户口令,还包含Cookie过期时间,以及其他相关随机数,这样计算的hash就非常安全。

假设用户仍以用户名"admin",口令"hello"登录成功,系统可以知道:

  1. 该用户的id,例如,1230001
  2. 该用户的口令,例如,"hello"
  3. Cookie过期时间,可由当前时间戳+固定时长计算,例如,1461288165
  4. 系统固定的一个随机字符串,例如,"secret"

把上面4部分拼起来,得到:

"1230001:hello:1461288165:secret"

当浏览器发送Cookie回服务器时,我们就可以按照下面的方式验证Cookie:

  1. 把Cookie分割成三部分,得到用户id,过期时间和hash值;
  2. 如果过期时间已到,直接丢弃;
  3. 根据用户id查找用户,得到用户口令;
  4. 按照生成Cookie时的算法计算md5,与Cookie自带的hash值对比。

如果用户自己对Cookie进行修改,无论改用户id、过期时间,还是hash值,都会导致最终计算结果不一致。

即使用户知道自己的id和口令,也知道服务器的生成算法,他也无法自己构造出有效的Cookie,原因就在于计算hash时的“系统固定的随机字符串”他不知道。

这个“系统固定的随机字符串”还有一个用途,就是编写代码的开发人员不知道生产环境服务器配置的随机字符串,他也无法伪造Cookie。

md5算法还可以换成更安全的sha1/sha256。

绑定用户

把User用ThreadLocal绑定到当前处理线程:

public class UserContext {
    public static final ThreadLocal<User> current = new ThreadLocal<User>();
}

别忘了开闭原则“对扩展开放,对修改关闭”

public class UserContext implements AutoCloseable {

    static final ThreadLocal<User> current = new ThreadLocal<User>();

    public UserContext(User user) {
        current.set(user);
    }

    public static User getCurrentUser() {
        return current.get();
    }

    public void close() {
        current.remove();
    }
}

单与否不看代码量本身,而是看调用起来是不是简单。在Filter中调用起来就非常简单:

public class MyFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        User user = tryGetAuthenticatedUser(request, response);
        try (UserContext context = new UserContext(user)) {
            chain.doFilter(request, response);
        }
    }
}
使用场景如:

try (UserContext context = new UserContext(user)) {
    // 当前用户是user:
    processProfile(UserContext.getCurrentUser());
    // 需要更高权限的admin才能执行的操作怎么办?
    // 方法是获取一个admin用户:
    try (UserContext context = new UserContext(getAdmin())) {
        // 现在的当前用户是admin:
        processAdminJob(UserContext.getCurrentUser());
    }
    // 现在当前用户又自动变回了普通user:
    processProfile(UserContext.getCurrentUser());
}





版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

设计一个可扩展的用户登录系统

在Web系统中,用户登录是最基本的功能。要实现用户名+密码登录,很多同学的第一想法就是直接创建一个Users表,包含username和password两列,这样,就可以实现登录了:id | usern...

设计一个可扩展的用户登录系统 (2)

转自 廖雪峰老师 在设计一个可扩展的用户登录系统 (1)中,我们设计了可扩展的数据库表的结构,基本思想是: Users表只存储User的Profile信息,没有任何认证信息(例如,不存Passwo...

设计一个可扩展的用户登录系统

在Web系统中,用户登录是最基本的功能。要实现用户名+密码登录,很多同学的第一想法就是直接创建一个Users表,包含username和password两列,这样,就可以实现登录了: id | us...

设计一个可扩展的用户登录系统 (3)

转 廖雪峰老师---------------- 设计一个可扩展的用户登录系统 (3) 廖雪峰 / 编程 / 2016-4-22 12:12 / 阅读: 5867 在系列 (1)和系列 (2)中...

灵活且可扩展的架构框架TOGAF——上海信息化培训中心

目前关于企业架构的方法论有很多种,市场占有率最大的和最知名的是国际非盈利性组织OpenGroup的TOGAF企业架构方法论。Open Group于1993年开始应客户要求制定系统架构的标准,在1995...

基于弹性计算平台——构建高可用、可扩展的应用

前不久,Facebook宣布投资10亿美元收购仅成立15个月的移动照片分享应用Instagram,消息传出时,人们不仅惊叹于这笔巨额的交易,更为这支13个人的小团队感到不可思议。Instagram的A...

扣丁学堂——ExpandableListView(可扩展列表)

扣丁学堂——ExpandableListView随堂代码分享【源码下载】 (如链接消失请直接给我留言) ExpandableListView代码演示效果截图 显然,这里显示的效果是类QQ...

分布式命令模式——互联系统的一种可扩展的命令模式

分布式命令模式是一种用来解决架构设计的建议模式。相比设计通常的应用而言,在互联系统中应该更多地考虑使用它。这种模式的目标是让独立系统与互联系统都有相同的设计。这种模式允许开发者将精力集中在设计一个遵循...

05.实例篇:仿QQ好友列表——ExpandableListView可扩展列表的使用(上篇)

1.qian ListView类控件在APP中经常使用,本文主要介绍ExpandableListView,完成两级列表显示的效果    这一节(上篇、下篇)主要介绍如何仿照QQ,以及ListVie...

灵活且可扩展的架构框架TOGAF——上海信息化培训中心

目前关于企业架构的方法论有很多种,市场占有率最大的和最知名的是国际非盈利性组织OpenGroup的TOGAF企业架构方法论。Open Group于1993年开始应客户要求制定系统架构的标准,在1995...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)