本文利用到的JustAuth的传送门。git
本文纯属菜鸡视角。在开发者至关简略的官方使用文档的基础上,进入源码查看文档中使用的函数的具体实现,同时经过QQ第三方登陆这一特例,工具开发者很是规范的命名和注释,推测整个工具的实现逻辑。缓存
绝大部分第三方登陆采用OAuth2.0协议,其流程符合以下流程图:
关于OAuth2.0流程复杂化了(用户受权登陆后,服务器不能直接拿到能够惟一标识用户的id)登陆流程,到底在安全性上如何提供了好处,请自行谷歌。安全
A阶段
跳转到QQ的受权登陆网页
必需参数 response_type client_id redirect_uri state
其中response_type为必定值服务器
B阶段
用户受权登陆后,腾讯那边带上必要的数据以GET参数的模型经过GET访问咱们设定的返回地址。
获得的数据 code state
并要校验发回的state与A阶段的state是否相同微信
// 官方文档中并未有此函数,只是我自用的。 private AuthQqRequest getAuthQqRequest(){ String client_id = 填入你本身的client_id; String redirect_uri = 填入你本身的redirect_url; String client_secret = 填入你本身的client_secret; AuthConfig build = AuthConfig.builder() .clientId(client_id) .clientSecret(client_secret) .redirectUri(redirect_uri) .build(); return new AuthQqRequest(build); }
/** * 官方伪代码 */ @RequestMapping("/render/{source}") public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException { AuthRequest authRequest = getAuthRequest(source); String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); response.sendRedirect(authorizeUrl); }
/** * 个人具体到QQ上的实现 * 由于我胸无大志只想着QQ因此不须要用{source}来肯定我在用谁的(是微信啊,仍是QQ啊仍是gitee啊)的第三方登陆功能。 */ @RequestMapping("/render") public void render(HttpServletResponse resp) throws IOException { AuthQqRequest authQqRequest = getAuthQqRequest(); resp.sendRedirect(authQqRequest.authorize(AuthStateUtils.createState()); } }
/** * 官方文档的伪代码 */ @RequestMapping("/callback/{source}") public Object login(@PathVariable("source") String source, AuthCallback callback) { AuthRequest authRequest = getAuthRequest(source); AuthResponse response = authRequest.login(callback); return response; }
进行心无大志,醉心QQ的化简app
/** * 官方文档的伪代码 */ @RequestMapping("/callback/QQ") public Object login(AuthCallback callback) { AuthRequest authRequest = getAuthQqRequest();//getAuthQqRequest()是准备阶段我自用的那个函数 AuthResponse response = authRequest.login(callback); return response; }
问题来了:这一阶段应当要完成的state校验是如何处理的呢?
合理的推测是在authRequest.login(callback);
中的login函数中实现的(这里的callback
是AuthCallback
类的实例,而AuthCallback
中有code,state等在B阶段时会被以GET参数形式被第三方调用回调地址传回的参数。由于SpringMVC致使这些参数直接被封装到callback
中了。)。所以进入代码探究:ide
default AuthResponse login(AuthCallback authCallback) { throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED); }
这是AuthRequest
类中的login方法,从异常信息可知,其依赖子类的具体实现。函数
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 public AuthResponse login(AuthCallback authCallback) { try { AuthChecker.checkCode(source, authCallback); this.checkState(authCallback.getState()); AuthToken authToken = this.getAccessToken(authCallback); AuthUser user = this.getUserInfo(authToken); return AuthResponse.builder().code(AuthResponseStatus.SUCCESS.getCode()).data(user).build(); } catch (Exception e) { Log.error("Failed to login with oauth authorization.", e); return this.responseError(e); } } //省略 }
显然,答案在this.checkState(authCallback.getState())
之中,去看看checkState方法。工具
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 protected void checkState(String state) { if (StringUtils.isEmpty(state) || !authStateCache.containsKey(state)) { throw new AuthException(AuthResponseStatus.ILLEGAL_REQUEST); } } //省略 }
如今,问题进一步细化。第一,校验state不为空无需多言,以后这里在检验state是否存在内存中,也就是说它以前就已经存入了内存,何时?怎么作的?第二,authStateCache
是什么?ui
关于第一个疑问,推测是在A阶段中的String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
过程当中缓存了state状态值是最合理的,由于紧接其后的B阶段就已经要求校验了。在此基础上有两个衍生推测:其一是AuthStateUtils.createState()
完成了缓存工做,其二是authRequest.authorize(...)
完成了缓存工做。
前往查看代码
public class AuthStateUtils { public static String createState() { return UuidUtils.getUUID(); } }
猜想一否决。
public String authorize(String state) { return UrlBuilder.fromBaseUrl(source.authorize()) .queryParam("response_type", "code") .queryParam("client_id", config.getClientId()) .queryParam("redirect_uri", config.getRedirectUri()) .queryParam("state", getRealState(state)) .build(); }
进一步怀疑由getRealState(state)
实现
public abstract class AuthDefaultRequest implements AuthRequest{ //省略 protected String getRealState(String state) { if (StringUtils.isEmpty(state)) { state = UuidUtils.getUUID(); } // 缓存state authStateCache.cache(state, state); return state; } //省略 }
猜想证明。确实在这一步完成了state的缓存。接下来就是考虑`
authStateCache`究竟是个什么东西。与问题二相同。
public abstract class AuthDefaultRequest implements AuthRequest { // 省略 protected AuthStateCache authStateCache; // 省略 }
AuthDefaultRequest
做为子类继承了父类AuthDefaultRequest
的成员变量authStateCache
。那么authStateCache
是被如何赋值?在目前截取到的代码中,authStateCache
均被直接使用而未见赋值,作出authStateCache
可能在构造器中被赋值的推测是合理的。
public abstract class AuthDefaultRequest implements AuthRequest { protected AuthConfig config; protected AuthSource source; protected AuthStateCache authStateCache; public AuthDefaultRequest(AuthConfig config, AuthSource source) { this(config, source, AuthDefaultStateCache.INSTANCE); } public AuthDefaultRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) { this.config = config; this.source = source; this.authStateCache = authStateCache; if (!AuthChecker.isSupportedAuth(config, source)) { throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE); } // 校验配置合法性 AuthChecker.checkConfig(config, source); } //省略 }
AuthDefaultRequest
做为一个抽象类,是不可能被new的,咱们new的通常都是它的具体实现类,具体到QQ上:
public class AuthQqRequest extends AuthDefaultRequest { public AuthQqRequest(AuthConfig config) { super(config, AuthDefaultSource.QQ); } public AuthQqRequest(AuthConfig config, AuthStateCache authStateCache) { super(config, AuthDefaultSource.QQ, authStateCache); } //省略 }
而回看准备阶段,用于生成AuthQqRequest
的代码,咱们是new AuthQqRequest(build)
这样建立实例的。即
new AuthQqRequest(build) new AuthQqRequest(build,AuthDefaultSource.QQ) new AuthDefaultRequest(build, AuthDefaultSource.QQ) new AuthDefaultRequest(build, AuthDefaultSource.QQ,AuthDefaultStateCache.INSTANCE)
到第四个构造器时图穷匕见,authStateCache
被赋值为AuthDefaultStateCache.INSTANCE
,那么接下来就要看AuthDefaultStateCache.INSTANCE
是个啥了。
public enum AuthDefaultStateCache implements AuthStateCache { INSTANCE; private AuthCache authCache; AuthDefaultStateCache() { authCache = new AuthDefaultCache(); } /** * 存入缓存 */ @Override public void cache(String key, String value) { authCache.set(key, value); } //省略 /** * 是否存在key,若是对应key的value值已过时,也返回false */ @Override public boolean containsKey(String key) { return authCache.containsKey(key); } //省略 }
注意AuthDefaultStateCache
是一个枚举,再加INSTANCE
,这种写法实际上是一种经过枚举实现的单例模式。具体状况能够Google,换成常见的单例形式,应该如此:
public enum AuthDefaultStateCache implements AuthStateCache { /* INSTANCE; private AuthCache authCache; AuthDefaultStateCache() { authCache = new AuthDefaultCache(); } */ //如下内容的效用与上面原码中上面的被注释部分差很少。主要体现一个单例模式。 private AuthDefaultStateCache(){} // 私有构造 private static AuthDefaultStateCache INSTANCE = null; // 私有单例对象 // 静态工厂 public static AuthDefaultStateCache getInstance(){ if (INSTANCE == null) { // 双重检测机制 synchronized (AuthDefaultStateCache.class) { // 同步锁 if (INSTANCE == null) { // 双重检测机制 INSTANCE = new AuthDefaultStateCache(); } } } return INSTANCE; } //其它部分是一个单例类内部的成员变量和一些方法。不存在什么等效。 }
既然是单例模式,那么AuthDefaultStateCache.INSTANCE
在整个应用中都是那一个,天然而然地,在A阶段得到它存储以后,再在B阶段得到,仍然是它,也所以天然能够查询以前被存下来的state了。