OAuth 2.0(四):手把手带你写代码接入 OAuth 2.0 授权服务

在这里插入图片描述

今天我们开始落地写代码,基于橘长之前接入过农业银行的授权,今天首先作为第三方服务来和大家分享「 手把手接入 OAuth 2.0 授权服务 」。

一、业务背景

近期团队帮银行做了一个互动营销活动,活动入口在行方的 App 上,当用户在行方 App 点击活动 banner 页跳转活动的时候参与。

在进活动之前作为业务方自然需要知道参与活动的人是谁,如何给它构建登录态。

这就是为什么橘长这边需要接入 行方 OAuth 2.0 组件的原因,本质就是获取 客户信息,回到活动业务形成登录态,进而可以参与活动。

使用的是 OAuth 2.0 中最完备的 授权许可机制 的这种接入方式,服务端发起型授权,为了方便展示,大部分采用了硬编码。

二、作为第三方软件需要做什么

1、静态注册

也可以称之为备案(注册信息) ,需要在 行方 的开放平台的管理态申请 OAuth 2.0 接入客户端,对于行方来说,确保第三方软件是可信的。

准备信息:第三方应用服务端 ip 地址、第三方应用回调通知地址、申请权限等。

String ip = "xxx.xx.xx.xx";
String callBackUrl = "https://xxx.xxx.xx/oauth/login/success"; 

等授权服务的后台人员处理之后会颁发相关配置,用来表示唯一标识 第三方软件 的相关配置。

如下是一个简单示例模板:

String appid = "appid_001";
String appSecret = "appSecret_001";
String scope = "user_info"; 

2、引导用户授权

1)第一步:用户访问第三方软件,判断凭据

如果没有携带 JWT 或 已过期,服务端响应 Http 状态码 401 给前端,前端会请求服务端的发起授权接口。

// 比方说访问活动首页接口
curl https://xxx.xx.xx/api/activity/act-01/index;

// 服务端响应 401
{"message": "用户会话已过期,请重新登录!","statusCode": 401
} 

2)第二步:客户端收到 401,请求授权接口

第三方软件后端会和授权服务交互,获取授权页地址,然后 302 跳转引导用户到授权页。

  • controller 层
@Slf4j
@RequestMapping("/oauth")
@RestController
public class OAuthController {@GetMapping("/login")public RedirectView login(final String redirect) {String sceneId = IdUtil.simpleUUID();JSONObject sceneInfo = JSONUtil.createObj().putOnce(OAuthConstant.REDIRECT_FOR_FRONT, redirect);// 缓存前端通知地址cache.set(OAuthConstant.KEY_PRE_OAUTH_SCENE + sceneId, sceneInfo, 300);String oauthUrl = oAuthService.buildOauthUrl(sceneId, callbackUrl);log.info("[发起 xxx 行方 OAuth 授权] 构建OAuth url为:[{}]", oauthUrl);// 302 跳转return new RedirectView(oauthUrl);}
} 

日志打印:

xxxx [发起授权] 构建的授权地址为:[http://xxx/oauth/authorize?client_id=xxx&redirect_uri=http%3A%2F%2Fxxx%2F%2Foauth%2Flogin%2Fsuccess&state=d8cb3943cd3a45818711fa4f6a8820e9&scope=custid%2Cphone&response_type=code] 
  • service 实现
@Override
public String buildOauthUrl(final String sceneId) {// 回调地址String callbackUrl = applicationConfig.getAppUrl() + "/oauth/login/success";// 带参String notifyUrl = UrlBuilder.of(callbackUrl, CharsetUtil.CHARSET_UTF_8).addQuery("sceneId", sceneId).build(); return xxxOAuthService.buildOauthUrl(notifyUrl);
} 

service 这层根据标准 OAuth 2.0 的要求更合理做法是 请求授权服务获取,这里授权服务设计有点不合理,后续调整。

3)第三步:授权服务回调通知,分发临时授权码

@RequestMapping("/login/success")
public RedirectView loginSuccess(final String sceneId,  final String code) { log.info("[xxx 行方授权回调通知] sceneId:[{}], code:[{}]", sceneId, code);// 后续业务操作
} 

日志打印:

xxxx [oauth 服务]-回调,接收到数据为:code:[xxx], state:[xxx] 

4)第四步:第三方服务通过 code 换取 token

