diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java index 77240d0a..ffa9fe0c 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/java/com/pj/oauth2/SaOAuthClientController.java @@ -23,7 +23,7 @@ public class SaOAuthClientController { // 相关参数配置 private final String clientId = "1001"; // 应用id private final String clientSecret = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥 - private final String serverUrl = "http://sa-oauth-server.com:8001"; // 服务端接口 + private final String serverUrl = "http://sa-oauth-server.com:8000"; // 服务端接口 // 进入首页 @RequestMapping("/") diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html index 711f1a97..79c33db3 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html @@ -42,33 +42,33 @@

模式一:授权码(Authorization Code)

授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid

- + 当请求链接不包含 scope 权限,或请求的 scope 近期已授权时,将无需用户手动确认,做到静默授权 - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/ + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/ - + 当请求链接包含具体的 scope 权限时,将需要用户手动确认,此时 OAuth-Server 会返回更多的数据 - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userid,userinfo + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userid,userinfo 我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废 - http://sa-oauth-server.com:8001/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value} + http://sa-oauth-server.com:8000/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value} 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) - http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value} + http://sa-oauth-server.com:8000/oauth2/userinfo?access_token={value}

模式二:隐藏式(Implicit)

- + 越过授权码的步骤,直接返回token到前端页面( 格式:http//:domain.com#token=xxxx-xxxx ) - http://sa-oauth-server.com:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo

模式三:密码式(Password)

@@ -76,7 +76,7 @@ 账号: 密码: - http://sa-oauth-server.com:8001/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value} + http://sa-oauth-server.com:8000/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value}

模式四:凭证式(Client Credentials)

@@ -87,7 +87,7 @@ 保证了服务的高可用

- http://sa-oauth-server.com:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value} + http://sa-oauth-server.com:8000/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}

更多资料请参考 Sa-Token 官方文档地址: diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/current/GlobalExceptionHandler.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/current/GlobalExceptionHandler.java new file mode 100644 index 00000000..f251e478 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/current/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.pj.current; + +import cn.dev33.satoken.util.SaResult; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理 + * @author click33 + * + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + // 全局异常拦截 + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } + +} diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java index e8358a24..72948364 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2DataLoaderImpl.java @@ -12,22 +12,22 @@ import org.springframework.stereotype.Component; @Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { - // 根据 client_id 获取 Client 信息 + // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 if("1001".equals(clientId)) { return new SaClientModel() - .setClientId("1001") - .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") - .addAllowUrls("*") - .addContractScopes("openid", "userid", "userinfo") - .setIsAutoMode(true); + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowUrls("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .setIsAutoMode(true); // 是否自动判断开放的授权模式 } return null; } - // 根据ClientId 和 LoginId 获取openid + // 根据 clientId 和 loginId 获取 openid @Override public String getOpenid(String clientId, Object loginId) { // 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java index 03fbbf60..44ddce9d 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java @@ -33,12 +33,13 @@ public class SaOAuth2ServerController { // Sa-OAuth2 定制化配置 @Autowired - public void setSaOAuth2Config(SaOAuth2Config cfg) { + public void configOAuth2Server(SaOAuth2Config cfg) { // 未登录的视图 cfg.notLoginView = ()->{ return new ModelAndView("login.html"); }; - // 登录处理函数 + + // 登录处理函数 cfg.doLoginHandle = (name, pwd) -> { if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); @@ -46,11 +47,12 @@ public class SaOAuth2ServerController { } return SaResult.error("账号名或密码错误"); }; - // 授权确认视图 - cfg.confirmView = (clientId, scope)->{ + + // 授权确认视图 + cfg.confirmView = (clientId, scopes)->{ Map map = new HashMap<>(); map.put("clientId", clientId); - map.put("scope", scope); + map.put("scope", scopes); return new ModelAndView("confirm.html", map); }; } diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml index f745a8d9..0d263504 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8001 + port: 8000 # sa-token配置 sa-token: diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md index e07e2cc9..e15f364c 100644 --- a/sa-token-doc/_sidebar.md +++ b/sa-token-doc/_sidebar.md @@ -37,7 +37,7 @@ - **单点登录** - [单点登录简述](/sso/readme) - [搭建统一认证中心:SSO-Server](/sso/sso-server) - - [SSO-Server 认证中心开放接口](/sso/sso-apidoc) + - [SSO-Server 认证中心开放 API 接口](/sso/sso-apidoc) - [SSO模式一 共享Cookie同步会话](/sso/sso-type1) - [SSO模式二 URL重定向传播会话](/sso/sso-type2) - [SSO模式三 Http请求获取会话](/sso/sso-type3) @@ -54,9 +54,18 @@ - **OAuth2.0** - [OAuth2.0简述](/oauth2/readme) - [OAuth2-Server搭建](/oauth2/oauth2-server) - - [OAuth2-Server端-API列表](/oauth2/oauth2-api) - - [OAuth2-二次开发说明](/oauth2/oauth2-dev) + - [OAuth2-Server端开放 API 接口](/oauth2/oauth2-apidoc) + - [配置 client 域名校验 ](/oauth2/oauth2-check-domain) + - [定制化登录页面与授权页面](/oauth2/oauth2-custom-login) + - [拆分式路由](/oauth2/3) + - [前后端分离模式整合方案](/oauth2/4) + - [平台中心模式开发](/oauth2/5) + - [自定义 Scope 权限以处理器](/oauth2/6) + - [为 Scope 划分等级](/oauth2/7) - [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking) + - [OAuth2 代码 API 参考](/oauth2/oauth2-dev) + - [常见问题说明](/oauth2/8) + - - **微服务** - [分布式Session会话](/micro/dcs-session) diff --git a/sa-token-doc/fun/sso-vs-oauth2.md b/sa-token-doc/fun/sso-vs-oauth2.md index a3cd5764..e8dd8b0f 100644 --- a/sa-token-doc/fun/sso-vs-oauth2.md +++ b/sa-token-doc/fun/sso-vs-oauth2.md @@ -14,6 +14,7 @@ QQ群库经常有小伙伴提问:项目需要搭建统一认证中心,是用 | 自有系统授权管理 | 支持度高 | 支持度低 | | Client级的权限校验 | 不支持 | 支持度高 | | 集成简易度 | 比较简单 | 难度中等 | +| 适合项目 | 企业内部项目整合 | 企业搭建统一认证授权平台,对外开放服务 | 注:以上仅为在 Sa-Token 中两种技术的差异度比较,不同框架的实现可能略有差异,但整体思想是一致的。 diff --git a/sa-token-doc/oauth2/oauth2-api.md b/sa-token-doc/oauth2/oauth2-api.md deleted file mode 100644 index a214c271..00000000 --- a/sa-token-doc/oauth2/oauth2-api.md +++ /dev/null @@ -1,265 +0,0 @@ -# Sa-Token-OAuth2 Server端 API列表 -基于官方仓库的搭建示例,`OAuth2-Server`端会暴露出以下API,`OAuth2-Client`端可据此文档进行对接 - ---- - -## 1、模式一:授权码(Authorization Code) - -### 1.1、获取授权码 - -根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符) -``` url -http://sa-oauth-server.com:8001/oauth2/authorize - ?response_type=code - &client_id={value} - &redirect_uri={value} - &scope={value} - &state={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| response_type | 是 | 返回类型,这里请填写:code | -| client_id | 是 | 应用id | -| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | -| scope | 否 | 具体请求的权限,多个用逗号隔开 | -| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加 | - -注意点: -1. 如果用户在Server端尚未登录:会被转发到登录视图,你可以参照文档或官方示例自定义登录页面 -2. 如果scope参数为空,或者请求的权限用户近期已确认过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放code授权码 - -用户确认授权之后,会被重定向至`redirect_uri`,并追加code参数与state参数,形如: -``` url -redirect_uri?code={code}&state={state} -``` - -Code授权码具有以下特点: -1. 每次授权产生的Code码都不一样 -2. Code码用完即废,不能二次使用 -3. 一个Code的有效期默认为五分钟,超时自动作废 -4. 每次授权产生新Code码,会导致旧Code码立即作废,即使旧Code码尚未使用 - - -### 1.2、根据授权码获取Access-Token -获得Code码后,我们可以通过以下接口,获取到用户的`Access-Token`、`Refresh-Token`、`openid`等关键信息 - -``` url -http://sa-oauth-server.com:8001/oauth2/token - ?grant_type=authorization_code - &client_id={value} - &client_secret={value} - &code={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 授权类型,这里请填写:authorization_code | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| code | 是 | 步骤1.1中获取到的授权码 | - -接口返回示例: - -``` js -{ - "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 - "msg": "ok", - "data": { - "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token值 - "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token值 - "expires_in": 7199, // Access-Token剩余有效期,单位秒 - "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 - "client_id": "1001", // 应用id - "scope": "userinfo", // 此令牌包含的权限 - "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" // openid - } -} -``` - - -### 1.3、根据 Refresh-Token 刷新 Access-Token (如果需要的话) -Access-Token的有效期较短,如果每次过期都需要重新授权的话,会比较影响用户体验,因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token` - -``` url -http://sa-oauth-server.com:8001/oauth2/refresh - ?grant_type=refresh_token - &client_id={value} - &client_secret={value} - &refresh_token={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 授权类型,这里请填写:refresh_token | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| refresh_token | 是 | 步骤1.2中获取到的`Refresh-Token`值 | - -接口返回值同章节1.2,此处不再赘述 - - -### 1.4、回收 Access-Token (如果需要的话) -在Access-Token过期前主动将其回收 - -``` url -http://sa-oauth-server.com:8001/oauth2/revoke - ?client_id={value} - &client_secret={value} - &access_token={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| access_token | 是 | 步骤1.2中获取到的`Access-Token`值 | - -返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": null -} -``` - - -### 1.5、根据 Access-Token 获取相应用户的账号信息 -注:此接口为官方仓库模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 - -``` url -http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value} -``` - -返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": { - "nickname": "shengzhang_", // 账号昵称 - "avatar": "http://xxx.com/1.jpg", // 头像地址 - "age": "18", // 年龄 - "sex": "男", // 性别 - "address": "山东省 青岛市 城阳区" // 所在城市 - } -} -``` - - -## 2、模式二:隐藏式(Implicit) - -根据以下格式构建URL,引导用户访问: -``` url -http://sa-oauth-server.com:8001/oauth2/authorize - ?response_type=token - &client_id={value} - &redirect_uri={value} - &scope={value} - $state={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| response_type | 是 | 返回类型,这里请填写:token | -| client_id | 是 | 应用id | -| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | -| scope | 否 | 具体请求的权限,多个用逗号隔开 | -| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加 | - -此模式会越过授权码的步骤,直接返回Access-Token到前端页面,形如: -``` url -redirect_uri#token=xxxx-xxxx-xxxx-xxxx -``` - -## 3、模式三:密码式(Password) -首先在Client端构建表单,让用户输入Server端的账号和密码,然后在Client端访问接口 -``` url -http://sa-oauth-server.com:8001/oauth2/token - ?grant_type=password - &client_id={value} - &client_secret={value} - &username={value} - &password={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 返回类型,这里请填写:password| -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| username | 是 | 用户的Server端账号 | -| password | 是 | 用户的Server端密码 | -| scope | 否 | 具体请求的权限,多个用逗号隔开 | - -接口返回示例: - -``` js -{ - "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 - "msg": "ok", - "data": { - "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token值 - "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token值 - "expires_in": 7199, // Access-Token剩余有效期,单位秒 - "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 - "client_id": "1001", // 应用id - "scope": "", // 此令牌包含的权限 - "openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__" // openid - } -} -``` - - -## 4、模式四:凭证式(Client Credentials) -以上三种模式获取的都是用户的 `Access-Token`,代表用户对第三方应用的授权, -在OAuth2.0中还有一种针对 Client级别的授权, 即:`Client-Token`,代表应用自身的资源授权 - -在Client端的后台访问以下接口: - -``` url -http://sa-oauth-server.com:8001/oauth2/client_token - ?grant_type=client_credentials - &client_id={value} - &client_secret={value} -``` - -参数详解: - -| 参数 | 是否必填 | 说明 | -| :-------- | :-------- | :-------- | -| grant_type | 是 | 返回类型,这里请填写:client_credentials| -| client_id | 是 | 应用id | -| client_secret | 是 | 应用秘钥 | -| scope | 否 | 申请权限 | - -接口返回值样例: -``` js -{ - "code": 200, - "msg": "ok", - "data": { - "client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值 - "expires_in": 7199, // Token剩余有效时间,单位秒 - "client_id": "1001", // 应用id - "scope": null // 包含权限 - } -} -``` - -注:`Client-Token`具有延迟作废特性,即:在每次获取最新`Client-Token`的时候,旧`Client-Token`不会立即过期,而是作为`Past-Token`再次储存起来, -资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用 - diff --git a/sa-token-doc/oauth2/oauth2-apidoc.md b/sa-token-doc/oauth2/oauth2-apidoc.md new file mode 100644 index 00000000..eedcdf04 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-apidoc.md @@ -0,0 +1,266 @@ +# Sa-Token-OAuth2 Server端 API列表 +基于官方仓库的搭建示例,`OAuth2-Server`端会暴露出以下API,`OAuth2-Client`端可据此文档进行对接 + +--- + +## 1、模式一:授权码(Authorization Code) + +### 1.1、获取授权码 + +根据以下格式构建URL,引导用户访问 (复制时请注意删减掉相应空格和换行符) +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=code + &client_id={client_id} + &redirect_uri={redirect_uri} + &scope={scope} + &state={state} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| response_type | 是 | 返回类型,这里请填写:`code` | +| client_id | 是 | 应用 id | +| redirect_uri | 是 | 用户确认授权后,重定向的 url 地址 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | +| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | + +注意点: +1. 如果用户在 `OAuth-Server` 端尚未登录:会被转发到登录视图,你可以参照文档或官方示例自定义登录页面。 +2. 如果 `scope` 参数为空,或者请求的 `scope` 用户近期已确认授权过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放 `code` 授权码。 + +用户确认授权之后,会被重定向至`redirect_uri`,并追加 `code` 参数与 `state` 参数,形如: +``` url +redirect_uri?code={code}&state={state} +``` + +`Code` 授权码具有以下特点: +1. 每次授权产生的 `Code` 码都不一样。 +2. `Code` 码用完即废,不能二次使用。 +3. 一个 `Code` 的有效期默认为五分钟,超时自动作废。 +4. 每次授权产生新 `Code` 码,会导致旧 `Code` 码立即作废,即使旧 `Code` 码尚未使用。 + + +### 1.2、根据授权码获取 Access-Token +获得 `Code` 码后,我们可以通过以下接口,获取到用户的 `Access-Token`、`Refresh-Token` 等信息。 + +``` url +http://{host}:{port}/oauth2/token + ?grant_type=authorization_code + &client_id={client_id} + &client_secret={client_secret} + &code={code} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 授权类型,这里请填写:`authorization_code` | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| code | 是 | 步骤 1.1 中获取到的授权码 | + +也可以通过 `Basic Authorization` 方式提交 `client` 信息,格式为在请求 `header` 头添加 `Authorization` 参数: +``` js +header['Authorization'] = base64(`${client_id}:${client_secret}`); +``` + +接口返回示例: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 + "msg": "ok", + "data": null, + "token_type": "bearer", + "access_token": "Gly7mnnXSdCxkOqmOwcA5SbG6ZtPmJVX7ZgSn1pidhRmnenBEgxbWJS8VWxA", // Access-Token值 + "refresh_token": "EuYNwpxdc18MpaZLPyhFeyAyzr2IOWEr4q3QUGgPWqdJujQqvohjQEDJpwOm", // Refresh-Token值 + "expires_in": 7199, // Access-Token剩余有效期,单位秒 + "refresh_expires_in": 2591999, // Refresh-Token剩余有效期,单位秒 + "client_id": "1001", // 应用 id + "scope": "userinfo" // 此令牌包含的权限 +} +``` + + +### 1.3、根据 Refresh-Token 刷新 Access-Token (如果需要的话) +Access-Token的有效期较短,如果每次过期都需要重新授权的话,会比较影响用户体验,因此我们可以在后台通过`Refresh-Token` 刷新 `Access-Token` + +``` url +http://{host}:{port}/oauth2/refresh + ?grant_type=refresh_token + &client_id={client_id} + &client_secret={client_secret} + &refresh_token={refresh_token} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 授权类型,这里请填写:`refresh_token` | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| refresh_token | 是 | 步骤1.2中获取到的 `Refresh-Token` 值 | + +接口返回值同章节1.2,此处不再赘述 + + +### 1.4、回收 Access-Token (如果需要的话) +在A ccess-Token 过期之前主动将其回收 + +``` url +http://{host}:{port}/oauth2/revoke + ?client_id={client_id} + &client_secret={client_secret} + &access_token={access_token} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| access_token | 是 | 步骤1.2中获取到的`Access-Token`值 | + +返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "data": null +} +``` + + +### 1.5、根据 Access-Token 获取相应用户的账号信息 +注:此接口为官方仓库模拟接口,正式项目中大家可以根据此样例,自定义需要的接口及参数 + +``` url +http://{host}:{port}/oauth2/userinfo?access_token={access_token} +``` + +返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "nickname": "shengzhang_", // 账号昵称 + "avatar": "http://xxx.com/1.jpg", // 头像地址 + "age": "18", // 年龄 + "sex": "男", // 性别 + "address": "山东省 青岛市 城阳区" // 所在城市 +} +``` + + +## 2、模式二:隐藏式(Implicit) + +根据以下格式构建URL,引导用户访问: +``` url +http://{host}:{port}/oauth2/authorize + ?response_type=token + &client_id={client_id} + &redirect_uri={redirect_uri} + &scope={scope} + &state={state} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| response_type | 是 | 返回类型,这里请填写:`token` | +| client_id | 是 | 应用 id | +| redirect_uri | 是 | 用户确认授权后,重定向的url地址 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | +| state | 否 | 随机值,此参数会在重定向时追加到url末尾,不填不追加,如果填写则每次填写的值不可以重复 | + +此模式会越过授权码的步骤,直接返回 `Access-Token` 到前端页面,形如: +``` url +redirect_uri#token=xxxx-xxxx-xxxx-xxxx +``` +注意 token 是以 `#` 锚参数的形式拼接到 url 上的。 + + +## 3、模式三:密码式(Password) +首先在Client端构建表单,让用户输入 Server 端的账号和密码,然后在 Client 端访问接口 +``` url +http://{host}:{port}/oauth2/token + ?grant_type=password + &client_id={client_id} + &client_secret={client_secret} + &username={username} + &password={password} + &scope={scope} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 返回类型,这里请填写:`password`| +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| username | 是 | 用户的 `OAuth2-Server` 端账号 | +| password | 是 | 用户的 `OAuth2-Server` 端密码 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | + +接口返回示例: + +``` js +{ + "code": 200, // 200表示请求成功,非200标识请求失败, 以下不再赘述 + "msg": "ok", + "access_token": "7Ngo1Igg6rieWwAmWMe4cxT7j8o46mjyuabuwLETuAoN6JpPzPO2i3PVpEVJ", // Access-Token 值 + "refresh_token": "ZMG7QbuCVtCIn1FAJuDbgEjsoXt5Kqzii9zsPeyahAmoir893ARA4rbmeR66", // Refresh-Token 值 + "expires_in": 7199, // Access-Token 剩余有效期,单位秒 + "refresh_expires_in": 2591999, // Refresh-Token 剩余有效期,单位秒 + "client_id": "1001", // 应用 id + "scope": "", // 此令牌包含的权限 +} +``` + + +## 4、模式四:凭证式(Client Credentials) +以上三种模式获取的都是用户的 `Access-Token`,代表用户对第三方应用的授权, +在OAuth2.0中还有一种针对 Client级别的授权, 即:`Client-Token`,代表应用自身的资源授权 + +在 Client 端的后台访问以下接口: + +``` url +http://{host}:{port}/oauth2/client_token + ?grant_type=client_credentials + &client_id={client_id} + &client_secret={client_secret} + &scope={scope} +``` + +参数详解: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| grant_type | 是 | 返回类型,这里请填写:`client_credentials`| +| client_id | 是 | 应用 id | +| client_secret | 是 | 应用秘钥 | +| scope | 否 | 具体请求的权限,多个用逗号(或空格)隔开 | + +接口返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "client_token": "HmzPtaNuIqGrOdudWLzKJRSfPadN497qEJtanYwE7ZvHQWDy0jeoZJuDIiqO", // Client-Token 值 + "expires_in": 7199, // Token剩余有效时间,单位秒 + "client_id": "1001", // 应用 id + "scope": null // 包含权限 +} +``` + +注:`Client-Token`具有延迟作废特性,即:在每次获取最新`Client-Token`的时候,旧`Client-Token`不会立即过期,而是作为`Past-Token`再次储存起来, +资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”, 保证了服务的高可用。 + diff --git a/sa-token-doc/oauth2/oauth2-check-domain.md b/sa-token-doc/oauth2/oauth2-check-domain.md new file mode 100644 index 00000000..f1cca9e3 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-check-domain.md @@ -0,0 +1,93 @@ +# OAuth2 整合-配置域名校验 + +--- + +### 1、code 劫持攻击 +在前面章节的 OAuth-Server 搭建示例中: + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + if("1001".equals(clientId)) { + return new SaClientModel() + // ... + .addAllowUrls("*") // 所有允许授权的 url + // ... + } + return null; + } + // 其它代码 ... +} +``` + +配置项 `AllowUrls` 意为配置此 `Client` 端所有允许的授权地址,不在此配置项中的 URL 将无法下发 `code` 授权码。 + +为了方便测试,上述代码将其配置为`*`,但是,在生产环境中,此配置项绝对不能配置为 * ,否则会有被 `code` 劫持的风险。 + +假设攻击者根据模仿我们的授权地址,巧妙的构造一个URL: + +> [http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com](http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com) + +当不知情的小红被诱导访问了这个 URL 时,它将被重定向至百度首页。 + +![oauth2-ticket-jc](https://oss.dev33.cn/sa-token/doc/oauth2-new/oauth2-ticket-jc.png 's-w-sh') + +可以看到,代表着用户身份的 code 授权码也显现到了URL之中,借此漏洞,攻击者完全可以构建一个 URL 将小红的 code 授权码自动提交到攻击者自己的服务器,伪造小红身份登录网站。 + + +### 2、防范方法 + +造成此漏洞的直接原因就是我们对此 client 配置了过于宽泛的 `AllowUrls` 允许授权地址,防范的方法也很简单,就是缩小 `AllowUrls` 授权范围。 + +我们将其配置为一个具体的URL: + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + if("1001".equals(clientId)) { + return new SaClientModel() + // ... + .addAllowUrls("http://sa-oauth-client.com:8002/") // 所有允许授权的 url + // ... + } + return null; + } + // 其它代码 ... +} +``` + +再次访问上述链接: + +![oauth2-feifa-rf](https://oss.dev33.cn/sa-token/doc/oauth2-new/oauth2-feifa-rf.png 's-w-sh') + +域名没有通过校验,拒绝授权! + + +### 3、配置安全性参考表 + +| 配置方式 | 举例 | 安全性 | 建议 | +| :-------- | :-------- | :-------- | :-------- | +| 配置为* | `*` | | **禁止在生产环境下使用** | +| 配置到域名 | `http://sa-oauth-client.com:8002/*` | | 不建议在生产环境下使用 | +| 配置到详细地址 | `http://sa-oauth-client.com:8002/xxx/xxx` | | 可以在生产环境下使用 | + + +### 4、其它规则 + +1、AllowUrls 配置的地址不允许出现 `@` 字符。 +*详见源码:[SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) +`checkRightUrl` 方法。* + +2、AllowUrls 配置的地址 `*` 通配符只允许出现在字符串末尾,不允许出现在字符串中间位置。 +*详见源码: [SaOAuth2Template.java](https://gitee.com/dromara/sa-token/blob/master/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java) +`checkAllowUrlListStaticMethod` 方法。* + +参考:[github/issue/529](https://github.com/dromara/Sa-Token/issues/529) +感谢这位 `@m4ra7h0n` 用户反馈的漏洞。 + diff --git a/sa-token-doc/oauth2/oauth2-custom-login.md b/sa-token-doc/oauth2/oauth2-custom-login.md new file mode 100644 index 00000000..25cb0bb1 --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-custom-login.md @@ -0,0 +1,94 @@ +# OAuth2 定制化登录页面 + +--- + + + +### 1、如何自定义 OAuth-Server 端的登录视图? + +重写 `cfg.notLoginView` 策略: + +``` java +@Autowired +public void configOAuth2Server(SaOAuth2Config cfg) { + // 配置:未登录时返回的View + cfg.notLoginView = ()->{ + return new ModelAndView("xxx.html"); + }; +} +``` + +在以上返回的视图中 ajax 方式调用 `/oauth2/doLogin` 接口,该接口接受以下参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| name | 否 | 账号 | +| pwd | 否 | 密码 | + +接口返回值根据你重写的 `cfg.doLoginHandle` 策略进行自由决定。 + + + +### 2、如何自定义登录API的接口地址? +根据需求点选择解决方案: + +#### 2.1、如果只是想在 doLoginHandle 函数里获取除 name、pwd 以外的参数? +``` java +// 在任意代码处获取前端提交的参数 +String xxx = SaHolder.getRequest().getParam("xxx"); +``` + +#### 2.2、想完全自定义一个接口来接受前端登录请求? +``` java +// 直接定义一个拦截路由为 `/oauth2/doLogin` 的接口即可 +@RequestMapping("/oauth2/doLogin") +public SaResult ss(String name, String pwd) { + System.out.println("------ 请求进入了自定义的API接口 ---------- "); + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok("登录成功!"); + } + return SaResult.error("登录失败!"); +} +``` + +#### 2.3、不想使用`/oauth2/doLogin`这个接口,想自定义一个API地址? + +答:直接在前端更改点击按钮时 Ajax 的请求地址即可 + + + +### 3、如何自定义 OAuth-Server 端的确认授权视图? + +重写 `cfg.confirmView` 策略: + +``` java +@Autowired +public void configOAuth2Server(SaOAuth2Config cfg) { + // 配置:授权确认视图 + cfg.confirmView = (clientId, scopes)->{ + Map map = new HashMap<>(); + map.put("clientId", clientId); + map.put("scope", scopes); + return new ModelAndView("confirm.html", map); + }; +} +``` + +在以上返回的视图中 ajax 方式调用 `/oauth2/doConfirm` 接口,即可完成授权,该接口接受以下参数: + +| 参数 | 是否必填 | 说明 | +| :-------- | :-------- | :-------- | +| client_id | 是 | 应用 id | +| scope | 是 | 具体授予的权限,多个用逗号(或空格)隔开 | + +接口返回值样例: +``` js +{ + "code": 200, + "msg": "ok", + "data": null, +} +``` + + diff --git a/sa-token-doc/oauth2/oauth2-server.md b/sa-token-doc/oauth2/oauth2-server.md index 9f2b631f..ad4a1724 100644 --- a/sa-token-doc/oauth2/oauth2-server.md +++ b/sa-token-doc/oauth2/oauth2-server.md @@ -11,7 +11,7 @@ ### 2、引入依赖 -创建SpringBoot项目 `sa-token-demo-oauth2-server`(不会的同学自行百度或参考仓库示例),添加pom依赖: +创建SpringBoot项目 `sa-token-demo-oauth2-server`(不会的同学自行百度或参考仓库示例),引入 `pom.xml` 依赖: @@ -43,36 +43,37 @@ implementation 'cn.dev33:sa-token-oauth2:${sa.top.version}' ### 3、开放服务 -1、新建 `SaOAuth2TemplateImpl` +1、自定义数据加载器:新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。 + ``` java /** - * Sa-Token OAuth2.0 整合实现 + * Sa-Token OAuth2:自定义数据加载器 */ @Component -public class SaOAuth2TemplateImpl extends SaOAuth2Template { +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { - // 根据 id 获取 Client 信息 + // 根据 clientId 获取 Client 信息 @Override public SaClientModel getClientModel(String clientId) { // 此为模拟数据,真实环境需要从数据库查询 if("1001".equals(clientId)) { return new SaClientModel() - .setClientId("1001") - .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") - .setAllowUrl("*") - .setContractScope("userinfo") - .setIsAutoMode(true); + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowUrls("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .setIsAutoMode(true); // 是否自动判断开放的授权模式 } return null; } - // 根据ClientId 和 LoginId 获取openid + // 根据 clientId 和 loginId 获取 openid @Override public String getOpenid(String clientId, Object loginId) { - // 此为模拟数据,真实环境需要从数据库查询 - return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"; + // 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询 + return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); } - + } ``` @@ -97,30 +98,32 @@ public class SaOAuth2ServerController { // Sa-OAuth2 定制化配置 @Autowired public void setSaOAuth2Config(SaOAuth2Config cfg) { - cfg. - // 配置:未登录时返回的View - setNotLoginView(() -> { - String msg = "当前会话在OAuth-Server端尚未登录,请先访问" - + " doLogin登录 " - + "进行登录之后,刷新页面开始授权"; - return msg; - }). - // 配置:登录处理函数 - setDoLoginHandle((name, pwd) -> { - if("sa".equals(name) && "123456".equals(pwd)) { - StpUtil.login(10001); - return SaResult.ok(); - } - return SaResult.error("账号名或密码错误"); - }). - // 配置:确认授权时返回的View - setConfirmView((clientId, scope) -> { - String msg = "

应用 " + clientId + " 请求授权:" + scope + "

" - + "

请确认: 确认授权

" - + "

确认之后刷新页面

"; - return msg; - }) - ; + + // 配置:未登录时返回的View + cfg.notLoginView = () -> { + String msg = "当前会话在OAuth-Server端尚未登录,请先访问" + + " doLogin登录 " + + "进行登录之后,刷新页面开始授权"; + return msg; + }; + + // 配置:登录处理函数 + cfg.doLoginHandle = (name, pwd) -> { + if("sa".equals(name) && "123456".equals(pwd)) { + StpUtil.login(10001); + return SaResult.ok(); + } + return SaResult.error("账号名或密码错误"); + }; + + // 配置:确认授权时返回的 view + cfg.confirmView = (clientId, scopes) -> { + String scopeStr = SaFoxUtil.convertListToString(scopes); + String msg = "

应用 " + clientId + " 请求授权:" + scopeStr + "

" + + "

请确认: 确认授权

" + + "

确认之后刷新页面

"; + return msg; + }; } // 全局异常拦截 @@ -134,7 +137,19 @@ public class SaOAuth2ServerController { ``` 注意:在`setDoLoginHandle`函数里如果要获取name, pwd以外的参数,可通过`SaHolder.getRequest().getParam("xxx")`来获取 -3、创建启动类: +3、全局异常处理 +``` java +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler + public SaResult handlerException(Exception e) { + e.printStackTrace(); + return SaResult.error(e.getMessage()); + } +} +``` + +4、创建启动类: ``` java /** * 启动:Sa-OAuth2 Server端 @@ -154,28 +169,44 @@ public class SaOAuth2ServerApplication { 1、由于暂未搭建Client端,我们可以使用Sa-Token官网作为重定向URL进行测试: ``` url -http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.cc&scope=userinfo +http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://sa-token.cc&scope=openid ``` 2、由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图 -![sa-oauth2-server-login-view](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-login-view.png 's-w-sh') +![sa-oauth2-server-login-view](https://oss.dev33.cn/sa-token/doc/oauth2-new/sa-oauth2-server-login-view.png 's-w-sh') 3、点击doLogin进行登录之后刷新页面,会提示我们确认授权 -![sa-oauth2-server-login-view](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-scope.png 's-w-sh') +![sa-oauth2-server-scope](https://oss.dev33.cn/sa-token/doc/oauth2-new/sa-oauth2-server-scope.png 's-w-sh') 4、点击确认授权之后刷新页面,我们会被重定向至 redirect_uri 页面,并携带了code参数 -![sa-oauth2-server-code](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-code.png 's-w-sh') +![sa-oauth2-server-code](https://oss.dev33.cn/sa-token/doc/oauth2-new/sa-oauth2-server-code.png 's-w-sh') 4、我们拿着code参数,访问以下地址: ``` url -http://sa-oauth-server.com:8001/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code} +http://sa-oauth-server.com:8000/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code} ``` 将得到 `Access-Token`、`Refresh-Token`、`openid`等授权信息 -![sa-oauth2-server-token](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-server-token.png 's-w-sh') +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "bearer", + "access_token": "cAls8jnBLmeo5yuCUMwb8zxaSsQPPzGINXF3NOCjCqFHplr6hagdT6A5HeR2", + "refresh_token": "L2rPbJ3aaOXwaB4Zu0EGWNz5EjVNpw5u2oMP9CS2IEap7rR3Hb76ZqqHS07J", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "openid", + "openid": "ded91dc189a437dd1bac2274be167d50" +} +``` + + 测试完毕 @@ -188,7 +219,7 @@ http://sa-oauth-server.com:8001/oauth2/token?grant_type=authorization_code&clien 依次启动`OAuth2-Server` 与 `OAuth2-Client`,然后从浏览器访问:[http://sa-oauth-client.com:8002](http://sa-oauth-client.com:8002) -![sa-oauth2-client-index](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-client-index.png 's-w-sh') +![sa-oauth2-client-index](https://oss.dev33.cn/sa-token/doc/oauth2-new/sa-oauth2-client-index.png 's-w-sh') 如图,可以针对OAuth2.0四种模式进行详细测试 diff --git a/sa-token-doc/oauth2/readme.md b/sa-token-doc/oauth2/readme.md index 59d9e4b0..5776fb42 100644 --- a/sa-token-doc/oauth2/readme.md +++ b/sa-token-doc/oauth2/readme.md @@ -2,12 +2,12 @@ --- -### 什么是OAuth2.0?解决什么问题? +### 什么是 OAuth2.0 ?解决什么问题? -简单来讲,OAuth2.0的应用场景可以理解为单点登录的升级版,单点登录解决了多个系统间会话的共享,OAuth2.0在此基础上增加了应用之间的权限控制 -(SO:有些系统采用OAuth2.0模式实现了单点登录,但这总给人一种“杀鸡焉用宰牛刀”的感觉) +简单来讲,OAuth2.0的应用场景可以理解为单点登录的升级版,单点登录解决了多个系统间会话的共享,OAuth2.0 在此基础上增加了应用之间的权限控制 +(SO:有些系统采用 OAuth2.0 模式实现了单点登录,但这总给人一种 “杀鸡焉用宰牛刀” 的感觉) -有关OAuth2.0的设计思想网上教程较多,此处不再重复赘述,详细可参考博客: +有关 OAuth2.0 的设计思想网上教程较多,此处不再重复赘述,详细可参考博客: [OAuth2.0 简单解释](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html) @@ -19,14 +19,40 @@ 基于不同的使用场景,OAuth2.0设计了四种模式: -1. 授权码(Authorization Code):OAuth2.0标准授权步骤,Server端向Client端下放Code码,Client端再用Code码换取授权Token -2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server端使用URL重定向方式直接将Token下放到Client端页面 -3. 密码式(Password):Client直接拿着用户的账号密码换取授权Token -4. 客户端凭证(Client Credentials):Server端针对Client级别的Token,代表应用自身的资源授权 +1. 授权码(Authorization Code):OAuth2.0 标准授权步骤,Server 端向 Client 端下放 `Code` 码,Client 端再用 `Code` 码换取授权 `Access-Token`。 +2. 隐藏式(Implicit):无法使用授权码模式时的备用选择,Server 端使用 URL 重定向方式直接将 `Access-Token` 下放到 Client 端页面。 +3. 密码式(Password):Client 端直接拿着用户的账号密码换取授权 `Access-Token`。 +4. 客户端凭证(Client Credentials):Server 端针对 Client 级别的 Token,代表应用自身的资源授权。 ![https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-setup.png](https://oss.dev33.cn/sa-token/doc/oauth2/sa-oauth2-setup.png) -接下来我们将通过简单示例演示如何在 Sa-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server) +接下来我们将通过简单示例演示如何在 Sa-Token-OAuth2 中完成这四种模式的对接: [搭建OAuth2-Server](/oauth2/oauth2-server) +### OAuth2.0 第三方开放平台完整开发流程参考 + +1. oauth2-server 平台端 + 1. 搭建 oauth2-server 数据后台管理端(后台人员对底层数据增删改查维护的地方)。 + 2. 搭建 oauth2-server 数据前台申请端(给第三方公司提供一个申请注册 client 的地方)。 + 3. 搭建 oauth2-server 授权端 以及其接口文档(让第三方公司拿到 access_token)。 + 4. 搭建 oauth2-server 资源端 以及其接口文档(让第三方公司通过 access_token 拿到对应的资源数据)。 + 5. 以上四端可以为一个项目,也可以为四个独立的项目。 + +2. oauth2-client 第三方公司端 + 1. 第三方公司登录 oauth-server 数据前台申请端,申请注册应用,拿到 `clientId`、`clientSecret` 等数据。 + 2. 在自己系统通过 `clientId`、`clientSecret` 等参数对接 oauth2-server 授权端,拿到 `access_token`。 + 3. 通过 `access_token` 调用 oauth2-server 资源端接口,拿到对应资源数据。 + +3. 用户端操作 + 1. 打开第三方公司开发的网站或APP等程序。 + 2. 一般有个“通过xx第三方登录”的按钮,点它。 + 3. 跳转到了 oauth2-server 端的网站,在此网站用 oauth2-server 的账号开始登录。 + 4. 登录完成,继续跳转到授权页,点击确认授权。 + 5. 授权完成,oauth2-server 端生成一个 code 码,重定向回 oauth2-client 的网站,把 code 参数挂到对应的 url 上。 + 6. oauth2-client 从 url 中读取 code 参数,提交到 oauth2-client 的后端。 + 7. oauth2-client 后端拿着 `code`、`clientId`、`clientSecret` 等信息调用 oauth2-server 授权端 的接口,得到 `access_token`。 + 8. 继续拿着 `access_token` 调用 oauth2-server 资源端获取此用户对应的数据。 + 9. 一般最终目的拿到一个 openid 值,oauth2-client 根据 openid 进行登录。生成自己的会话 token ,返回到数据到前端。 + 10. 前端拿到自己 oauth2-client 生成的会话 token ,完成登录。开始进行业务操作。 + diff --git a/sa-token-doc/sso/sso-apidoc.md b/sa-token-doc/sso/sso-apidoc.md index 53d47ef4..093a6e4f 100644 --- a/sa-token-doc/sso/sso-apidoc.md +++ b/sa-token-doc/sso/sso-apidoc.md @@ -28,6 +28,11 @@ http://{host}:{port}/sso/auth - 情况一:当前会话在 SSO 认证中心未登录,会进入登录页开始登录。 - 情况二:当前会话在 SSO 认证中心已登录,会被重定向至 `redirect` 地址,并携带 `ticket` 参数。 +`ticket` 码具有以下特点: +1. 每次授权产生的 `ticket` 码都不一样。 +2. `ticket` 码用完即废,不能二次使用。 +3. 一个 `ticket` 的有效期默认为五分钟,超时自动作废。 +4. 每次授权产生新 `ticket` 码,会导致旧 `ticket` 码立即作废,即使旧 `ticket` 码尚未使用。 ### 2、RestAPI 登录接口 ``` url diff --git a/sa-token-doc/use/config.md b/sa-token-doc/use/config.md index 0e80c9cb..89a97a97 100644 --- a/sa-token-doc/use/config.md +++ b/sa-token-doc/use/config.md @@ -297,9 +297,9 @@ sa-token.sso-client.is-slo=true | 参数名称 | 类型 | 默认值 | 说明 | | :-------- | :-------- | :-------- | :-------- | | isCode | Boolean | true | 是否打开模式:授权码(`Authorization Code`) | -| isImplicit | Boolean | false | 是否打开模式:隐藏式(`Implicit`) | -| isPassword | Boolean | false | 是否打开模式:密码式(`Password`) | -| isClient | Boolean | false | 是否打开模式:凭证式(`Client Credentials`) | +| isImplicit | Boolean | true | 是否打开模式:隐藏式(`Implicit`) | +| isPassword | Boolean | true | 是否打开模式:密码式(`Password`) | +| isClient | Boolean | true | 是否打开模式:凭证式(`Client Credentials`) | | isNewRefresh | Boolean | false | 是否在每次 `Refresh-Token` 刷新 `Access-Token` 时,产生一个新的 Refresh-Token | | codeTimeout | long | 300 | Code授权码 保存的时间(单位:秒) 默认五分钟 | | accessTokenTimeout | long | 7200 | `Access-Token` 保存的时间(单位:秒)默认两个小时 | diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java index a88b6b7a..7c6f5ff6 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java @@ -19,14 +19,18 @@ import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; import cn.dev33.satoken.oauth2.dao.SaOAuth2Dao; import cn.dev33.satoken.oauth2.data.convert.SaOAuth2DataConverter; -import cn.dev33.satoken.oauth2.data.model.*; -import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; +import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; +import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.CodeModel; +import cn.dev33.satoken.oauth2.data.model.RefreshTokenModel; import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; +import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel; import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode; import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.strategy.SaOAuth2Strategy; import cn.dev33.satoken.util.SaFoxUtil; +import java.util.LinkedHashMap; import java.util.List; /** @@ -211,6 +215,7 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { String clientTokenValue = SaOAuth2Strategy.instance.createClientToken.execute(clientId, scopes); ClientTokenModel ct = new ClientTokenModel(clientTokenValue, clientId, scopes); ct.expiresTime = System.currentTimeMillis() + (cm.getClientTokenTimeout() * 1000); + ct.extraData = new LinkedHashMap<>(); SaOAuth2Strategy.instance.workClientTokenByScope.accept(ct); // 3、保存新Client-Token