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

news2025/1/16 12:37:11

一、业务背景

近期团队帮银行做了一个互动营销活动,活动入口在行方的 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 请求等。

下一篇橘长将给大家带来「 手把手搭建 OAuth 2.0 授权服务 」的解读,感谢你的关注,如果你觉得有所收益,欢迎点赞、转发、评论,感谢认可!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/155948.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node.js--》详解express中的身份认证

目录 Web开发模式 身份认证 Session认证机制 在Express中使用Session认证 JWT认证机制 在Express中使用JWT Web开发模式 目前主流的Web开发模式有两种&#xff1a; 基于服务端渲染的传统Web开发模式 服务端渲染的概念&#xff1a;服务器发送给客户端的HTML页面&#x…

【JavaEE】进入Web开发的世界-CSS

目录 一、CSS的基本规则 二、HTML中如何引入CSS规则&#xff08;html资源如何和css资源产生联系&#xff09; 三、选择器&#xff08;selector&#xff09;规则 3.1各种选择器 3.2普通选择器使用 3.3选择器之间的优先级规则(即覆盖规则) 3.4进一步的选择器规则 3.5伪类…

测试必会 Docker 实战(一):掌握高频命令,夯实内功基础

在 Dokcer 横空出世之前&#xff0c;应用打包一直是大部分研发团队的痛点。在工作中&#xff0c;面对多种服务&#xff0c;多个服务器&#xff0c;以及多种环境&#xff0c;如果还继续用传统的方式打包部署&#xff0c;会浪费大量时间精力。在 Docker 出现后&#xff0c;它以更…

Java设计模式-外观模式Facade

介绍 外观模式&#xff08;Facade&#xff09;&#xff0c;也叫“过程模式&#xff1a;外观模式为子系统中的一组接口提供一个一致的界面&#xff0c;此模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用外观模式通过定义一个一致的接口&#xff0c;用以…

回溯法--n皇后问题

回溯法有两个模板--子集树、排列树&#xff0c;他们有回溯法的共同特点&#xff1a;深度优先搜索&#xff0c;如果一条路走不通&#xff0c;再退回来&#xff08;类似递归&#xff09;问题描述八皇后问题的历史八皇后问题最早是由国际象棋棋手马克斯贝瑟尔&#xff08;Max Bezz…

常用的字符串与内存操作函数(2)

Tips 1. 2. strerror() 1. C语言的库函数在运行的时候&#xff0c;如果发生错误&#xff0c;就会将错误码存在一个变量里面。这个变量就是&#xff1a;errno&#xff08;全局变量&#xff09;。 2. 这个函数的功能在于把错误码转化为对应的错误信息&#xff0c;错误信息…

macOS 13.2 开发者预览版 Beta2(22D5038i)发布

黑果魏叔 1 月 11 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 13.2 开发者预览版 Beta 2 更新&#xff08;内部版本号&#xff1a;22D5038i&#xff09;&#xff0c;本次更新距离上次发布隔了 27 天。macOS Ventura 带来了台前调度、连续互通相机、FaceTime 通话接…

纯CSS输入区域下划线

使用纯CSS实现文本输入的下划线&#xff0c;简洁有效&#xff0c;实现效果&#xff1a; 实现方式 实现方式也很简单&#xff1a; background: linear-gradient(#999 1px, transparent 0) 0 -1px/100% 2em; 例子代码&#xff1a; <!DOCTYPE html> <html lang"e…

【C语言航路】第十站:指针(三)深刻理解指针运算

目录 一、深刻理解指针和数组 1.一维数组 2、字符数组 &#xff08;1&#xff09;字符变量存放到数组中 &#xff08;2&#xff09;字符串存放到字符数组 &#xff08;3&#xff09; 字符串存放到一个指针中 3.二维数组 二、指针与数组经典笔试题 1.题1 2.题2 3.题3…

MCU-51:单片机之红外遥控(外部中断)