private String getToken(final String clientId, final String clientSecret, final String code) {JSONObject tokenJson = this.getTokenFromOAuthServer(clientId, clientSecret, code, redirectUri);String accessToken = tokenJson.getStr("access_token");if (!JSONUtil.isNull(tokenJson) && StrUtil.isNotEmpty(accessToken)) {return accessToken;}throw new RuntimeException("token获取异常!");
}

/**
 * 从 授权服务 获取 token
 *
 * @author huangyin
 */
private JSONObject getTokenFromOAuthServer(final String clientId,  final String clientSecret,  final String code,  final String redirectUri) { // 请求资源地址 String requertUrl = oauthServerConfig.getBaseUrl() + "/oauth/token";// 构建请求参数Map<String, Object> formMap = new HashMap<>(5);// 授权码许可模式formMap.put("grant_type", "authorization_code");formMap.put("client_id", clientId);formMap.put("client_secret", clientSecret);formMap.put("code", code);// http 请求JSONObject response = this.doPostFormData(requertUrl, formMap);log.info("[从授权服务获取 token] 结果为:[{}]", response);return response;
}

/**
 * 抽离 post 请求方法,form-data 传参
 *
 * @author huangyin
 */
private JSONObject doPostFormData(final String sourceUrl, final Map<String, Object> formArgs) {try {// 采用 开源工具 hutoolString response = HttpRequest.post(sourceUrl).form(formArgs).timeout(3000).execute().body();log.info("[从授权服务post form请求] 请求地址:[{}],请求参数:[{}],原始响应:[{}]", sourceUrl, formArgs, response);if (JSONUtil.isJson(response)) {// 依据授权服务 api response 定义做结果处理JSONObject responseJson = JSONUtil.parseObj(response);String code = responseJson.getStr("code");JSONObject data = responseJson.getJSONObject("data");if ("0000".equals(code) && !JSONUtil.isNull(data)) {return data;}}} catch (Exception e) {log.error("[从授权服务 post form 请求] 异常,请求地址:[{}],参数:[{}],异常信息:[{}]", sourceUrl, formArgs, e.getMessage());}throw new RuntimeException("授权服务异常!");
} 

打印日志:

xxxx [授权服务 code 获取 token] 结果为:[{"access_token":"xxx","expires_in":7200}] 

5)第五步:拿到 凭据,访问业务接口

当用授权码换取到 凭据 之后,通过凭据去获取用户在受保护资源服务的数据,比方说获取用户信息。

public String getUserInfoFromOAuthServer(String token) {String sourceUrl = oauthServerConfig.getBaseUrl() + "/oauth/userInfo";try {// header 头部方式提交 凭据String response = HttpRequest.post(sourceUrl).header("Authorization", "Bearer " + accessToken).timeout(3000).execute().body();log.info("[从授权服务 post 请求获取用户信息] 请求地址:[{}],原始响应:[{}]", sourceUrl, response);if (JSONUtil.isJson(response)) {// 依据授权服务 api response 定义做结果处理JSONObject responseJson = JSONUtil.parseObj(response);String rtCode = responseJson.getStr("code");String data = responseJson.getStr("data");if ("0000".equals(rtCode) && StrUtil.isNotEmpty(data)) {return data;}}} catch (Exception e) {log.error("[从授权服务 post 请求获取用户信息] 异常,请求地址:[{}],异常信息:[{}]", sourceUrl, e.getMessage());}throw new RuntimeException("授权服务异常!");
} 

打印日志:

xxxx [从授权服务换取用户信息] 解密出来的用户信息为:[{"openid":"xxx","headImg":"xxx"}] 

6)第六步:用户信息入库,分发业务 code 给前端

