可扩展的登录——第三方登录
来源:互联网 发布:采集淘宝卖家信息 编辑:程序博客网 时间: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"
登录成功,系统可以知道:
- 该用户的id,例如,
1230001
; - 该用户的口令,例如,
"hello"
; - Cookie过期时间,可由当前时间戳+固定时长计算,例如,
1461288165
; - 系统固定的一个随机字符串,例如,
"secret"
。
把上面4部分拼起来,得到:
"1230001:hello:1461288165:secret"
当浏览器发送Cookie回服务器时,我们就可以按照下面的方式验证Cookie:
- 把Cookie分割成三部分,得到用户id,过期时间和hash值;
- 如果过期时间已到,直接丢弃;
- 根据用户id查找用户,得到用户口令;
- 按照生成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());}
- 可扩展的登录——第三方登录
- 第三方登录——QQ登录
- PHP第三方登录—QQ登录
- 第三方登录—QQ登录
- 【第三方登录】第三方登录 Part1 —— QQ登录(2016-09最新版)
- Android——第三方QQ登录
- 第三方登录—OAuth2.0协议
- 第三方登录—OAuth2.0协议
- 关于mob第三方登录的坑——微信篇
- QQ的第三方登录
- ShareSdk的第三方登录
- 第三方登录的原理
- 第三方登录的原理
- 第三方登录的总结
- 第三方登录的原理
- 第三方登录的原理
- QQ的第三方登录
- Android_第三方登录——QQ登录
- 机器视觉开源代码集合
- 成睿进销存软件免费版
- C++学习笔记1(结构体,命名空间,标准输入输出,引用,函数,构造函数)
- google gerrit repo, git commit如何自动生成Change-Id
- 解决debian中安装mysql后其他机器navicat无法连接问题
- 可扩展的登录——第三方登录
- Android listview和gridview获取当前xy坐标是第几个item
- Codevs 3196 黄金宝藏
- Java并发编程:Callable、Future和FutureTask
- java-调试hotspot
- 技巧:通过特定URI打开Win10指定设置页面
- Rails问题整理
- LeetCode 145 Binary Tree Postorder Traversal (后序遍历二叉树)
- phpstorm 2016.2 的最新破解方法