网上介绍OAuth协议的文章一大堆,但是都是介绍协议的规定,或者相关功能的使用流程等;比如xx登录先用appName和appId获取认证code,然后用code获取token,之后就可以使用token访问用户资源。
1、对于初学者来说这个过程有些知其然不知其所以然,帖子看了一大堆仍然头晕眼花,因此本篇文章我们采用逆向思维从需求出发通过分析实现一个三方登录框架流程
相信很多开发者用过或者接触过第三方登录,至少应该接触过业务流程:比如你在网上看了一篇文章写的很好,想给作者评论,却发现只有注册用户才能评论,可是你又不想去完成繁琐的注册流程如下图我们可以使用qq登录
点击qq图标我们会跳转到qq登录页,注意下面这个页面是qq的页面
选择你要授权的权限,并输入用户名和密码,验证通过后返回之前的页面就可以拿到qq的昵称等,相应的网站可能会使用qq返回的uid自动为你创建一个临时账号。这就是第三方登录的一个简单流程。
2、下面我们分析一下如果让我们来实现这个功能我们要怎么做呢?假设我们只需要获取用户头像的功能。
后面添加了一个单机版模拟原理的案例,大家先看分析然后看一遍案例就明白啦。
一:先实现一个基础版(此处的各个版本方案均为为了演示清楚基本原理,并不代表真实的生产实现)
访问我们的网站并点击qq登录,我们就给他重定向到qq提供的登录地址,这里qq负责验证用户的用户名及密码,校验通过他返回给我们一个状态码token=xxxx(别问为什么不直接把用户名和密码返回。。。因为不安全),告诉我们用户的qq名和密码验证通过了,同时qq服务器自己记录一下这个token对应的是哪个用户(比如将token和用户保存到redis)。然后我们就可以用这个token取调用用户头像的接口了(因为qq密码已经验证通过了)。
二:第二个版本
我们想一下上一个版本存在哪些问题呢?首先:qq验证通过后要跳转回我们自己的网站,这时候token只能放到重定向链接后当做参数返回,导致大家都可以看到认证通过后的token不安全;其次:token没有删除过期等机制,授权一次永久使用;
解决:重定向返回我们的网站时qq认证服务器先不返回token,而是生成一个一次性使用的code,重定向到我们的网站时通过后端携带code参数再次发送请求到qq认证服务器,此时qq服务器再生成token并保存,然后把token返回给客户端服务器,并且设置token的过期时间比如1小时自动过期;为了省去token过期用户重新授权的情况,qq还可以提供一个刷新token过期时间的方法。视客户端具体业务而定是否需要一致刷新token过期时间(有的网站可能授权一次持续一周,一周后需要再次授权,有的可能只需要授权一次就可以了)。
三:第三版
token的安全问题和过期问题解决后,我们看一下还有那些需要完善的呢?
比如qq服务器的认证功能不能随便谁都可以调用吧,得搞个权限控制,因此第三方网站想接qq登录,必须先在qq管理后台注册app,qq审核通过后给第三方网站分配一个appName和appId,请求qq登录时需要带上这些参数让它知道是谁来找他认证。
另外有的服务端可能对自己对外开放的权限需要细粒度的控制,比如获取昵称头像算基本授权,获取邮箱手机号算高级授权,这种情况就需要在授权接口中添加授权类型字段,告诉服务端你需要申请什么类型的授权,如果只申请了获取头像和昵称,那么最后获得的token只能读头像和昵称,无法读取用户邮箱因为用户没有给授权权限。
以上就是第三方登录的基本原理和流程,详细的协议说明,各种认证方式和类型,spring整合等大家自行去百度吧,文章很多,相信你掌握了这篇原理其它的就很容易看懂了。
oauth是一个协议标准。
百度百科:OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。
可以找一个qq或微信文档看一下,有条件的可以自己写一个demo。good luck for you。
3、认证方式:
授权码 Authorization Code
隐式许可 Implicit
资源所有者密码凭证 Resource Owner Password Credentials
客户端凭证 Client Credentials
4、我们以最常用的授权码方式来做demo,以下代码只是原理示意
图太长分了三张,源代码帖到最后供大家copy
启动项目访问:http://localhost:8080/oauth2/login?app=hero
返回:localhost:8080/index?app=hero&code=serverCode
我们可以看到后面多了一个code
访问返回的重定向链接,就可以返回token
实际的codeMap,tokenMap、未授权app等他家可以自己打断点试一下,也可以自己添加refreshtoken的相关方法进行测试,线上的话认证服务器和获取数据服务器一般是分离的,可以使用redis做缓存以及设置过期时间等。
源代码:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Controller
public class OauthController {
//模拟用户授权生成code,下一步通过code换token,一个code只能用一次用完即清除
private static ConcurrentHashMap<String, String> codeMap = new ConcurrentHashMap<>();
//模拟token和用户对应关系的存储结构
private static ConcurrentHashMap<String, String> tokenMap = new ConcurrentHashMap<>();
//模拟保存用户所有授权过的网站,用户也可以手动取消授权清除token
private static ConcurrentHashMap<String, Set<String>> authMap = new ConcurrentHashMap<>();
//备案过的第三方网站标识
private static HashSet<String> apps = new HashSet<>();
//模拟第三方网站必须先申请appId才能使用本公司的联合登录认证,这里模拟一个网站:hero
static {
apps.add("hero");
}
/**
* 登录页授权中心
*
* @param app 备案过的第三方网站appId
* @return 重定向地址,授权通过后携带code
*/
@RequestMapping("/oauth2/login")
@ResponseBody
public String login(String app) {
if (!apps.contains(app)) {
System.out.println("客户端未申请appId");
//此处为模拟,线上应该是重定向
return "localhost:8080/index?app=hero&code=error";
}
//用户同意授权并登陆成功后,随机生成一个code并保存下来,code保存到codeMap,并包含某个用户是否对某个app授权等信息
String code = "serverCode";
codeMap.put(code, "用户名");
Set<String> authSet = new HashSet<>();
authSet.add(app);
authMap.put("用户名", authSet);
//此处为模拟,线上应该是重定向
return "localhost:8080/index?app=hero&code=" + code;
}
/**
* code换token
* @param code 用户授权码
* @param app 备案过的第三方网站appId
* @return token
*/
@RequestMapping("/oauth2/token")
@ResponseBody
public Map<String, String> token(String code, String app) {
Map<String, String> map = new HashMap<>();
String str = codeMap.get(code);
if (str == null || str.length() == 0) {
System.out.println("用户未授权该app");
map.put("msg", "用户未授权该app");
} else {
//随机生成一个token
String token = "serverToken";
tokenMap.put(token, str);
//code用过一次后清除
codeMap.remove(code);
map.put("token", token);
}
return map;
}
/**
* 模拟第三方网站
* @param code 授权码
* @param app 备案过的第三方网站appId
* @return token
*/
@RequestMapping("/index")
@ResponseBody
public Map<String, String> index(String code, String app) {
Map<String, String> map = new HashMap<>();
if (code == null || code.equals("error")) {
map.put("success", "false");
return map;
}
//code换token
Map<String, String> token = token(code, app);
return token;
}
}