拿到用户信息,写入活动服务的业务表中,然后通知前端说授权完成啦,颁发活动业务的 临时码(code)给客户端,便于客户端来换取活动业务的 JWT。

  • 用户信息入库
public RedirectView loginSuccess(String ...) {// 拷贝OauthUser oauthUser = BeanUtil.copyProperties(userInfoDto, OauthUser.class);oauthUserService.createOrUpdate(oauthUser);// 通知前端return this.redirectFrontEndUrl(state, userInfoDto);
} 
  • 服务端通知前端
private RedirectView redirectFrontEndUrl(final String sceneId,  final UserInfoDto userInfoDto) {// 生成 业务 codeString businessCode = IdUtil.simpleUUID();try {// 反查拿到前端通知地址cache.set(SmallBeanOauthConstant.SMALL_KEY_PRE_USER_INFO + businessCode, userInfoDto, 300);JSONObject sceneInfo = JSONUtil.parseObj(cache.get(SmallBeanOauthConstant.SMALL_KEY_PRE_OAUTH_SCENE + sceneId));String redirectFrontEndUrl = sceneInfo.getStr("redirectFrontEndUrl");if (StrUtil.isEmpty(redirectFrontEndUrl)) {log.warn("授权:分发业务code给前端地址为空!");// TODO 这块需要设置默认值,或者是响应前端401,重跳授权} String notifyUrl = UrlBuilder.of(redirectFrontEndUrl, StandardCharsets.UTF_8).addQuery("code", businessCode).build();// 通知前端return new RedirectView(notifyUrl);} catch (Exception e) {log.error("[OAuth 颁发 code 给客户端异常] 授权id:[{}],异常信息:[{}], [{}]", sceneId, e.getMessage(), e.getCause());}throw new RuntimeException("授权失败,请稍后重试!");
} 

7)第七步:客户端通过 code 换取 业务 jwt

/**
 * 业务 code to jwt:构建登录态
 *
 * @param codeToJwtDto 分发的业务 code
 * @return ResponseEntity
 */
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody CodeToJwtDto codeToJwtDto) {ValidatorUtil.validateEntity(codeToJwtDto);String code = codeToJwtDto.getCode();Object cacheUserInfo = cache.get(OAuthConstant.USER_INFO_PREFIX + code);if (null == cacheUserInfo) {throw new ParamException("code非法!", HttpStatus.UNPROCESSABLE_ENTITY.value());}// 删除掉 codecache.delete(OAuthConstant.USER_INFO_PREFIX + code);// 一系列其他业务处理等,然后生成 JWT Map<String, Object> tokenMap = this.buildJwt(activityUser);return ResponseEntity.status(HttpStatus.CREATED).body(tokenMap);
}

private Map<String, Object> buildJwt(ActivityUser activityUser) {// 构建 jwt 需要相关参数Map<String, Object> claims = new HashMap<>(2);claims.put("authId", activityUser.getId());claims.put("authRole", "user");String token = JwtUtil.generateToken(claims, applicationConfig.getExpiration(), applicationConfig.getTokenSigningKey());// 构建响应前端 token 信息Map<String, Object> tokenMap = new HashMap<>(3);tokenMap.put("accessToken", token);tokenMap.put("tokenType", "Bearer");tokenMap.put("expiresIn", applicationConfig.getExpiration());return tokenMap;
} 

获取到的 JWT:

{
	accessToken=header.payload.signature, 
	tokenType=Bearer, 
	expiresIn=86400
} 

三、总结

今天橘长一步一步带着大家写代码手把手接入 OAuth 2.0 授权服务,大家需要记住几点:

1、关注 授权服务 的官方文档,开放平台接入文档是一个很重要的凭据。

2、第三方软件接入授权尽量采用 服务端发起型 授权,使用 授权码许可机制,因为这更安全、更完备。

