03 | 授权服务:授权码和访问令牌的颁发流程是怎样的?
授权服务的工作过程
小兔软件需要去到京东的平台那里”备案“注册,京东商家开放平台就会给小兔软件 app_id 和 app_secret 等信息,以方便后面授权时的各种身份校验,也会有请求资源的访问范围,比如只能访问3个月内的订单数据。
代码:
Map<String,String> appMap = new HashMap<String, String>();//模拟第三方软件注册之后的数据库存储
appMap.put("app_id","APPID_RABBIT");
appMap.put("app_secret","APPSECRET_RABBIT");
appMap.put("redirect_uri","http://localhost:8080/AppServlet-ch03");
appMap.put("scope","nickname address pic");
授权服务的工作,可以划分为 颁发授权码 code、颁发访问令牌 access_token。
过程一:颁发授权码 code
第一步,验证基本信息
授权服务需要对回调地址做基本的校验,这个回调地址在client注册时就已经提供,因此可以检查是否被替换。(因为有些非法的请求,可能会伪造回调地址,钓鱼之类的)
if(!appMap.get("redirect_uri").equals(redirectUri)){
//回调地址不存在
}
第二步,验证权限范围(第一次)。
第三方软件可以获得你的昵称、头像、性别、地理位置等. 比如是否授予小兔获取 3 个月以前的订单的访问权限。
第一次权限校验
String scope = request.getParameter("scope");
if(!checkScope(scope)){
//超出注册的权限范围
}
第三步,生成授权请求页面。
第四步,验证权限范围(第二次)。
第二次校验主要是用户选择的权限范围,我们也需要校验,判断是否合理。
String[] rscope =request.getParameterValues("rscope");
if(!checkScope(rscope)){
//超出注册的权限范围
}
第五步,处理授权请求,生成授权码 code。
当小明同意授权之后,授权服务会校验响应类型 response_type 的值
String responseType = request.getParameter("response_type");
if("code".equals(responseType)){
}
在授权服务中,需要将生成的授权码 code 值与 app_id、user 进行关系映射,比如小明给小兔软件进行的授权
String code = generateCode(appId,"USERTEST");//模拟登录用户为USERTEST
private String generateCode(String appId,String user) {
...
String code = strb.toString();
codeMap.put(code,appId+"|"+user+"|"+System.currentTimeMillis());
return code;
}
OAuth 2.0 规范建议授权码 code 值有效期为 10 分钟,并且一个授权码 code 只能被使用一次。授权服务还需要将生成的授权码 code 跟已经授权的权限范围 rscope 进行绑定并存储
Map<String,String[]> codeScopeMap = new HashMap<String, String[]>();
codeScopeMap.put(code,rscope);//授权范围与授权码做绑定
第六步,重定向至第三方软件。
生成授权码 code 值之后,授权服务需要将该 code 值告知第三方软件小兔.这个是前端的操作,重定向
Map<String, String> params = new HashMap<String, String>();
params.put("code",code);
String toAppUrl = URLParamsUtil.appendParams(redirectUri,params);//构造第三方软件的回调地址,并重定向到该地址
response.sendRedirect(toAppUrl);//授权码流程的“第二次”重定向
过程二:颁发访问令牌 access_token
当小兔拿着授权码 code 来请求的时候,授权服务需要为之生成最终的请求访问令牌
第一步,验证第三方软件是否存在。
接收到的 grant_type 的类型为 authorization_code。
String grantType = request.getParameter("grant_type");
if("authorization_code".equals(grantType)){
}
由于颁发访问令牌是通过后端通信完成的,所以这里除了要校验 app_id 外,还要校验 app_secret。
if(!appMap.get("app_id").equals(appId)){
//app_id不存在
}
if(!appMap.get("app_secret").equals(appSecret)){
//app_secret不合法
}
第二步,验证授权码 code 值是否合法。
授权服务在颁发授权码 code 的阶段已经将 code 值存储了起来.所以我们只需要校验
String code = request.getParameter("code");
if(!isExistCode(code)){//验证code值
//code不存在
return;
}
codeMap.remove(code);//授权码一旦被使用,须立即作废
确认过授权码 code 值有效以后,应该立刻从存储中删除当前的 code 值,以防止第三方软件恶意使用一个失窃的授权码 code 值来请求授权服务。
第三步,生成访问令牌 access_token 值。
access_token 的值原则:唯一性、不连续性、不可猜性
个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权。授权服务还需要将授权范围跟访问令牌 access_token 做绑定。
Map<String,String[]> tokenScopeMap = new HashMap<String, String[]>();
String accessToken = generateAccessToken(appId,"USERTEST");//生成访问令牌access_token的值
tokenScopeMap.put(accessToken,codeScopeMap.get(code));//授权范围与访问令牌绑定
//生成访问令牌的方法
private String generateAccessToken(String appId,String user){
String accessToken = UUID.randomUUID().toString();
String expires_in = "1";//1天时间过期
tokenMap.put(accessToken,appId+"|"+user+"|"+System.currentTimeMillis()+"|"+expires_in);
return accessToken;
}
信息通过结构化的处理放入令牌本身。我们将包含了一些信息的令牌,称为结构化令牌,简称 JWT。
刷新令牌
比如令牌10分钟之后过期,总不能让用户10分钟再授权一次吧,所以就需要续约,刷新。
其实,颁发刷新令牌和颁发访问令牌是一起实现的,都是在过程二的步骤三生成访问令牌 access_token 中生成的。
Map<String,String> refreshTokenMap = new HashMap<String, String>();
String refreshToken = generateRefreshToken(appId,"USERTEST");//生成刷新令牌refresh_token的值
private String generateRefreshToken(String appId,String user){
String refreshToken = UUID.randomUUID().toString();
refreshTokenMap.put(refreshToken,appId+"|"+user+"|"+System.currentTimeMillis());
return refreshToken;
}
访问令牌失效了,就使用刷新令牌。
使用刷新令牌
在 OAuth 2.0 规范中,刷新令牌是一种特殊的授权许可类型,是嵌入在授权码许可类型下的一种特殊许可类型。
第一步,接收刷新令牌请求,验证基本信息。
此时请求中的 grant_type 值为 refresh_token。
String grantType = request.getParameter("grant_type");
if("refresh_token".equals(grantType)){
}
使用刷新令牌也是需要校验合法性,
String grantType = request.getParameter("grant_type");
if("refresh_token".equals(grantType)){
//该refresh_token值不存在
}
if(!appStr.startsWith(appId+"|"+"USERTEST")){
//该refresh_token值不是颁发给该第三方软件的
}
需要注意,一个刷新令牌被使用以后,授权服务需要将其废弃,并重新颁发一个刷新令牌
第二步,重新生成访问令牌
总结
- 授权服务的核心就是,先颁发授权码 code 值,再颁发访问令牌 access_token 值。
- 在颁发访问令牌的同时还会颁发刷新令牌 refresh_token 值,这种机制可以在无须用户参与的情况下用于生成新的访问令牌。
- 授权还要有授权范围,不能让第三方软件获得比注册时权限范围还大的授权,也不能获得超出了用户授权的权限范围,始终确保最小权限安全原则。
若access_token未超时,那么进行refresh_token有两种方式,
(1)不会改变access_token,但超时时间会刷新,相当于续期access_token
(2)更新access_token的值,我们建议【统一更新access_token的值】。(不可以让“token一个更长的有效期”存在的)
原文