目录一、红外遥控简介二、硬件电路三、基本发送与接收四、NEC码五、代码演示5.1 红外遥控5.2 红外遥控电机调速注意&#xff1a;一定要看一、红外遥控简介 红外发射装置 也就是通常我们说的红外遥控器是由键盘电路、红外编码电路、电源电路和红外发射电路组成。红外发射电路的…

爬虫逆向之字体反爬(一)、镀金的天空-字体反爬-1

题目地址&#xff1a;http://www.glidedsky.com/level/crawler-font-puzzle-1 写一下之前处理过的几个字体反爬实战&#xff0c;也是很常见的一种反爬类型&#xff0c;这是第一篇 先来看一下题目 源码拿到的数字&#xff0c;和实际显示在网页的数字&#xff0c;明显不一样的 …

相见恨晚的 IDEA 使用技巧,能让你的代码飞起来

Live Templates 是什么&#xff0c;听上去感觉挺玄乎的。有的同学用过之后觉得简直太好用了&#xff0c;不能说大大提高了开发效率吧&#xff0c;至少也是小小的提高一下&#xff0c;节省了很多敲重复代码的时间。有的同学用过之后说&#xff1a;没什么用&#xff0c;奇技淫巧罢…

C语言_程序环境和预处理

目录 1. 程序的翻译环境 2. 程序的执行环境 3. C语言程序的翻译链接 4. 预编译过程详解 4.1 预定义符号介绍 4.1.1 __FILE__ //进行编译的源文件 4.1.2 __LINE__//文件当前的行号 4.1.3 __DATE__//文件被编译的日期 4.1.4 __TIME__//文件被编译的时间 4.1.5 __STDC__…

基于python的学生信息管理系统

《学生信息管理系统》程序使用说明在IDLE中运行《学生信息管理系统》即可进入如图1所示的系统主界面。在该界面中可以选择要使用功能对应的菜单进行不同的操作。在选择功能菜单时&#xff0c;有两种方法&#xff0c;一种是输入1&#xff0c;另一种是按下键盘上的↑或↓方向键进…

【Javascript】高阶函数,JSON,forEach,map,filter,reduce函数

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录高阶函数箭头函数apply函数JSONfilter函数map函数总结reduce函数find/findIndex函数every/some函…

线缆行业单绞机控制算法(详细图解)

在了解单绞机之前需要大家对收放卷以及排线控制有一定的了解,不清楚的可以参看下面几篇博客,这里不再赘述,受水平和能力所限,文中难免出现错误和不足之处,诚恳的欢迎大家批评和指正。 收放卷行业开环闭环控制算法 PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-CSDN博…

知识难懂到什么程度

爱因斯坦相对论&#xff0c;诺贝尔评委会都看不懂&#xff0c;到底说的是什么1905年&#xff0c;爱因斯坦发布狭义相对论&#xff1b;1916年&#xff0c;发布广义相对论&#xff1b;2年后&#xff0c;英国的汤姆逊教授通过实验验证了广义相对论的正确&#xff0c;同时也把爱因期…

容器适配器中stack queue priority_queue的介绍及模拟实现

文章目录容器适配器的概念deque的介绍及底层结构stack的介绍 stack的模拟实现 queue的介绍 queue的模拟实现 priority_queue的介绍 priority_queue的模拟实现 容器适配器的概念 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验…

清华大佬超全超详细讲解——C++STL看这份教程就够了

2022 年年度编程语言揭榜啦&#xff01;在上个月预想的 C、C、Python 三种候选语言中&#xff0c;C 脱颖而出&#xff0c;成为 TIOBE 2022 年度编程语言的最终获得者&#xff01;新的一波学习热潮要来了。TIOBE 编程语言社区排行榜是编程语言流行趋势的一个指标&#xff0c;每月…

数据分析思维(三)|测试/对比思维

测试/对比思维 1、概念 测试/对比思维可以说在数据分析的工作中随处可见。当我们通过各种手段得到一些结果数据后&#xff0c;如何评价结果的好坏呢&#xff1f;这个时候你可能会想到和标准结果进行比较、和之前的数据进行对照等等方法&#xff0c;这些方法归根结底就是一种测…