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

来源:互联网 发布:采集淘宝卖家信息 编辑:程序博客网 时间:2024/06/08 17:24

   设计一个可扩展的登录系统,分为三步,首先要设计好数据表,其次拿到登录用户的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());}





0 0
原创粉丝点击