3、强烈建议你手把手撸一遍,OAuth 2.0 接入的代码很考验基本功和代码风格,其中用到了 Redis 缓存、Hutool 工具去发起 Http 请求等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用Java代码接入Duros OAuth2.0参数的示例: ```java import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; public class DurosOAuth2Example { private static final String CLIENT_ID = "your_client_id"; private static final String CLIENT_SECRET = "your_client_secret"; private static final String REDIRECT_URI = "your_redirect_uri"; public static void main(String[] args) { try { // Build the authorization URL String authorizationUrl = buildAuthorizationUrl(); // Print the authorization URL to the console and prompt the user to visit it System.out.println("Visit the following URL to authorize the application: " + authorizationUrl); System.out.println("Enter the authorization code:"); // Read the authorization code from the console BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String authorizationCode = reader.readLine().trim(); // Exchange the authorization code for an access token String accessToken = exchangeAuthorizationCodeForAccessToken(authorizationCode); // Use the access token to make API requests String apiUrl = "https://api.duros.io/v1/users/me?"; Map<String, String> parameters = new HashMap<String, String>(); parameters.put("param1", "value1"); parameters.put("param2", "value2"); String response = makeApiRequest(apiUrl, accessToken, parameters); // Print the API response to the console System.out.println("API response: " + response); } catch (IOException e) { e.printStackTrace(); } } private static String buildAuthorizationUrl() throws MalformedURLException { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append("https://duros.io/oauth2/authorize?"); urlBuilder.append("response_type=code"); urlBuilder.append("&client_id=").append(CLIENT_ID); urlBuilder.append("&redirect_uri=").append(URLEncoder.encode(REDIRECT_URI, "UTF-8")); urlBuilder.append("&scope=user"); return urlBuilder.toString(); } private static String exchangeAuthorizationCodeForAccessToken(String authorizationCode) throws IOException { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append("https://duros.io/oauth2/token?"); urlBuilder.append("grant_type=authorization_code"); urlBuilder.append("&code=").append(authorizationCode); urlBuilder.append("&client_id=").append(CLIENT_ID); urlBuilder.append("&client_secret=").append(CLIENT_SECRET); urlBuilder.append("&redirect_uri=").append(URLEncoder.encode(REDIRECT_URI, "UTF-8")); URL url = new URL(urlBuilder.toString()); URLConnection connection = url.openConnection(); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setDoOutput(true); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line = null; StringBuilder responseBuilder = new StringBuilder(); while ((line = reader.readLine()) != null) { responseBuilder.append(line); } String response = responseBuilder.toString(); int accessTokenStartIndex = response.indexOf("access_token") + "access_token".length() + 3; int accessTokenEndIndex = response.indexOf(",", accessTokenStartIndex) - 1; String accessToken = response.substring(accessTokenStartIndex, accessTokenEndIndex); return accessToken; } private static String makeApiRequest(String apiUrl, String accessToken, Map<String, String> parameters) throws IOException { StringBuilder urlBuilder = new StringBuilder(apiUrl); boolean isFirstParameter = true; for (Map.Entry<String, String> parameter : parameters.entrySet()) { if (isFirstParameter) { urlBuilder.append("?"); isFirstParameter = false; } else { urlBuilder.append("&"); } urlBuilder.append(parameter.getKey()).append("=").append(parameter.getValue()); } URL url = new URL(urlBuilder.toString()); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("Authorization", "Bearer " + accessToken); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line = null; StringBuilder responseBuilder = new StringBuilder(); while ((line = reader.readLine()) != null) { responseBuilder.append(line); } String response = responseBuilder.toString(); return response; } } ``` 在上面的代码中,您需要将 `your_client_id`,`your_client_secret` 和 `your_redirect_uri` 替换为您在 Duros 开发者门户中创建的 OAuth2.0 应用程序的客户端 ID、客户端密钥和重定向URI。还可以设置请求的参数,这些参数将作为查询参数附加到 API URL 中。 该示例使用了Java的 `java.net` 包中的类来构建HTTP请求和读取响应。请注意,在实际应用程序中,您可能需要使用更专业的HTTP客户端库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值