基本概念
三个角色
- 服务提供方,用户使用服务提供方来存储受保护的资源,如照片,视频,联系人列表
- 用户,存放在服务提供方的受保护的资源的拥有者
- 客户端,要访问服务提供方资源的第三方应用,通常是网站,如提供照片打印服务的网站。在认证过程之前,客户端要向服务提供者申请客户端标识。
令牌
每一个令牌(Token)授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)
OAuth
OAuth允许用户提供一个令牌给客户端,使得在客户端不用用户名和密码,在特定时间直接能访问服务端的特定资源。其目的在于为 API 访问授权提供一个安全、开放的标准。
基础术语
- Consumer Key:消费方对于服务提供方的身份唯一标识。
- Consumer Secret:用来确认消费方对于 Consumer Key 的拥有关系。
- Request Token:获得用户授权的请求令牌,用于交换 Access Token。
- Access Token:用于获得用户在服务提供方的受保护资源。
- Token Secret:用来确认消费方对于令牌(Request Token 和 Access Token)的拥有关系。
特点
- 安全。OAuth 与别的授权方式不同之处在于:OAuth 的授权不会使消费方(Consumer)触及到用户的帐号信息(如用户名与密码),也是是说,消费方无需使用用户的用户名与密码就可以申请获得该用户资源的授权。
- 开放。任何消费方都可以使用 OAuth 认证服务,任何服务提供方 (Service Provider) 都可以实现自身的 OAuth 认证服务。
- 简单。不管是消费方还是服务提供方,都很容易于理解与使用。
功能
服务提供方需要提供基本的功能: 1.提供用于获取未授权的 Request Token 服务地址,获取用户授权的 Request Token 服务地址,以及使用授权的 Request Token 换取 Access Token 的服务地址。 2.提供基于 Form 的用户认证,以便于用户可以登录服务提供方做出授权。 3.授权的管理,比如用户可以在任何时候撤销已经做出的授权。
消费方需要如下的基本功能: 1.从服务提供方获取 Customer Key/Customer Secret。 2.提供与服务提供方之间基于 HTTP 的通信机制,以换取相关的令牌。
场景解读
问题:系统安全审核反馈对core服务直接通过url访问可以获取数据,属于未授权访问。 分析: 1.对于core侧服务,相当于是服务提供方,园区系统调用core侧服务获取相应的资源属于消费方,令牌如何授权?一种园区开发人员、core侧人员共同当做用户,传递令牌;另一种是园区开发人员去core侧某授权网站获取令牌,有待确认。 2.是否是core侧开启OAuth认证授权服务,通过url无法直接获取资源,用以解决安全审核问题,需要确认。 3.园区开发人员,如何在代码中整合OAuth,在所有调用处能获取到令牌并完成授权服务,需要进一步研究。 4.消费侧需要处理的动作:a.获取令牌 b.调用服务时使用令牌。 5.据了解devops对https和oauth本身都有一些支持,可能需要先去研究一下devops相关的内容。
服务器侧行为本次场景的角色是消费者,如果需要搭建OAuth服务,需要参看链接spring boot如何整合OAuth、spring-security、OAuth2学习之路自行搭建一个开发授权服务。
消费侧行为 1.了解devops如何使用devops获取令牌 2.分析如何在代码中使用url的地方使用令牌
实现
1.开启OAuth服务:core侧编排文件 运行命令 --withOAuth2
2.创建用户:devops使用内建超级账户登录,在用户管理处创建
3.创建用户后获取token,更新token的API,token有效期过期,使用刷新接口对token进行刷新,重新获取
4.使用'token;
curl -i -X GET "http://ip:8080/v5/users" -H "accept: application/json" -H "Authorization: Bearer <token>"
5.代码修改,目前所有的core侧服务使用Feign的方法调用了接口,需要添加Header
a.devops侧
- 超级账户->用户管理->创建用户,使用的权限是external_user (获取到oauthAccessToken、oauthRefreshToken)
- 超级账户->oauth2查看授权(获取到oauthClientId、oauthClientSecret)
b.园区代码侧配置
- properties配置文件
#OAuth认证的API
oauth.getTokenInfoUrl = http://${CORE_HOST:<ip>}:port/api/v1/oauth2/info
oauth.refreshTokenUrl = http://${CORE_HOST:<ip>}:port/api/v1/oauth2/token
oauth.redirectUrl = http://${CORE_HOST:<ip>}:port/api/v1/oauth2/mock_callback
- adapter.config表格添加环境变量key-value
oauthClientId
oauthAccessToken
oauthRefreshToken
oauthClientSecret
c.代码实现
- Feign拦截器
@Configuration
@Slf4j
public class FeignAuthRequestInterceptor {
@Resource
private FeignAuthRequestService feignAuthRequestService;
/**
* OAuth信息Header的值的前缀
*/
private static final String OAUTH_VALUE_PREFIX = "Authorization";
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// request设置OAuth请求头
template.header(OAUTH_VALUE_PREFIX, "Bearer " + feignAuthRequestService.getToken());
}
};
}
}
- 获取token,以及考虑到token过期重新生成token
@Service
@Slf4j
public class FeignAuthRequestService {
@Value("${oauth.getTokenInfoUrl}")
private String getTokenInfoUrl;
@Value("${oauth.refreshTokenUrl}")
private String refreshTokenUrl;
@Value("${oauth.redirectUrl}")
private String redirectUrl;
@RoutingInjected
private ActionService actionService;
public String getToken() {
try {
// getTokenInfo 判断过期时间是否大于0
String accessToken = actionService.getConfigValue(ConfigEnum.OAUTH_ACCESS_TOKEN.getMsg());
if (getExpiresTime(accessToken) > 0) {
return accessToken;
} else {
return getNewToken();
}
} catch (Exception e) {
log.info("getToken error:", e);
if (e instanceof BusinessException) {
throw new BusinessException(((BusinessException) e).getErrorCode(), ((BusinessException) e).getErrorMsg());
} else {
throw new BusinessException(BusinessExceptionEnum.WRONG_PARAMETER, "token获取失败");
}
}
}
/**
* token过期获取新的token
* 并把对应环境变量进行修改
*
* @return
*/
private String getNewToken() throws IOException {
String clientId = actionService.getConfigValue(ConfigEnum.OAUTH_CLIENT_ID.getMsg());
String clientSecret = actionService.getConfigValue(ConfigEnum.OAUTH_CLIENT_SECRET.getMsg());
String refreshToken = actionService.getConfigValue(ConfigEnum.OAUTH_REFRESH_TOKEN.getMsg());
String param = "?client_id=" + clientId + "&client_secret=" +
clientSecret + "&grant_type=refresh_token&redirect_uri=" + "&refresh_token=" + refreshToken;
ResponseResult response = HttpClient4Util.post(refreshTokenUrl + param, "", null);
JSONObject jsonObject = JSON.parseObject(response.getData());
String accessNewToken = jsonObject.getString("access_token");
String refreshNewToken = jsonObject.getString("refresh_token");
actionService.updateConfigValue(ConfigEnum.OAUTH_ACCESS_TOKEN.getMsg(), accessNewToken);
actionService.updateConfigValue(ConfigEnum.OAUTH_REFRESH_TOKEN.getMsg(), refreshNewToken);
return accessNewToken;
}
/**
* 获取OAuth的获取时间
*
* @param token
* @return
* @throws IOException
*/
private Integer getExpiresTime(String token) throws IOException {
ResponseResult response = HttpClient4Util.post(getTokenInfoUrl + "?code=" + token, "", null);
JSONObject jsonObject = JSON.parseObject(response.getData());
return jsonObject.getInteger("expires_in");
}
}
- 调用core侧,FeignClinet修改
@FeignClient(name="action",url = "${core.apiServiceUrlV5}", configuration = FeignAuthRequestInterceptor.class)