!4 Merge v3.0.0 to cofig main

Merge pull request !4 from monkeyk7/3.0.0
git-as-svn/v1/config
monkeyk7 2023-11-01 15:32:26 +00:00 committed by Gitee
commit 18c861b785
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
145 changed files with 8147 additions and 3641 deletions

Binary file not shown.

View File

@ -1 +0,0 @@
distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip

231
README.md
View File

@ -1,24 +1,26 @@
## spring-oauth-server
<br/>
java config版本(Spring Boot)
java config版本
<strong>Spring与OAuth2的整合示例</strong>
<strong>Spring与OAuth2的整合示例. OIDC1.0 + OAuth2.1</strong>
项目用Maven管理
<br/>
Base on Spring-Boot
Base on SpringBoot
使用的技术与版本号
使用的主要技术与版本号
<ol>
<li>JDK (1.8.0_40)</li>
<li>spring-security-oauth2 (2.3.8.RELEASE)</li>
<li>spring-security-jwt (1.1.1.RELEASE)</li>
<li>Spring Boot(2.1.4.RELEASE)</li>
<li>JDK (openjdk 17)</li>
<li>Spring Boot(3.1.2)</li>
<li>Spring Core(6.0.11)</li>
<li>spring-security-oauth2-authorization-server (1.1.1)</li>
<li>thymeleaf (3.1.1.RELEASE)</li>
</ol>
<hr/>
<h3>授权协议</h3>
<em><a href="https://gitee.com/shengzhao/spring-oauth-server/tree/master/LICENSE">GPL-2.0</a></em>
@ -31,69 +33,58 @@ Base on Spring-Boot
<li> <a href="https://v.youku.com/v_show/id_XMzg2Mjk0Mjk0MA==.html?f=51900110&o=1">在线测试环境的使用</a> 2018-10-14</li>
<li> <a href="https://v.youku.com/v_show/id_XNDMwMTg4MTQ5Mg==.html?f=51900110&o=1">spring-oauth-server v2.0.1更新内容简介</a> 2019-08-05</li>
</ol>
> 注意以上视频主要适用于v2.x版本
<a href="http://list.youku.com/albumlist/show/id_51900110.html" target="_blank">http://list.youku.com/albumlist/show/id_51900110.html</a>
(持续更新...)
<br/>
<br/>
<p>
1000 star Gitee奖杯: <a href="http://andaily.com/blog/wp-content/uploads/2019/09/sos-1000-stars.jpg" target="_blank">sos-1000-stars.jpg</a> [2019年]
</p>
<hr/>
<h3>版本分支介绍</h3>
<strong>MySQL版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/">master</a></strong>
<br/>
<strong>MongoDB版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/tree/mongodb/">mongodb</a></strong>
<br/>
<strong>Redis版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/tree/config-redis/">
config-redis</a></strong>
<hr/>
<h3>相关项目</h3>
<h4>
OAuth2客户端项目请访问 <a href="https://gitee.com/mkk/spring-oauth-client">spring-oauth-client</a>
</h4>
<h4>
在线测试访问地址 <a href="http://andaily.com/spring-oauth-server/">http://andaily.com/spring-oauth-server/</a>
</h4>
<h4>
Shiro与OLTU整合的OAuth2项目 <a href="https://gitee.com/mkk/oauth2-shiro">https://gitee.com/mkk/oauth2-shiro</a>
(相比spring-oauth-server, 该项目入门门槛相对较低, 代码更加透明, 理解更容易,可扩展性更强, 且模块化开发)
</h4>
- MySQL版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/">master</a>
- MongoDB版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/tree/mongodb/">mongodb</a>
- Redis版本请访问Branch: <a href="https://gitee.com/shengzhao/spring-oauth-server/tree/config-redis/">config-redis</a>
<hr/>
<h3>相关项目</h3>
1. OAuth2客户端项目请访问 <a href="https://gitee.com/mkk/spring-oauth-client">spring-oauth-client</a>
2. 在线测试访问地址 <a href="https://andaily.com/spring-oauth-server">https://andaily.com/spring-oauth-server</a> (v1.0)
3. Shiro与OLTU整合的OAuth2项目 <a href="https://gitee.com/mkk/oauth2-shiro">https://gitee.com/mkk/oauth2-shiro</a>
(相比spring-oauth-server, 该项目入门门槛相对较低, 代码更加透明, 理解更容易,可扩展性更强, 且模块化开发)
<hr/>
<div>
<h3>如何使用?</h3>
<ol>
<li>
项目是Maven管理的, 需要本地安装maven(开发用的maven版本号为3.3.3), 还有MySql(开发用的mysql版本号为5.6)
</li>
<li>
<a href="https://gitee.com/shengzhao/spring-oauth-server/repository/archive?ref=master">下载</a>(或clone)项目到本地
</li>
<li>
创建MySQL数据库(如数据库名oauth2_boot), 并运行相应的SQL脚本(脚本文件位于others/database目录),
<br/>
运行脚本的顺序: initial_db.ddl -> oauth.ddl -> initial_data.ddl
</li>
<li>
修改application.properties(位于src/main/resources目录)中的数据库连接信息(包括username, password等)
</li>
<li>
将本地项目导入到IDE(如Intellij IDEA)中, 可直接运行<code>SpringOauthServerApplication.java</code>进行访问或配置Tomcat(或类似的servlet运行服务器), 并启动Tomcat(默认端口为8080);
<br/>
也可通过maven package命令将项目编译为war文件(spring-oauth-server.war),
将war放在Tomcat中并启动(注意: 这种方式需要将application.properties加入到classpath中并正确配置数据库连接信息).
<br/>
若使用<code>java -jar spring-oauth-server.war</code>启动, 则需要使用参数<em>spring.config.location</em>指定配置文件,
如:<code>java -jar spring-oauth-server.war --spring.config.location=xxx.properties</code>
<br/>
提示若打包为war则项目的 contextPath(根路径) 为 'spring-oauth-server'.
</li>
<li>
参考<a href="https://gitee.com/shengzhao/spring-oauth-server/blob/config/others/oauth_test.txt">oauth_test.txt</a>(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080/ 在界面上操作).
</li>
</ol>
</div>
1. 项目是Maven管理的, 需要本地安装Maven(开发用的maven版本号为3.6.0), 还有MySql(开发用的mysql版本号为5.7.22)
2. <a href="https://gitee.com/shengzhao/spring-oauth-server">下载</a>(或clone)项目源码到本地
3. 创建MySQL数据库(默认数据库名oauth2_boot), 并运行相应的SQL脚本(脚本文件位于others/database目录),
运行脚本的顺序: <code>initial_db.ddl -> oauth.ddl -> initial_data.ddl</code>
4. 修改application.properties(位于src/main/resources目录)中的数据库连接信息(包括username, password等)
5. 将本地项目导入到IDE(如Intellij IDEA)中, 可直接运行<code>SpringOauthServerApplication.java</code>进行访问或配置Tomcat(或类似的servlet运行服务器), 并启动Tomcat(默认端口为8080);
也可通过maven package命令将项目编译为jar文件(spring-oauth-server.jar),
使用命令<code>java -jar</code>启动访问.
若使用<code>java -jar spring-oauth-server.jar</code>启动, 建议使用参数<em>spring.config.location</em>指定配置文件,
如:<code>java -jar spring-oauth-server.jar --spring.config.location=xxx.properties</code>
6. 参考<a href="https://gitee.com/shengzhao/spring-oauth-server/blob/config/others/oauth2.1-flow.md">oauth2.1-flow.md</a>(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080/ 在界面上操作).
<div>
<h4>配置参数说明</h4>
说明配置文件<em>application.properties</em>中的主要变量。
@ -103,35 +94,36 @@ config-redis</a></strong>
</thead>
<tbody>
<tr><td>spring.datasource.*</td><td></td><td>-</td><td>数据库连接相关配置</td></tr>
<tr><td>spring.mvc.*</td><td></td><td>-</td><td>Spring MVC相关配置</td></tr>
<tr><td>logging.level.root</td><td></td><td>INFO</td><td>默认的日志级别</td></tr>
<tr><td>sos.token.store</td><td></td><td>jwt</td><td>可选值jwt,jdbc配置Token存储方式v2.1.0增加</td></tr>
<tr><td>sos.token.store.jwt.key</td><td></td><td>IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa</td><td><em>sos.token.store</em>为jwt时配置具体的jwt key(长度16位或32位)</td></tr>
<tr><td>sos.reuse.refresh-token</td><td></td><td>true</td><td>可选值true,falsetrue重复使用refresh_token值直到过期false每次刷新时生成新的refresh_token值(类似session机制进行续期)v2.1.0增加</td></tr>
<tr><td>spring.thymeleaf.*</td><td></td><td>-</td><td>Spring MVC thymeleaf相关配置</td></tr>
<tr><td>server.port</td><td></td><td>8080</td><td>服务运行端口号</td></tr>
<tr><td>spring.security.oauth2.authorizationserver.issuer</td><td></td><td></td><td>OAuth2 issuer, 生产环境配置对外访问完整地址</td></tr>
<tr><td>spring.application.name</td><td></td><td></td><td>应用组件名称</td></tr>
</tbody>
</table>
</div>
<hr/>
<h3>grant_type 介绍</h3>
<br/>
说明OAuth2支持的grant_type(授权方式)与功能
说明OAuth2.1支持的grant_type(授权方式)与功能
<ol>
<li><code>authorization_code</code> -- 授权码模式(即先登录获取code,再获取token)</li>
<li><code>password</code> -- 密码模式(将用户名,密码传过去,直接获取token)</li>
<li><code>authorization_code + PKCE</code> -- 授权码模式+PKCE (即先登录获取code, 请求时增加参数<em>code_challenge</em><em>code_challenge_method</em>; 再获取token,增加参数<em>code_verifier</em>)</li>
<li><code>password</code> -- 密码模式(将用户名,密码传过去,直接获取token) <mark>OAuth2.1不推荐使用</mark></li>
<li><code>refresh_token</code> -- 刷新access_token</li>
<li><code>implicit</code> -- 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash)</li>
<li><code>device_code</code> -- 适用于各类无输入键盘的物联网智能设备进行认证授权, 通过类似'扫码登录'形式完成整个流程 <strong>OAuth2.1新增</strong></li>
<li><code>client_credentials</code> -- 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向'服务端'获取资源)</li>
<li><code>jwt-bearer</code> -- 增强client端请求安全性的断言(assertion)实现; 通过类似'双向SSL'的机制来让server端验证client端的签名实现强安全性.</li>
</ol>
> 注意相比OAuth2.0,去掉了 **implicit** 模式
<hr/>
<h3>帮助与改进</h3>
<ol>
<li>
<p>
与该项目相关的博客请访问 <a target="_blank" href="http://blog.csdn.net/monkeyking1987/article/details/16828059">http://blog.csdn.net/monkeyking1987/article/details/16828059</a>
与该项目相关的博客请访问 <a target="_blank" href="https://blog.csdn.net/monkeyking1987/article/details/16828059">https://blog.csdn.net/monkeyking1987/article/details/16828059</a>
</p>
</li>
<li>
@ -164,23 +156,18 @@ config-redis</a></strong>
<code>改变token过期的时间的配置</code>, 请下载文件<a href="https://git.oschina.net/shengzhao/spring-oauth-server/attach_files/download?i=6589&u=http%3A%2F%2Ffiles.git.oschina.net%2Fgroup1%2FM00%2F00%2F43%2FcHwGbFRpuk6ANN2CAANJ-Rkiz_c649.jpg%3Ftoken%3D686e6d5b1e9ab04446dbfeb977c3b7a1%26ts%3D1421151523%26filename%3D%E6%94%B9%E5%8F%98token%E8%BF%87%E6%9C%9F%E7%9A%84%E6%97%B6%E9%97%B4%E7%9A%84%E9%85%8D%E7%BD%AE.jpg">改变token过期的时间的配置.jpg</a>
</li>
<li>
<code>自定义 grant_type</code>, 默认情况支持的grant_type包括 [password,authorization_code,refresh_token,implicit], 若不需要其中的某些grant_type,
<code>自定义 grant_type</code>, 默认情况支持的grant_type包括 [password,authorization_code,refresh_token,device_code], 若不需要其中的某些grant_type,
则可以修改 oauth_client_details 表中的 authorized_grant_types 字段的值;
<br/>
若想把整个OAuth2服务修改来只支持某些grant_type, 请修改 <i>security.xml</i>文件中的
<label>oauth2:authorization-server</label> 中的内容,将对应的 grant_type 注释或删掉即可
</li>
<li>
<div>
<code>如何刷新access_token(refresh_token)</code>, 在通过客户端(如移动设备)登录成功后返回的数据如下
<br/>
<pre>{"access_token":"3420d0e0-ed77-45e1-8370-2b55af0a62e8","token_type":"bearer","refresh_token":"b36f4978-a172-4aa8-af89-60f58abe3ba1","expires_in":43199,"scope":"read write"}
</pre>
<pre>{"access_token":"eyJraWQiOiJteW9pZGMta2V5aWQiLCJhbGciOiJSUzI1...","token_type":"bearer","refresh_token":"UCFNxUj4ytr241KzwJJgnMno1RfmoLs0GKVxNWPjW5VZ7d4U4YsDM7...","expires_in":43199,"scope":"openid"}</pre>
<br/>
若需要刷新获取新的token(一般在 expires_in 有效期时间快到时), 请求的URL类似如下
<br/>
<pre>http://localhost:8080/oauth/token?client_id=mobile-client&client_secret=mobile&grant_type=refresh_token&refresh_token=b36f4978-a172-4aa8-af89-60f58abe3ba1
</pre>
<pre>http://localhost:8080/oauth2/token?client_id=mobile-client&client_secret=mobile&grant_type=refresh_token&refresh_token=UCFNxUj4ytr241KzwJJgnMno1RfmoLs0GKVxNWPjW5VZ7</pre>
<br/>
注意: refresh_token 参数值必须与登录成功后获取的 refresh_token 一致, 且grant_type = refresh_token
<br/>
@ -200,14 +187,28 @@ config-redis</a></strong>
<ul>
<li>
<p>
Version: <strong>2.1.1</strong> [pending]
Version: <strong>3.0.0</strong> [finished]
<br/>
Date: 2023-10-10 / 2023-10-31
</p>
<ol>
<li><p>底层安全架构升级jdk升级17, spring6.x, springboot3.x, thymeleaf替换servlet/jsp</p></li>
<li><p>全面升级支持 OAuth2.1协议与 OIDC1.0协议</p></li>
<li><p>构建包由war换成jar, SQL相应调整</p></li>
<li><p>用spring-security-oauth2-authorization-server升级替换spring-security-oauth2, 详见<a href="https://andaily.com/blog/?p=20077">背景说明</a></p></li>
<li><p>界面使用说明按OAuth2.1进行友好设计并更新各提示语句</p></li>
<li><p>增加spring-restdocs文档支持, 自动生成API相关文档</p></li>
</ol>
</li>
<li>
<p>
Version: <strong>2.1.1</strong> [canceled]
<br/>
Date: 2022-05-05 / ---
</p>
<ol>
<li><p>尝试升级替换spring-security-oauth2, 详见<a href="https://andaily.com/blog/?p=20077">背景说明</a></p></li>
</ol>
<br/>
</li>
<li>
<p>
@ -343,7 +344,7 @@ config-redis</a></strong>
<ol>
<li><p>#73 - Upgrade 'spring-security-oauth2' version to '2.0.6.RELEASE' (current: 1.0.5.RELEASE) [CANCELED]</p></li>
<li><p><del>#74 - oauth mysql ddl add create_time, default is now() </del></p></li>
<li><p><del>#75 - Add user information API, for <a href="https://gitee.com/mkk/spring-oauth-client"><code>spring-oauth-client</code></a> project use
<li><div><del>#75 - Add user information API, for <a href="https://gitee.com/mkk/spring-oauth-client"><code>spring-oauth-client</code></a> project use
<pre>
URL: /unity/user_info
Login: Yes (ROLE_UNITY)
@ -352,7 +353,7 @@ config-redis</a></strong>
Login: Yes (ROLE_MOBILE)
Data Format: JSON
</pre>
</del></p>
</del></div>
</li>
<li><p><del>#77 - User add Privilege domain.
Addition initial two user: unityuser(ROLE_UNITY),mobileuser("ROLE_MOBILE).
@ -371,9 +372,10 @@ config-redis</a></strong>
<hr/>
<h3>数据库表字段说明</h3>
<p>
在0.3版本中添加了<code>db_table_description.html</code>文件(位于/others目录), 用来说明数据库脚本文件<code>oauth.ddl</code>中各表,各字段的用途及使用场合.
在0.3版本中添加了<code>db_table_description.html</code>文件(位于/others目录, 3.0.0版本db_table_description_3.0.0.html),
用来说明数据库脚本文件<code>oauth.ddl</code>中各表,各字段的用途及使用场合.
<br/>
也可在线访问<a href="http://andaily.com/spring-oauth-server/db_table_description.html">http://andaily.com/spring-oauth-server/db_table_description.html</a>.
也可在线访问<a href="https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html">db_table_description_3.0.0.html</a>.
</p>
@ -402,6 +404,8 @@ config-redis</a></strong>
<li><p><em>2019-08-04</em> 发布 <a href="https://gitee.com/shengzhao/spring-oauth-server/tree/2.0.1/">2.0.1</a> 版本</p></li>
<li><p><em>2020-06-04</em> 发布 2.0.2 版本</p></li>
<li><p><em>2022-05-01</em> 发布 2.1.0 版本</p></li>
<li><p><em>2023-10-10</em> 开始全新大版本 3.0.0 开发</p></li>
<li><p><em>2023-10-31</em> 发布 3.0.0 全新版本</p></li>
</ol>
</div>
@ -412,76 +416,84 @@ config-redis</a></strong>
<ul>
<li>
<p>
<a href="http://tools.ietf.org/html/rfc6749">RFC 6749 - The OAuth 2.0 Authorization Framework</a>, OAuth2.0协议(英文)
<a href="https://tools.ietf.org/html/rfc6749">RFC 6749 - The OAuth 2.0 Authorization Framework</a>, OAuth2.0协议(英文)
</p>
</li>
<li>
<p>
<a href="http://oauth.net/2/">OAuth 2.0 &mdash; OAuth</a>, OAuth2.0官方网站
<a href="https://oauth.net/2/">OAuth 2.0 &mdash; OAuth</a>, OAuth2.0官方网站
</p>
</li>
<li>
<p>
<a href="http://netment.iteye.com/blog/945402">OAuth2核心参数说明</a>, 重点介绍了grant_type 与 response_type 以及示例
<a href="https://netment.iteye.com/blog/945402">OAuth2核心参数说明</a>, 重点介绍了grant_type 与 response_type 以及示例
</p>
</li>
<li>
<p>
<a href="http://apiwiki.poken.com/authentication/oauth2">OAuth2 flows</a>, 详细介绍OAuth2的流程,各类错误发生时的响应
<a href="https://apiwiki.poken.com/authentication/oauth2">OAuth2 flows</a>, 详细介绍OAuth2的流程,各类错误发生时的响应
</p>
</li>
<li>
<p>
<a href="http://www.oschina.net/translate/oauth-2-developers-guide">OAuth 2 开发人员指南Spring security oauth2</a>, 翻译OAuth 2 Developers Guide(spring security oauth2)
<a href="https://www.oschina.net/translate/oauth-2-developers-guide">OAuth 2 开发人员指南Spring security oauth2</a>, 翻译OAuth 2 Developers Guide(spring security oauth2)
</p>
</li>
<li>
<p>
<a href="http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html">理解OAuth 2.0</a>, 介绍OAuth2各类grant_type的使用
<a href="https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html">理解OAuth 2.0</a>, 介绍OAuth2各类grant_type的使用
</p>
</li>
<li>
<p>
<a href="http://www.dannysite.com/blog/178/">OAuth2隐式授权Implicit Grant类型的开放授权</a>, 介绍grant_type='implicit'模式
<a href="https://www.dannysite.com/blog/178/">OAuth2隐式授权Implicit Grant类型的开放授权</a>, 介绍grant_type='implicit'模式
</p>
</li>
<li>
<p>
<a href="http://oltu.apache.org/">Apache Oltu</a>, Java版的 OAuth2参考实现, 建议去了解了解
<a href="https://oltu.apache.org/">Apache Oltu</a>, Java版的 OAuth2参考实现, 建议去了解了解
</p>
</li>
<li>
<p>
<a href="http://andaily.com/blog/?p=440">OIDC基于OAuth2的下一代身份认证授权协议</a>
<a href="https://andaily.com/blog/?p=440">OIDC基于OAuth2的下一代身份认证授权协议</a>
</p>
</li>
<li>
<p>
<a href="http://andaily.com/blog/?p=19776">在spring-oauth-server中将AccessToken存入Redis的配置</a>
<a href="https://andaily.com/blog/?p=19776">在spring-oauth-server中将AccessToken存入Redis的配置</a>
</p>
</li>
<li>
<p>
<a href="http://andaily.com/blog/?p=19793">如何通过代码生成AccessToken</a>
<a href="https://andaily.com/blog/?p=19793">如何通过代码生成AccessToken</a>
</p>
</li>
<li>
<p>
<a href="http://andaily.com/blog/?p=19884">OAuth2中 access_tokenrefresh_token的各类配置与使用场景FAQ</a>
<a href="https://andaily.com/blog/?p=19884">OAuth2中 access_tokenrefresh_token的各类配置与使用场景FAQ</a>
</p>
</li>
<li>
<p><a href="https://oauth.net/2.1/">OAuth2.1 介绍</a></p>
</li>
<li>
<p><a href="https://andaily.com/blog/?p=20004">OAuth 2.1的状态与主要特征</a>, 个人总结</p>
</li>
<li>
<p><a href="https://openid.net/developers/how-connect-works/">OIDC官方网站</a></p>
</li>
</ul>
<hr/>
<h4>
与项目相关的技术文章请访问 <a href="http://andaily.com/blog/?cat=19">http://andaily.com/blog/?cat=19</a> (不断更新与OAuth2相关的文章)
</h4>
<br/>
<strong>
与项目相关的技术文章请访问 <a href="https://andaily.com/blog/?cat=19">http://andaily.com/blog/?cat=19</a> (不断更新与OAuth2相关的文章)
</strong>
<div>
<h4>问答与讨论</h4>
<strong>问答与讨论</strong>
<br/>
与项目相关的与OAuth2相关的问题与回答以及各类讨论请访问<br/>
<a href="http://andaily.com/blog/?dwqa-question_category=oauth">http://andaily.com/blog/?dwqa-question_category=oauth</a>
<a href="https://andaily.com/blog/?dwqa-question_category=oauth">http://andaily.com/blog/?dwqa-question_category=oauth</a>
</div>
@ -502,23 +514,22 @@ config-redis</a></strong>
<hr/>
<div>
<h3>捐助</h3>
<br/>
支付宝: monkeyking1987@126.com (**钊)
<br/>
快意江湖 -- 100元
<br/>
yufan -- 100元
- 快意江湖 -- 100元
- yufan -- 100元
</div>
<hr/>
<h3>其他...</h3>
<p>
关注更多开源项目请访问 <a href="http://andaily.com/my_projects.html">http://andaily.com/my_projects.html</a>
关注更多开源项目请访问 <a href="https://andaily.com/my_projects.html">https://andaily.com/my_projects.html</a>
</p>
<p>
<em>欢迎联系作者 <a href="mailto:sz@monkeyk.com">sz@monkeyk.com</a> 进行探讨</em>
</p>
<hr/>

225
mvnw vendored
View File

@ -1,225 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Migwn, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
echo $MAVEN_PROJECTBASEDIR
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

143
mvnw.cmd vendored
View File

@ -1,143 +0,0 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

Binary file not shown.

View File

@ -2,42 +2,57 @@
truncate user_;
truncate user_privilege;
-- admin, password is admin ( All privileges)
insert into user_(id,guid,create_time,email,password,phone,username,default_user)
values
(21,'29f6004fb1b0466f9572b02bf2ac1be8',now(),'admin@andaily.com','$2a$10$XWN7zOvSLDiyxQnX01KMXuf5NTkkuAUtt23YxUMWaIPURcR7bdULi','028-1234567','admin',1);
-- admin, password is Admin@2013 ( All privileges)
insert into user_(id, guid, create_time, email, password, phone, username, default_user)
values (21, '29f6004fb1b0466f9572b02bf2ac1be8', now(), 'admin@andaily.com',
'$2a$10$bIIt6KqIMweTZZC.IIHBLuN3dEIJL0LQFRPrtWTujn9O3Sl5Us5vW', '028-1234567', 'admin', 1);
insert into user_privilege(user_id,privilege) values (21,'ADMIN');
insert into user_privilege(user_id,privilege) values (21,'UNITY');
insert into user_privilege(user_id,privilege) values (21,'MOBILE');
insert into user_privilege(user_id, privilege)
values (21, 'ADMIN');
insert into user_privilege(user_id, privilege)
values (21, 'UNITY');
insert into user_privilege(user_id, privilege)
values (21, 'MOBILE');
-- unity, password is unity ( ROLE_UNITY)
insert into user_(id,guid,create_time,email,password,phone,username,default_user)
values
(22,'55b713df1c6f423e842ad68668523c49',now(),'unity@andaily.com','$2a$10$gq3eUch/h.eHt20LpboSXeeZinzSLBk49K5KD.Ms4/1tOAJIsrrfq','','unity',0);
-- unity, password is Unity#2013 ( ROLE_UNITY)
insert into user_(id, guid, create_time, email, password, phone, username, default_user)
values (22, '55b713df1c6f423e842ad68668523c49', now(), 'unity@andaily.com',
'$2a$10$M/bdEKNH12ksSmMgt0p3YeSjW4C5auAjE8by9BY6oEkHTjGKNDqTO', '', 'unity', 0);
insert into user_privilege(user_id,privilege) values (22,'UNITY');
insert into user_privilege(user_id, privilege)
values (22, 'UNITY');
-- mobile, password is mobile ( ROLE_MOBILE)
insert into user_(id,guid,create_time,email,password,phone,username,default_user)
values
(23,'612025cb3f964a64a48bbdf77e53c2c1',now(),'mobile@andaily.com','$2a$10$BOmMzLDaoiIQ4Q1pCw6Z4u0gzL01B8bNL.0WUecJ2YxTtHVRIA8Zm','','mobile',0);
-- mobile, password is Mobile*2013 ( ROLE_MOBILE)
insert into user_(id, guid, create_time, email, password, phone, username, default_user)
values (23, '612025cb3f964a64a48bbdf77e53c2c1', now(), 'mobile@andaily.com',
'$2a$10$MJKW44F.e.UH.54OY36b6eCPpp8KRszL3vAgqLyL1WWnpbGp7A8zW', '', 'mobile', 0);
insert into user_privilege(user_id,privilege) values (23,'MOBILE');
insert into user_privilege(user_id, privilege)
values (23, 'MOBILE');
-- initial oauth client details test data
-- 'unity-client' support browser, js(flash) visit, secret: unity
-- 'unity-client' support browser device visit, secret: unity
-- 'mobile-client' only support mobile-device visit, secret: mobile
truncate oauth_client_details;
insert into oauth_client_details
(client_id, resource_ids, client_secret, scope, authorized_grant_types,
web_server_redirect_uri,authorities, access_token_validity,
refresh_token_validity, additional_information, create_time, archived, trusted)
values
('unity-client','sos-resource', '$2a$10$QQTKDdNfj9sPjak6c8oWaumvTsa10MxOBOV6BW3DvLWU6VrjDfDam', 'read','authorization_code,refresh_token,implicit',
'http://localhost:8080/spring-oauth-server/unity/dashboard','ROLE_CLIENT',null,
null,null, now(), 0, 0),
('mobile-client','sos-resource', '$2a$10$uLvpxfvm3CuUyjIvYq7a9OUmd9b3tHFKrUaMyU/jC01thrTdkBDVm', 'read','password,refresh_token',
null,'ROLE_CLIENT',null,
null,null, now(), 0, 0);
truncate oauth2_registered_client;
insert into oauth2_registered_client
(id, create_time, client_id, client_secret, client_name, client_authentication_methods,
authorization_grant_types, redirect_uris, post_logout_redirect_uris, scopes, client_settings, token_settings)
values ('851eee5eaba94b0cacca53a3ef543423', now(), 'unity-client',
'$2a$10$QQTKDdNfj9sPjak6c8oWaumvTsa10MxOBOV6BW3DvLWU6VrjDfDam',
'Unity-Client',
'client_secret_post,client_secret_jwt,client_secret_basic',
'refresh_token,urn:ietf:params:oauth:grant-type:device_code,authorization_code',
'http://localhost:8080/unity/dashboard', null, 'openid,profile,email',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":true,"settings.client.require-authorization-consent":true}',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","ES256"],"settings.token.access-token-time-to-live":["java.time.Duration",7200.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",172800.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",120.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}'),
('aedd67f6dae441b99e3a0fb27889ce12', now(), 'mobile-client',
'$2a$10$uLvpxfvm3CuUyjIvYq7a9OUmd9b3tHFKrUaMyU/jC01thrTdkBDVm',
'Mobile-Client',
'client_secret_post,client_secret_basic',
'refresh_token,password',
null, null, 'openid,profile',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":true,"settings.client.require-authorization-consent":true}',
'{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","ES256"],"settings.token.access-token-time-to-live":["java.time.Duration",7200.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",172800.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",120.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}');

View File

@ -12,29 +12,40 @@
-- ###############
-- Domain: User
-- ###############
Drop table if exists user_;
CREATE TABLE user_ (
id int(11) NOT NULL auto_increment,
guid varchar(255) not null unique,
create_time datetime ,
archived tinyint(1) default '0',
email varchar(255),
password varchar(255) not null,
phone varchar(255),
username varchar(255) not null unique,
default_user tinyint(1) default '0',
last_login_time datetime ,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;
Drop table if exists user_;
CREATE TABLE user_
(
id int(11) NOT NULL auto_increment,
guid varchar(255) not null unique,
create_time datetime,
archived tinyint(1) default '0',
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
username varchar(255) not null unique,
password varchar(255) not null,
enabled tinyint(1) default '1',
phone varchar(255),
email varchar(255),
address varchar(255),
nickname varchar(255),
updated_at int(15) default 0,
default_user tinyint(1) default '0',
last_login_time datetime,
PRIMARY KEY (id),
index idx_username (username)
) ENGINE = InnoDB
AUTO_INCREMENT = 20
DEFAULT CHARSET = utf8;
-- ###############
-- Domain: Privilege
-- ###############
Drop table if exists user_privilege;
CREATE TABLE user_privilege (
user_id int(11),
privilege varchar(255),
KEY user_id_index (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Drop table if exists user_privilege;
CREATE TABLE user_privilege
(
user_id int(11),
privilege varchar(255),
KEY user_id_index (user_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;

View File

@ -1,66 +1,84 @@
--
-- Oauth sql -- MYSQL
-- Oauth sql -- MYSQL v3.0.0
--
Drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information TEXT,
create_time timestamp default now(),
archived tinyint(1) default '0',
trusted tinyint(1) default '0',
autoapprove VARCHAR (255) default 'false'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Drop table if exists oauth2_registered_client;
CREATE TABLE oauth2_registered_client
(
id varchar(100) NOT NULL,
archived TINYINT(1) DEFAULT '0',
create_time DATETIME,
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
client_id varchar(100) NOT NULL,
client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at datetime DEFAULT NULL,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- authorization
Drop table if exists oauth2_authorization;
CREATE TABLE oauth2_authorization
(
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at datetime DEFAULT NULL,
authorization_code_expires_at datetime DEFAULT NULL,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at datetime DEFAULT NULL,
access_token_expires_at datetime DEFAULT NULL,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at datetime DEFAULT NULL,
oidc_id_token_expires_at datetime DEFAULT NULL,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at datetime DEFAULT NULL,
refresh_token_expires_at datetime DEFAULT NULL,
refresh_token_metadata blob DEFAULT NULL,
user_code_value blob DEFAULT NULL,
user_code_issued_at datetime DEFAULT NULL,
user_code_expires_at datetime DEFAULT NULL,
user_code_metadata blob DEFAULT NULL,
device_code_value blob DEFAULT NULL,
device_code_issued_at datetime DEFAULT NULL,
device_code_expires_at datetime DEFAULT NULL,
device_code_metadata blob DEFAULT NULL,
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
Drop table if exists oauth_access_token;
create table oauth_access_token (
create_time timestamp default now(),
token_id VARCHAR(255),
token BLOB,
authentication_id VARCHAR(255) UNIQUE,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication BLOB,
refresh_token VARCHAR(255)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
create_time timestamp default now(),
token_id VARCHAR(255),
token BLOB,
authentication BLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Drop table if exists oauth_code;
create table oauth_code (
create_time timestamp default now(),
code VARCHAR(255),
authentication BLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- authorization consent
Drop table if exists oauth2_authorization_consent;
CREATE TABLE oauth2_authorization_consent
(
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (registered_client_id, principal_name)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- Add indexes
create index token_id_index on oauth_access_token (token_id);
create index authentication_id_index on oauth_access_token (authentication_id);
create index user_name_index on oauth_access_token (user_name);
create index client_id_index on oauth_access_token (client_id);
create index refresh_token_index on oauth_access_token (refresh_token);
create index token_id_index on oauth_refresh_token (token_id);
create index code_index on oauth_code (code);

View File

@ -0,0 +1,506 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="https://andaily.com/spring-oauth-server/favicon.ico" />
<title>数据库表说明 - spring-oauth-server</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container-fluid">
<h2 class="page-header">spring-oauth-server 数据库表说明
<small class="badge" title="Version">v3.0.0</small>
</h2>
<p class="text-muted">以下对<a target="_blank"
href="https://gitee.com/shengzhao/spring-oauth-server">spring-oauth-server</a>项目中的
<code>oauth.ddl</code> <code>initial_db.ddl</code>文件(位于/others/database目录)中的表字及段进行说明,
内容包括字段说明与使用场景等</p>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th style="width:10%;">表名</th>
<th style="width: 10%">字段名</th>
<th>字段类型</th>
<th>字段说明</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="17">oauth2_registered_client</td>
<td>id</td>
<td>varchar</td>
<td>主键,系统自动生成</td>
</tr>
<tr>
<td>archived</td>
<td>tinyint</td>
<td>
用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档).
<br/>
对该字段的具体使用请参考<code>CustomJdbcClientDetailsService.java</code>,在该类中,扩展了在查询client_details的SQL加上<em>archived
= 0</em>条件 (扩展字段)
</td>
</tr>
<tr>
<td>create_time</td>
<td>datetime</td>
<td>数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)</td>
</tr>
<tr>
<td>updated_time</td>
<td>timestamp</td>
<td>数据的最后更新时间,由数据库自行更新维护</td>
</tr>
<tr>
<td>client_id</td>
<td>varchar</td>
<td>
唯一,不能为空.
<br/>
用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成).
<br/>
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.
</td>
</tr>
<tr>
<td>client_id_issued_at</td>
<td>timestamp</td>
<td>client_id的签发时间, 默认为数据创建时间</td>
</tr>
<tr>
<td>client_secret</td>
<td>varchar</td>
<td>
用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成),加密保存.
<br/>
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.
</td>
</tr>
<tr>
<td>client_secret_expires_at</td>
<td>datetime</td>
<td>client_secret的过期时间,null表示永不过期</td>
</tr>
<tr>
<td>client_name</td>
<td>varchar</td>
<td>客户端(client)的名称,一般是一个有业务意义的名称</td>
</tr>
<tr>
<td>client_authentication_methods</td>
<td>varchar</td>
<td>认证支持的方式,多个由逗号分隔; 如: client_secret_basic,client_secret_post; 一般指认证时传递client_secret支持哪些方式</td>
</tr>
<tr>
<td>authorization_grant_types</td>
<td>varchar</td>
<td>
指定客户端支持的grant_type,可选值包括<em>authorization_code</em>,<em>urn:ietf:params:oauth:grant-type:device_code</em>,<em>refresh_token</em>,
<em>urn:ietf:params:oauth:grant-type:jwt-bearer</em>,<em>client_credentials</em>,
若支持多个grant_type用逗号(,)分隔,如: "authorization_code,refresh_token".
<br/>
在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有:
"authorization_code,refresh_token"(针对通过浏览器访问的客户端);
"client_credentials"(针对另一个服务端的场景,不需要用户参与).
<br/>
<em>urn:ietf:params:oauth:grant-type:device_code</em><em>urn:ietf:params:oauth:grant-type:jwt-bearer</em>是OAuth2.1中新增.
</td>
</tr>
<tr>
<td>redirect_uris</td>
<td>varchar</td>
<td>
OAuth2 认证后回调uri 一般传递code, 多个由逗号分隔;
可为空, 当grant_type为<code>authorization_code</code>时,
在OAuth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:
<ul>
<li>
当grant_type=<code>authorization_code</code>时, 第一步 <code>从 spring-oauth-server获取
'code'</code>时客户端发起请求时必须有<code>redirect_uri</code>参数, 该参数的值必须与
<code>web_server_redirect_uri</code>的值一致. 第二步 <code>用 'code' 换取 'access_token'</code>
时客户也必须传递相同的<code>redirect_uri</code>.
<br/>
在实际应用中, <em>redirect_uris</em>在注册时是必须填写的, 一般用来处理服务器返回的<code>code</code>,
验证<code>state</code>是否合法与通过<code>code</code>去换取<code>access_token</code>值.
<br/>
<a href="https://gitee.com/mkk/spring-oauth-client">spring-oauth-client</a>项目中,
可具体参考<code>AuthorizationCodeController.java</code>中的<code>authorizationCodeCallback</code>方法.
</li>
</ul>
</td>
</tr>
<tr>
<td>post_logout_redirect_uris</td>
<td>varchar</td>
<td> OAuth2 退出时 post 的客户端重定向 uri; 可选 多个由逗号分隔, 一般在client注册时可填写</td>
</tr>
<tr>
<td>scopes</td>
<td>varchar</td>
<td>
指定客户端申请的权限范围,可选值在OIDC协议中定义,
包括<em>openid</em>,<em>profile</em>,<em>email</em>,<em>address</em>,<em>phone</em>;若有多个值用逗号(,)分隔,如:
"openid,email".
<br/>
openid是必须有的,其他值若有则在获取的<code>id_token</code>中会包含对应的值.
<br/>
在实际应该中, 该值一般由服务端指定, 常用的值为<em>openid</em>.
</td>
</tr>
<tr>
<td>client_settings</td>
<td>varchar</td>
<td>
客户端的各类设置, 如是否支持PKCE用户授权(consent)确认是否必须等; 详见代码<code>ClientSettings.java</code>;
此字段存储JSON格式的数据值.
</td>
</tr>
<tr>
<td>token_settings</td>
<td>varchar</td>
<td>
对token的各类设置; 如 token有效期, refresh_token有效期等; 详见代码<code>TokenSettings.java</code>;
此字段存储JSON格式的数据值.
</td>
</tr>
<tr>
<td colspan="3">
<p class="text-info">
<em class="glyphicon glyphicon-info-sign"></em>
在项目中,主要操作<code>oauth2_registered_client</code>表的类是<code>ClientDetailsController.java</code>,
<code>OauthClientDetails.java</code>更多的细节请参考该类; 也可以根据实际的需要,去扩展或修改该类的实现.
</p>
</td>
</tr>
<!-- oauth2_authorization -->
<tr>
<td rowspan="35">oauth2_authorization</td>
<td>id</td>
<td>varchar</td>
<td>主键</td>
</tr>
<tr>
<td>registered_client_id</td>
<td>varchar</td>
<td>
外键, 关联<code>oauth2_registered_client</code>的id字段
</td>
</tr>
<tr>
<td>principal_name</td>
<td>varchar</td>
<td>认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段</td>
</tr>
<tr>
<td>authorization_grant_type</td>
<td>varchar</td>
<td>OAuth2的 grant_type 类型</td>
</tr>
<tr>
<td>authorized_scopes</td>
<td>varchar</td>
<td>此次授权的范围(scope)</td>
</tr>
<tr>
<td>attributes</td>
<td>blob</td>
<td>进行认证授权的各类信息,JSON格式</td>
</tr>
<tr>
<td>state</td>
<td>varchar</td>
<td>认证请求中传递的 state 值</td>
</tr>
<tr>
<td>authorization_code_value</td>
<td>blob</td>
<td><code>authorization_code</code>流程中的<em>code</em></td>
</tr>
<tr>
<td>authorization_code_issued_at</td>
<td>datetime</td>
<td><code>authorization_code</code>流程中的<em>code</em>签发时间</td>
</tr>
<tr>
<td>authorization_code_expires_at</td>
<td>datetime</td>
<td><code>authorization_code</code>流程中的<em>code</em>过期时间</td>
</tr>
<tr>
<td>authorization_code_metadata</td>
<td>blob</td>
<td><code>authorization_code</code>流程中的<em>code</em>的属性设置, 如值是否有效</td>
</tr>
<tr>
<td>access_token_value</td>
<td>blob</td>
<td>access_token 值</td>
</tr>
<tr>
<td>access_token_issued_at</td>
<td>datetime</td>
<td>access_token 签发时间</td>
</tr>
<tr>
<td>access_token_expires_at</td>
<td>datetime</td>
<td>access_token 过期时间</td>
</tr>
<tr>
<td>access_token_metadata</td>
<td>blob</td>
<td>access_token 属性设置, 如各类claims中的属性与值</td>
</tr>
<tr>
<td>access_token_type</td>
<td>varchar</td>
<td>access_token 类型, 一般是Bearer</td>
</tr>
<tr>
<td>access_token_scopes</td>
<td>varchar</td>
<td>此次授权的scope范围值,如: openid,profile</td>
</tr>
<tr>
<td>oidc_id_token_value</td>
<td>blob</td>
<td>OIDC中id_token 值</td>
</tr>
<tr>
<td>oidc_id_token_issued_at</td>
<td>datetime</td>
<td>id_token 签发时间</td>
</tr>
<tr>
<td>oidc_id_token_expires_at</td>
<td>datetime</td>
<td>id_token 过期时间</td>
</tr>
<tr>
<td>oidc_id_token_metadata</td>
<td>blob</td>
<td>id_token 属性设置, 如各类claims中的属性与值</td>
</tr>
<tr>
<td>refresh_token_value</td>
<td>blob</td>
<td>refresh_token 值</td>
</tr>
<tr>
<td>refresh_token_issued_at</td>
<td>datetime</td>
<td>refresh_token 签发时间</td>
</tr>
<tr>
<td>refresh_token_expires_at</td>
<td>datetime</td>
<td>refresh_token 过期时间</td>
</tr>
<tr>
<td>refresh_token_metadata</td>
<td>blob</td>
<td>refresh_token 属性设置, 如是否复用(reuse)</td>
</tr>
<tr>
<td>user_code_value</td>
<td>blob</td>
<td><code>device_code</code>流程中的user_code值</td>
</tr>
<tr>
<td>user_code_issued_at</td>
<td>datetime</td>
<td>user_code 签发时间</td>
</tr>
<tr>
<td>user_code_expires_at</td>
<td>datetime</td>
<td>user_code 过期时间</td>
</tr>
<tr>
<td>user_code_metadata</td>
<td>blob</td>
<td>user_code 属性设置, 如是否已经验证</td>
</tr>
<tr>
<td>device_code_value</td>
<td>blob</td>
<td><code>device_code</code>流程中的device_code值</td>
</tr>
<tr>
<td>device_code_issued_at</td>
<td>datetime</td>
<td>device_code 签发时间</td>
</tr>
<tr>
<td>device_code_expires_at</td>
<td>datetime</td>
<td>device_code 过期时间</td>
</tr>
<tr>
<td>device_code_metadata</td>
<td>blob</td>
<td>device_code 属性设置, 如是否已经验证</td>
</tr>
<tr>
<td>updated_time</td>
<td>timestamp</td>
<td>数据的最后修改时间, 由数据库自动维护更新</td>
</tr>
<tr>
<td colspan="3">
<p class="text-info">
<em class="glyphicon glyphicon-info-sign"></em> 该表用于存储在OAuth2.1授权过程中各类信息数据,
支持各类<code>grant_type</code>场景;
<code>oauth2_authorization</code>表的主要操作在<code>JdbcOAuth2AuthorizationService.java</code>类中,
更多的细节请参考该类.
<br/>
注意: 若对性能有要求, 此表的数据存储设计需要进行优化(如存redis或利用JWT特性简化一些不必要的存储字段).
</p>
</td>
</tr>
<!-- oauth2_authorization_consent -->
<tr>
<td rowspan="5">oauth2_authorization_consent</td>
<td>registered_client_id</td>
<td>varchar</td>
<td>外键, 关联<code>oauth2_registered_client</code>表的id字段</td>
</tr>
<tr>
<td>principal_name</td>
<td>varchar</td>
<td>认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段</td>
</tr>
<tr>
<td>authorities</td>
<td>varchar</td>
<td>授权确认过期中的属性, 如scope范围</td>
</tr>
<tr>
<td>updated_time</td>
<td>timestamp</td>
<td>数据的最后修改时间, 由数据库自动维护更新</td>
</tr>
<tr>
<td colspan="3">
<p class="text-info">
<em class="glyphicon glyphicon-info-sign"></em> 该表主要存储在授权过程中需要用户进行确认(consent)的信息;
在项目中,主要操作<code>oauth2_authorization_consent</code>表的对象是<code>JdbcOAuth2AuthorizationConsentService.java</code>.
更多的细节请参考该类.
</p>
</td>
</tr>
<!-- user_ -->
<tr>
<td rowspan="15">user_</td>
<td>id</td>
<td>int</td>
<td>主键, 自增长, 数据库自动生成</td>
</tr>
<tr>
<td>guid</td>
<td>varchar</td>
<td>唯一, 业务id</td>
</tr>
<tr>
<td>create_time</td>
<td>datetime</td>
<td>数据创建时间</td>
</tr>
<tr>
<td>updated_time</td>
<td>timestamp</td>
<td>数据的最后修改时间, 由数据库自动维护更新</td>
</tr>
<tr>
<td>username</td>
<td>varchar</td>
<td>用户名, 非空, 唯一</td>
</tr>
<tr>
<td>password</td>
<td>varchar</td>
<td>密码, 加密存储, 非空</td>
</tr>
<tr>
<td>enabled</td>
<td>tinyint</td>
<td>是否启用, 默认1(即启用)</td>
</tr>
<tr>
<td>phone</td>
<td>varchar</td>
<td>手机号</td>
</tr>
<tr>
<td>email</td>
<td>varchar</td>
<td>邮箱地址</td>
</tr>
<tr>
<td>address</td>
<td>varchar</td>
<td>个人地址</td>
</tr>
<tr>
<td>nickname</td>
<td>varchar</td>
<td>用户昵称, 别名</td>
</tr>
<tr>
<td>updated_at</td>
<td>int</td>
<td>最后数据更新时间值</td>
</tr>
<tr>
<td>default_user</td>
<td>tinyint</td>
<td>是否默认用户, 默认0(不是); 只用在初始化数据时使用</td>
</tr>
<tr>
<td>last_login_time</td>
<td>datetime</td>
<td>最后登录时间</td>
</tr>
<tr>
<td colspan="3">
<p class="text-info">
<em class="glyphicon glyphicon-info-sign"></em> 在项目中,主要使用<code>user_</code>表的对象是<code>UserServiceImpl.java</code>;
对应的实体是<code>User.java</code>;
在Spring Security中, 此表存储的数据对应<code>UserDetails.java</code>类.
</p>
</td>
</tr>
<!-- user_privilege -->
<tr>
<td rowspan="3">user_privilege</td>
<td>user_id</td>
<td>int</td>
<td>外键, 关联<code>user_</code>的id字段</td>
</tr>
<tr>
<td>privilege</td>
<td>varchar</td>
<td>权限值, 如: ROLE_USER</td>
</tr>
<tr>
<td colspan="3">
<p class="text-info">
<em class="glyphicon glyphicon-info-sign"></em> 此表存储用户的权限值, 一个用户可以有多个权限值.
</p>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<hr/>
<p class="text-muted">
&copy; 2013 - 2023 <a href="https://gitee.com/shengzhao/spring-oauth-server" target="_blank">spring-oauth-server</a>
</p>
</div>
</div>
</body>
</html>

View File

@ -1,12 +1,13 @@
使用的主要技术与版本号
*Spring-Boot (2.1.4.RELEASE)
*spring-security-oauth2 (2.3.5.RELEASE)
*Java (openjdk 17)
*Spring-Boot (3.1.2)
*spring-security-oauth2-authorization-server (1.1.1)
如何使用?
1.项目是Maven管理的, 需要本地安装maven(开发用的maven版本号为3.3.3), 还有MySql(开发用的mysql版本号为5.6)
1.项目是Maven管理的, 需要本地安装maven(开发用的maven版本号为3.6.0), 还有MySql(开发用的mysql版本号为5.7.22)
2.下载(或clone)项目到本地
@ -15,11 +16,9 @@
4.修改application.properties(位于src/resources目录)中的数据库连接信息(包括username, password等)
5.将本地项目导入到IDE(如Intellij IDEA)中,配置Tomcat(或类似的servlet运行服务器), 并启动Tomcat(默认端口为8080)
另: 也可通过maven package命令将项目编译为war文件(spring-oauth-server.war),
将war放在Tomcat中并启动(注意: 这种方式需要将application.properties加入到classpath中并正确配置数据库连接信息).
5.将本地项目导入到IDE(如Intellij IDEA)中,直接运行 SpringOauthServerApplication.java (默认端口为8080)
6.参考oauth_test.txt(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080/spring-oauth-server).
6.参考oauth2.1-flow.md(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080).
7. 运行单元测试时请先创建数据库 oauth2_boot_test, 并依次运行SQL脚本.
运行脚本的顺序: initial_db.ddl -> oauth.ddl

308
others/oauth2.1-flow.md Normal file
View File

@ -0,0 +1,308 @@
v3.0.0+ used
## authorization_code flow
Core-Class: OAuth2AuthorizationEndpointFilter
1. start authorize
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=client11&scope=openid&redirect_uri=http://localhost:8083/oauth2/callback&state=93820ss0-32p
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=client11&scope=openid profile&redirect_uri=http://localhost:8083/oauth2/callback&state=93820ss0-32p
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=client11&scope=openid profile email&redirect_uri=http://localhost:8083/oauth2/callback&state=93820ss0-32p
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=client11&scope=openid profile phone&redirect_uri=http://localhost:8083/oauth2/callback&state=93820ss0-32p
2. response code
http://localhost:8083/oauth2/callback?code=-VEnyAcEflDxjMh4Hr-6YejZq4Mel5gihFy_FMyotDxLhILeMBQheJkL4mdJ0sKD_C8xpa_sMNGf_I2tYJIVki8a4ktT2QsHojhbV3HpbGLVhJ0qDc8kfXjWt7u_24QO&state=93820ss0-32p
3. get access_token
- Core-Class: OAuth2TokenEndpointFilter
- URL: http://localhost:8080/oauth2/token [POST]
- cURL
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="client11"' \
--form 'grant_type="authorization_code"' \
--form 'redirect_uri="http://localhost:8083/oauth2/callback"' \
--form 'code="-VEnyAcEflDxjMh4Hr-6YejZq4Mel5gihFy_FMyotDxLhILeMBQheJkL4mdJ0sKD_C8xpa_sMNGf_I2tYJIVki8a4ktT2QsHojhbV3HpbGLVhJ0qDc8kfXjWt7u_24QO"' \
--form 'client_secret="secret22"'
response
{
"access_token": "7154afT_cxvLDq1naSg6Aq9ueSFSW8xRr5txryW5MlddRe7nV0RogTYwPsJc_rrRqwaIvLleerLhkjtIN2E2U-4J_BzvYNCsv8BVLqeerCObwgwpP3t__NMMUakzRL2i",
"refresh_token": "TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr",
"scope": "openid profile",
"id_token": "eyJraWQiOiJzb3MtZWNjLWtpZDEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1bml0eSIsImF1ZCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsInVwZGF0ZWRfYXQiOiIiLCJhenAiOiI2dXJOTGdSNm9zazJFNTZla3AiLCJhdXRoX3RpbWUiOjE2OTc3MDczNTQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MCIsIm5pY2tuYW1lIjoiIiwiZXhwIjoxNjk3NzA5MjA4LCJpYXQiOjE2OTc3MDc0MDgsImp0aSI6IjEyNTc0MjU2NTk4MDI2ODY2NzI3NDAwMTMxNjk5NDk0Iiwic2lkIjoidXdwN255RnJwdlNtWmlQS2hCdWVSVFZfcVRKYkN6ZjAyTmYwQTZGN1lrSSJ9.3w-7EY9SwKA-UkXlhDfD2BbSwP6nCSLZxNgKwhkkMY8YPbMkygbj374SmEmsit7NlpRXHCtW6ULZ9_IVZ9MTBg",
"token_type": "Bearer",
"expires_in": 3599
}
4. refresh access_token
- Core-Class: OAuth2TokenEndpointFilter
- URL: http://localhost:8080/oauth2/token [POST]
- cURL
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'grant_type="refresh_token"' \
--form 'refresh_token="TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr"'
response
{
"access_token": "YnVdTXl0MhslsrOjiz1ffSixvPnWCN-XS-UBlkS89daZbd_TvXtSSo_ODuFVWPWw1KsO5WQykVPjwSe_Kreo8ngIP9DglaXJMbYJJu4Wa6_geOINj5ksmnbfb6pHrQHr",
"refresh_token": "TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr",
"scope": "openid profile",
"id_token": "eyJraWQiOiJzb3MtZWNjLWtpZDEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1bml0eSIsImF1ZCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsInVwZGF0ZWRfYXQiOjAsImF6cCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsImF1dGhfdGltZSI6MTY5NzcwNzM1NCwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwibmlja25hbWUiOiIiLCJleHAiOjE2OTc3MjQyNjMsImlhdCI6MTY5NzcyMjQ2MywianRpIjoiMDc4OTc4MTUxNzEwNTgwNDE2ODY0NzgxMDQ1OTM5MDYiLCJzaWQiOiJ1d3A3bnlGcnB2U21aaVBLaEJ1ZVJUVl9xVEpiQ3pmMDJOZjBBNkY3WWtJIn0.j0KVv7bAi85zbX-0wvWe83n_CQdmJLGrHJNFwF5jA1-wa8QzaSwJbznpjbHLGTv-UbI2YeHLn8N5iGXDarbC9Q",
"token_type": "Bearer",
"expires_in": 3599
}
5. get userinfo
- Core-Class: OidcUserInfoEndpointFilter
- URLhttp://localhost:8080/userinfo
- cURL
curl --location 'http://localhost:8080/userinfo' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJraWQiOiJteW9pZGMta2V5aWQiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDExIiwibmJmIjoxNjkyMDg0OTQ2LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODc4MSIsImV4cCI6MTY5MjA5MjE0NiwiaWF0IjoxNjkyMDg0OTQ2LCJqdGkiOiJkMDI0NTNhNS0xNmRmLTRiZGYtOTBhMS1lOGYyYjMxOWY5YzMifQ.hvVjgkGHsmDfFZia-B4H1D3vo03Yuj0Kd2KvF-EGuS9BzZTzvee8XetiRO-C6mqRw1s-Wa6wZB4QwB9-WyLc7tpu0TgfKDDn71nJQNZ2QgzcNIUlclxG5K21mVMmrA-c4Le5HGPLWsGItDkpqA1OtgL4U622kGHrf0RJCmpC_WxPnECYsI84dgILE6n9s27UZQhYtYLiq5aoovvHImrztTClRmNTwc4iB9RX_gpb9YFs0diMWvIBgDokEAJE_K9BY0HZqpqj7T1ilecfbcv_T2Ebd8JnnZyCTUcpIyZ4DlWqzvnEp70cz945NuaYQG-_VPSjhGiymsNxWkP0HMGRuQ' \
response
{
"sub": "admin",
"updated_at": "123456990",
"nickname": "xxx"
}
## client_credentials flow
- URL: http://localhost:8080/oauth2/token [POST]
- cURL
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'grant_type="client_credentials"' \
--form 'scope="openid profile"'
response
{
"access_token": "p2i1WHiiFBCgTJFTs63OvO9-bclB9DbsgsebDo_ntMw_BAleu2RzIQzzFfaaJAR5oiL3xwN3xMyNTRZSrXM_1ANycleysPU5l3xuZ0aQX4V-Va178qg6e-PvLqLBsD_i",
"scope": "openid profile",
"token_type": "Bearer",
"expires_in": 3599
}
## authorization_code + PKCE flow
Proof Key for Code Exchange (RFC7636)
1. start authorize
http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=client11&scope=openid profile&redirect_uri=http://localhost:8083/oauth2/callback&state=state9990988&code_challenge=HNxPXD6eoV_3eEWmd7Oktz_sYDRkgwUV39DAY97pmPc&code_challenge_method=S256
2. response code
http://localhost:8083/oauth2/callback?code=Laulaadi78kB0DkQKvCPv96KMk56s8NQjwA3lJ_IagKn1u3x-5jrTBATu_5rZDLsXq89Lp4nNjAqYMnQjohz8WFV5Ql9R0Bj46w7yYkT8hfTEEGkHYxJC8K3Qf6_riF0&state=state9990988
3. get access_token
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="client11"' \
--form 'grant_type="authorization_code"' \
--form 'redirect_uri="http://localhost:8083/oauth2/callback"' \
--form 'code="Laulaadi78kB0DkQKvCPv96KMk56s8NQjwA3lJ_IagKn1u3x-5jrTBATu_5rZDLsXq89Lp4nNjAqYMnQjohz8WFV5Ql9R0Bj46w7yYkT8hfTEEGkHYxJC8K3Qf6_riF0"' \
--form 'client_secret="secret22"' \
--form 'code_verifier="OXhHcFQ5TWIzSTdBUGJ0RlBZZm5xUEN2QnIzSkpyTXFCOVlSMHFBd2ZCSmhjZ1FK"'
response
{
"access_token": "eyJraWQiOiJteW9pZGMta2V5aWQiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDExIiwibmJmIjoxNjkyNzYyNjA5LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIl0sImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODc4MSIsImV4cCI6MTY5Mjc2OTgwOSwiaWF0IjoxNjkyNzYyNjA5LCJqdGkiOiJkNmRlZGVmNi1lYmFhLTRjOTEtYjhjZC1kM2QxZGQ2OTIzNzEifQ.Fuuu9jI1uXEevvJswgqvsyR0PZkvn8ijYX3PjDhJj4_t_L0U0DbWTJNr8-dQWVA2AuIjlLs_5SsI8mq_sZOfZc8TBZRhJYbSiluLoNKxaHTHfMimY0Zb712x2mZ9NS_DzEPJeNLTTxvm0X7mmLgoXdc2hYSEbXVYicIGaidIBy6rFaSMyA5bdmSoI3gfwW2PQ58NBHDQDkEZmWmLZ6ZkLKGANzSpWUmraA7lhV_UphmHqk55kcgqEWQKNqD3x6OZ20jpUgtrkr6TjbtFmjMOYV7r0_jMGihmPSjXoXYspDcrS9T9fE9oW7_rSe1YUnQaR9s5ghkqFCki7WS7Tnzj-w",
"refresh_token": "VWbIs3Ls2pAZknHSXGV5oH_VHNQwoiWmSDQi0UbQesApSWR1xpYB2Ggyct4iCzITKE5STJEbRPKZUTJNvuFfWFv3rgJYD4ggZ0nHnkQ3GQ_a471DxWU--smzwRpb4vxx",
"scope": "openid profile",
"id_token": "eyJraWQiOiJlY2Mta2lkLTEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDExIiwidXBkYXRlZF9hdCI6IjEyMzQ1Njk5MCIsImF6cCI6ImNsaWVudDExIiwiYXV0aF90aW1lIjoxNjkyNzYyNTQ2LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg3ODEiLCJuaWNrbmFtZSI6Inh4eCIsImV4cCI6MTY5Mjc2NDQwOSwiaWF0IjoxNjkyNzYyNjA5LCJqdGkiOiJkZDM2ZGEyNy1lYTI4LTRlM2YtOTk5My01NDgyNzI0ZmE5NWUiLCJzaWQiOiJZZWNCLUo2Xy14Nlo0YnZiOW43RGIweDJIYy12bk5VWVpoSGNjNUVfM293In0.cT7k6P8IQNpGHiX4B1GB4wDxOUltvWM0PlyLWDQLk5tD3gnU-JvaGre2QeJBUeYLyZG17iZQWvfAxMAFpSolFQ",
"token_type": "Bearer",
"expires_in": 7199
}
## DEVICE_CODE flow
Core-Class: OAuth2DeviceAuthorizationEndpointFilter
1. device call device_authorization
curl --location 'http://localhost:8080/oauth2/device_authorization' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'scope="openid profile"'
response
{
"user_code": "PCKJ-FWZS",
"device_code": "ZPMq2sfyHPj_pJ78T6J4yGcsAAi_XbuBjtQz2NLxYWKDHbcqUhg2nFHe3Ynp3V1SyCOwYEoaz9lPvqt-oj0sXKxJDnC5usJmANVqMQ-8Qjpp1ROi9gljdQY2NO3YYvIo",
"verification_uri_complete": "http://127.0.0.1:8080/oauth2/device_verification?user_code=PCKJ-FWZS",
"verification_uri": "http://127.0.0.1:8080/oauth2/device_verification",
"expires_in": 300
}
2. Logged user visit verification_uri_complete using a browser (or another authorized device use QR and so on)
http://localhost:8080/oauth2/device_verification
then type user_code and submit the form
Core-Class: OAuth2DeviceVerificationEndpointFilter
3. device get token
request
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="client11"' \
--form 'grant_type="urn:ietf:params:oauth:grant-type:device_code"' \
--form 'client_secret="secret22"' \
--form 'device_code="voqSMpNJAvVlMBQ1_R65a_MMWD344YKQqrlo86JG-VeFRz6iCMdhn5VBLwbNoHaidP9db33BJDaLWHHtpEP98NpwEf9wre_X-o8kq1_Dg8aj0r9lRP5aH-ZNI8wpon6b"'
response [200]
{
"access_token": "QqPGuiF9c2HKYQEdxrs9E0WsRijEl_z9sINI6CFD5yMulXaZutLTktVtLP3zcr22XuYJOzWZMzOgvjWl2tqAoMo3S2MHBgxjPmx5gfr6DjeQPsW3fFPVc6pOa5Ll6u4S",
"refresh_token": "7vtQtkU95tjt7nkaX8DZnDVntrgPYIoXB6_4WsV9FzMi-ppoPB_H5qmufi4EHqAuJPwdlxXYdDbVYoGudXd0iCPfmqT5B8CcW7zRsgaKQOHQlPw9Ju3wMGNSRk14YRWI",
"scope": "profile",
"token_type": "Bearer",
"expires_in": 3599
}
or [400]
{
"error": "authorization_pending",
"error_uri": "https://datatracker.ietf.org/doc/html/rfc8628#section-3.5"
}
## JWT_BEARER flow
- Core-Class: JwtClientAssertionAuthenticationProvider
- URL: http://localhost:8080/oauth2/token
- grant_type=authorization_code
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="vLIXDF9GXg6Psfh1uzwVFUj0fucX2Zn9"' \
--form 'client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"' \
--form 'scope="openid"' \
--form 'grant_type="authorization_code"' \
--form 'client_assertion="eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ2TElYREY5R1hnNlBzZmgxdXp3VkZVajBmdWNYMlpuOSIsInN1YiI6InZMSVhERjlHWGc2UHNmaDF1endWRlVqMGZ1Y1gyWm45IiwiYXVkIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNjk4MTE5NjMxfQ.-40zh9Sao9JzP4_eYVnIpreuk76Nql4ue3hNuyhu59c"' \
--form 'code="CyN4YB2Y9p8y1lqfUQc0_jxbuL0spqP8pC8vriwzwKP4AQhtYriMVF-obChcf83rwLILZP8z-uSVKcS-eGvZPE-vTM-LbiMXic0tXW1fzWfYd0r7ijGapX1Nnho3-XWn"' \
--form 'redirect_uri="https://andaily.com/oauth2/callback"'
- grant_type=client_credentials
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn"' \
--form 'client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"' \
--form 'scope="openid"' \
--form 'grant_type="client_credentials"' \
--form 'client_assertion="eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkb2ZPeDZoanhsV3c5cWUyYm5GdnFiaVBodVd3R1dkbiIsInN1YiI6ImRvZk94NmhqeGxXdzlxZTJibkZ2cWJpUGh1V3dHV2RuIiwiYXVkIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNjk4MzI4NDI0fQ.A-CMlBoOqtlWVQiu8RjK9xWKG4lqBMT7IMCVIDJc3hsSZk7KvApL2lPx3k2b9bDM8Ysr7VXnFPfQbN8RN4sTsf2x-cpzDQ-vFBGMFqgaXZckuba21moT42GWyTULQ2_HRYy8bLCfOiX7BG4HyJYHf2JDrZgQ3pPu3VhH5D9bJ5_y6WcZxDlVMBUMXGRuhwl0tCTc8L0Ss3azPD82wMblDavCUTxNzOvb0qc3orVEjgUW77cxzGi929TtWtCvBH8dyNh_CAsvYJKAJDskTnLKv6GihL33pNHBhfjwSUP2s-_LPD6Z7gjf9GJHSSz7TeztX3NU9-FaoJZjYGR2lq2F2A"' \
--form 'client_secret="dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn"'
## revoke token API
Core-Class: OAuth2TokenRevocationEndpointFilter
URL: http://localhost:8080/oauth2/revoke
curl --location 'http://localhost:8080/oauth2/revoke' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'token="TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr"'
response
200 [HTTP]
## introspect token API
Core-Class: OAuth2TokenIntrospectionEndpointFilter
URL: http://localhost:8080/oauth2/introspect
curl --location 'http://localhost:8080/oauth2/introspect' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'token="GaHu88XEEAz41xMHfDk05bg9uSJ5Go1RF6jOe5eX7OhHD_52NK_fuwvVWq_dTRIhK8WR9SnCAtBBc0fVsOyGgz8-MhmVTG-dcDi6QtGQQtYxwmGrD-fOhpmePdUv6pwV"'
response
{
"active": true,
"sub": "admin",
"aud": [
"6urNLgR6osk2E56ekp"
],
"nbf": 1697721873,
"scope": "openid profile",
"iss": "http://127.0.0.1:8080",
"exp": 1697725474,
"iat": 1697721874,
"jti": "a1aa8f82-c885-45b3-a469-c2f595e8f12d",
"client_id": "6urNLgR6osk2E56ekp",
"token_type": "Bearer"
}
## logout token API
Core-Class: OidcLogoutEndpointFilter
URL: http://localhost:8080/connect/logout?id_token_hint=${id_token}&client_id={client_id}&post_logout_redirect_uri=${post_logout_redirect_uri}&state=${state}
## .well-known URL
### OIDC 1.0
- URL: http://localhost:8080/.well-known/openid-configuration
- Core-Class: OidcProviderConfigurationEndpointFilter
- Response: {"issuer":"http://localhost:8080","authorization_endpoint":"http://localhost:8080/oauth2/authorize","device_authorization_endpoint":"http://localhost:8080/oauth2/device_authorization","token_endpoint":"http://localhost:8080/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"jwks_uri":"http://localhost:8080/oauth2/jwks","userinfo_endpoint":"http://localhost:8080/userinfo","end_session_endpoint":"http://localhost:8080/connect/logout","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","urn:ietf:params:oauth:grant-type:device_code"],"revocation_endpoint":"http://localhost:8080/oauth2/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"introspection_endpoint":"http://localhost:8080/oauth2/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}
### OAuth 2.1
- URL: http://localhost:8080/.well-known/oauth-authorization-server
- Core-Class: OAuth2AuthorizationServerMetadataEndpointFilter
- Response: {"issuer":"http://localhost:8080","authorization_endpoint":"http://localhost:8080/oauth2/authorize","device_authorization_endpoint":"http://localhost:8080/oauth2/device_authorization","token_endpoint":"http://localhost:8080/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"jwks_uri":"http://localhost:8080/oauth2/jwks","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","urn:ietf:params:oauth:grant-type:device_code"],"revocation_endpoint":"http://localhost:8080/oauth2/revoke","revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"introspection_endpoint":"http://localhost:8080/oauth2/introspect","introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","client_secret_jwt","private_key_jwt"],"code_challenge_methods_supported":["S256"]}
## jwks URL
- URL: http://localhost:8080/oauth2/jwks
- Core-Class: NimbusJwkSetEndpointFilter
- Response: {"keys":[{"kty":"EC","crv":"P-256","kid":"sos-ecc-kid1","key_ops":["sign","deriveKey","decrypt","encrypt","verify"],"x":"UyCuPXhC0_KLRqfWPNDU4ZljSx7lQ_vP7VbYDiOZmsk","y":"2HuQhn3bfkmYiB6BLQKlN8tkI8awkeOiKaNk1cu06ow","alg":"ES256"},{"kty":"RSA","e":"AQAB","kid":"sos-rsa-kid2","key_ops":["deriveKey","verify","encrypt","decrypt","sign"],"alg":"RS256","n":"st2IswiZyQXHy86KBYQdEYv3sAfWpyx-e4o0Dcqvpck0E1FpZfVcFzbLy9B7YHvXv1SseVcg93iiNYgGlPDeZxPllz4-oIisDvSmEJdAidhqQxxpMeSjeQzvVu4CKjGFG9jA68pTm-KDia3Y516b4tPyKhHGIUZq2yJrNIs2QjTikYbn5AxAQ244cDPTsuEV5yqdOdyWvdlrn4WSFLiPt31MboT6et7Hmm90fwbMDSaWWb2XNo2gOnzWFwlNO2s8zK_Z1IWhmreb_XH5mW9xirrT03nbnLTLcmLtZYHFKjP55zRFDgKsXeo9BQNG3dkCsWz0N8pURaN6cuXYoYGU7Q"}]}
---
## reference doc
https://springdoc.cn/spring-authorization-server/index.html
https://developer.aliyun.com/article/1050110
[jwt-bearer] https://developer.atlassian.com/cloud/jira/software/user-impersonation-for-connect-apps/
在线PKCE生成工具
1. PKCEUtils.java
2. https://tonyxu-io.github.io/pkce-generator/

View File

@ -1,4 +1,6 @@
适用范围v3.0.0 之前的版本。
方式1:基于浏览器 (访问时后跳到登录页面,登录成功后跳转到redirect_uri指定的地址) [GET]
说明:只能使用admin或unity 账号登录才能有权限访问,若使用mobile账号登录将返回Access is denied
http://localhost:8080/oauth/authorize?client_id=unity-client&redirect_uri=http%3a%2f%2flocalhost%3a8080%2fspring-oauth-server%2funity%2fdashboard&response_type=code&scope=read

396
pom.xml
View File

@ -1,205 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.monkeyk</groupId>
<artifactId>spring-oauth-server</artifactId>
<version>2.1.0</version>
<packaging>war</packaging>
<name>${project.artifactId}</name>
<description>Spring OAuth Server (Spring Boot)</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring.security.oauth.version>2.3.8.RELEASE</spring.security.oauth.version>
<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
<test.skip>false</test.skip>
</properties>
<dependencies>
<!-- Provided -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- OAuth2-->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring.security.oauth.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring.security.jwt.version}</version>
</dependency>
<!--Redis-->
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-data-redis</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.3.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--HikariCP-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.sitemesh</groupId>
<artifactId>sitemesh</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!--Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warSourceExcludes>*/classes/application.properties</warSourceExcludes>
<!-- <packagingExcludes>*/classes/application.properties</packagingExcludes>-->
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Implementation-BuildNumber>${project.version}</Implementation-BuildNumber>
<Implementation-Title>spring-oauth-server(boot)</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-URL>http://monkeyk.com</Implementation-URL>
<Implementation-Vendor>Not Vendor Yet, Inc.</Implementation-Vendor>
</manifestEntries>
<manifest>
<!--<mainClass>${start-class}</mainClass>-->
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>${test.skip}</skip>
<forkMode>none</forkMode>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
<developers>
<developer>
<name>shengzhao</name>
<email>shengzhao@shengzhaoli.com</email>
</developer>
<!--more developer-->
</developers>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.monkeyk</groupId>
<artifactId>spring-oauth-server</artifactId>
<version>3.0.0</version>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>Spring OAuth Server (Spring Boot)</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<test.skip>false</test.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--Redis-->
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-data-redis</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--HikariCP-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- unit test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- spring-doc since v3.0.0 -->
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<attributes>
<project-version>${project.version}</project-version>
<project-id>${project.artifactId}</project-id>
</attributes>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Implementation-BuildNumber>${project.version}</Implementation-BuildNumber>
<Implementation-Title>spring-oauth-server(boot)</Implementation-Title>
<Implementation-Version>${project.version}</Implementation-Version>
<Implementation-URL>https://monkeyk.com</Implementation-URL>
<Implementation-Vendor>CloudJac, Inc.</Implementation-Vendor>
</manifestEntries>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>${test.skip}</skip>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
<developers>
<developer>
<name>shengzhao</name>
<email>shengzhao@shengzhaoli.com</email>
</developer>
<!--more developer-->
</developers>
</project>

View File

@ -0,0 +1,22 @@
= {project-id} API Docs
:toc: left
:toc-title: Contents
:revnumber: {project-version}
// == 应用版本API
// .http-request
// include::{snippets}/UnityControllerTest/version/http-request.adoc[]
// .curl-request
// include::{snippets}/UnityControllerTest/version/curl-request.adoc[]
// .request-body
// include::{snippets}/UnityControllerTest/version/request-body.adoc[]
// .http-response
// include::{snippets}/UnityControllerTest/version/http-response.adoc[]
// .response-body
// include::{snippets}/UnityControllerTest/version/response-body.adoc[]
== Unity resource API
operation::UnityControllerTest/userInfo[]

View File

@ -8,12 +8,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* 2017-12-05
*
* @author Shengzhao Li
* @since 1.0.0
*/
@SpringBootApplication
public class SpringOauthServerApplication {
/**
* main
* others/how_to_use.txt
*
* @param args args

View File

@ -1,31 +0,0 @@
package com.monkeyk.sos;
import com.monkeyk.sos.web.WebUtils;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
/**
* 2017-12-05
*
* @author Shengzhao Li
*/
public class SpringOauthServerServletInitializer extends SpringBootServletInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//主版本号
servletContext.setAttribute("mainVersion", WebUtils.VERSION);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringOauthServerApplication.class);
}
}

View File

@ -1,92 +0,0 @@
package com.monkeyk.sos.config;
import com.monkeyk.sos.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 2020/6/9
* <p>
* <p>
* JWT TokenStore config
* <p>
* 使
* <pre>sos.token.store=jwt</pre>
*
* @author Shengzhao Li
* @since 2.1.0
*/
@Configuration
@ConditionalOnProperty(name = "sos.token.store", havingValue = "jwt")
public class JWTTokenStoreConfiguration {
/**
* jwtKey
* <p>
* HMAC key, default: IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa
* alg: HMACSHA256
*/
@Value("${sos.token.store.jwt.key:IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa}")
private String jwtKey;
/**
* 使 refresh_token true
*
* @since 2.1.0
*/
@Value("${sos.reuse.refresh-token:true}")
private boolean reuseRefreshToken;
@Bean
public JwtAccessTokenConverter accessTokenConverter(UserService userService) {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
DefaultUserAuthenticationConverter userAuthenticationConverter = new DefaultUserAuthenticationConverter();
userAuthenticationConverter.setUserDetailsService(userService);
// userAuthenticationConverter.setDefaultAuthorities(new String[]{"USER"});
tokenConverter.setUserTokenConverter(userAuthenticationConverter);
tokenConverter.setIncludeGrantType(true);
// tokenConverter.setScopeAttribute("_scope");
jwtAccessTokenConverter.setAccessTokenConverter(tokenConverter);
jwtAccessTokenConverter.setSigningKey(this.jwtKey);
return jwtAccessTokenConverter;
}
/**
* JWT TokenStore
*
* @since 2.1.0
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public DefaultTokenServices tokenServices(TokenStore tokenStore, JwtAccessTokenConverter tokenEnhancer, ClientDetailsService clientDetailsService) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setClientDetailsService(clientDetailsService);
//support refresh token
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenEnhancer(tokenEnhancer);
tokenServices.setReuseRefreshToken(this.reuseRefreshToken);
return tokenServices;
}
}

View File

@ -1,58 +0,0 @@
package com.monkeyk.sos.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* 2020/6/9
* <p>
* <p>
* JDBC TokenStore config
* 使
* <pre>sos.token.store=jdbc</pre> ()
*
* @author Shengzhao Li
* @since 2.1.0
*/
@Configuration
@ConditionalOnProperty(name = "sos.token.store", havingValue = "jdbc", matchIfMissing = true)
public class JdbcTokenStoreConfiguration {
/**
* 使 refresh_token true
*
* @since 2.1.0
*/
@Value("${sos.reuse.refresh-token:true}")
private boolean reuseRefreshToken;
/**
* JDBC TokenStore
*/
@Bean
public TokenStore tokenStore(DataSource dataSource) {
return new JdbcTokenStore(dataSource);
}
@Bean
public DefaultTokenServices tokenServices(TokenStore tokenStore, ClientDetailsService clientDetailsService) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setClientDetailsService(clientDetailsService);
//support refresh token
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(this.reuseRefreshToken);
return tokenServices;
}
}

View File

@ -1,9 +1,8 @@
package com.monkeyk.sos.config;
import com.monkeyk.sos.web.WebUtils;
import com.monkeyk.sos.web.filter.CharacterEncodingIPFilter;
import com.monkeyk.sos.web.filter.SOSSiteMeshFilter;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -12,8 +11,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
@ -23,6 +21,7 @@ import java.util.List;
* <p>
*
* @author Shengzhao Li
* @since 2.0.0
*/
@Configuration
public class MVCConfiguration implements WebMvcConfigurer {
@ -45,7 +44,7 @@ public class MVCConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
WebMvcConfigurer.super.configureMessageConverters(converters);
converters.add(new StringHttpMessageConverter(Charset.forName(WebUtils.UTF_8)));
converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
@ -53,7 +52,7 @@ public class MVCConfiguration implements WebMvcConfigurer {
* UTF-8
*/
@Bean
public FilterRegistrationBean encodingFilter() {
public FilterRegistrationBean<Filter> encodingFilter() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CharacterEncodingIPFilter());
registrationBean.addUrlPatterns("/*");
@ -63,18 +62,4 @@ public class MVCConfiguration implements WebMvcConfigurer {
}
/**
* sitemesh filter
*/
@Bean
public FilterRegistrationBean sitemesh() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SOSSiteMeshFilter());
registrationBean.addUrlPatterns("/*");
//注意: 在 spring security filter之后
registrationBean.setOrder(8899);
return registrationBean;
}
}

View File

@ -1,26 +0,0 @@
package com.monkeyk.sos.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
/**
* 2018/3/22
*
* #oauth2 #oauth2.hasScope('read')
*
* @author Shengzhao Li
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class OAuth2MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}

View File

@ -1,35 +1,45 @@
package com.monkeyk.sos.config;
import com.monkeyk.sos.domain.oauth.CustomJdbcClientDetailsService;
import com.monkeyk.sos.service.OauthService;
import com.monkeyk.sos.service.UserService;
import com.monkeyk.sos.web.oauth.OauthUserApprovalHandler;
import com.monkeyk.sos.domain.oauth.ClaimsOAuth2TokenCustomizer;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.function.Consumer;
import static com.monkeyk.sos.domain.shared.SOSConstants.CUSTOM_CONSENT_PAGE_URI;
/**
* 2018/2/8
@ -43,177 +53,183 @@ import javax.sql.DataSource;
public class OAuth2ServerConfiguration {
/*Fixed, resource-id */
/**
* Fixed, resource-id
*
* @deprecated Not used from v3.0.0
*/
public static final String RESOURCE_ID = "sos-resource";
/**
* // unity resource
* UNITY 访
* keystore file name
*
* @since 3.0.0
*/
@Configuration
@EnableResourceServer
protected static class UnityResourceServerConfiguration extends ResourceServerConfigurerAdapter {
public static String KEYSTORE_NAME = "jwks.json";
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(false);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
/**
* @since 3.0.0
*/
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* @since 3.0.0
*/
private AuthenticationManager authenticationManagerOAuth2;
/**
* authorizationServerSecurityFilterChain
*
* @since 3.0.0
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.sessionManagement(sessionManagementConfigurer -> {
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
});
http.authorizeHttpRequests(registry -> {
registry
// 所有以 /unity/ 开头的 URL属于此资源
.requestMatchers().antMatchers("/unity/**")
.and()
.authorizeRequests()
.antMatchers("/unity/**").access("#oauth2.hasScope('read') and hasRole('UNITY')");
.requestMatchers("/unity/**").hasAnyRole("UNITY")
// 所有以 /m/ 开头的 URL属于此资源
.requestMatchers("/m/**").hasAnyRole("MOBILE");
});
}
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint ->
// deviceAuthorizationEndpoint.verificationUri("/activate")
// )
.deviceVerificationEndpoint(deviceVerificationEndpoint ->
deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)
)
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
// Enable OpenID Connect 1.0
.oidc(oidcConfigurer -> {
oidcConfigurer.providerConfigurationEndpoint(endpointConfigurer -> {
//扩展oidc默认能力
endpointConfigurer.providerConfigurationCustomizer(oidcProviderConfigurationCustomizer());
});
});
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer(oauth2ResourceServer ->
//ext jwt
oauth2ResourceServer.jwt(Customizer.withDefaults()));
DefaultSecurityFilterChain filterChain = http.build();
this.authenticationManagerOAuth2 = http.getSharedObject(AuthenticationManager.class);
return filterChain;
}
/**
* // mobile resource
* MOBILE 访
* OAuth2 AuthenticationManager
*
* @return AuthenticationManager
* @since 3.0.0
*/
@Configuration
@EnableResourceServer
protected static class MobileResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(false);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
// Since we want the protected resources to be accessible in the UI as well we need
// session creation to be allowed (it's disabled by default in 2.0.6)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
// 所有以 /m/ 开头的 URL属于此资源
.requestMatchers().antMatchers("/m/**")
.and()
.authorizeRequests()
.antMatchers("/m/**").access("#oauth2.hasScope('read') and hasRole('MOBILE')");
}
public AuthenticationManager authenticationManagerOAuth2() {
return authenticationManagerOAuth2;
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
/**
* oidc
*
* @since 3.0.0
*/
private Consumer<OidcProviderConfiguration.Builder> oidcProviderConfigurationCustomizer() {
return builder -> {
builder.idTokenSigningAlgorithms(strings -> {
strings.add(SignatureAlgorithm.ES256.getName());
}).grantTypes(grantTypes -> {
//向下兼容添加v3.0.0
// grantTypes.add(AuthorizationGrantType.PASSWORD.getValue());
grantTypes.add(AuthorizationGrantType.JWT_BEARER.getValue());
})
.scopes(strings -> {
strings.add(OidcScopes.PROFILE);
strings.add(OidcScopes.EMAIL);
strings.add(OidcScopes.ADDRESS);
strings.add(OidcScopes.PHONE);
});
};
}
@Autowired
private TokenStore tokenStore;
/**
*
*
* @return RegisteredClientRepository
* @since 3.0.0
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
// return new InMemoryRegisteredClientRepository(client);
return new JdbcRegisteredClientRepository(this.jdbcTemplate);
}
@Autowired
private DefaultTokenServices tokenServices;
/**
* , jdbc
*
* @since 3.0.0
*/
@Bean
public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(this.jdbcTemplate, registeredClientRepository);
}
@Autowired
private ClientDetailsService clientDetailsService;
/**
* , jdbc
*
* @since 3.0.0
*/
@Bean
public OAuth2AuthorizationService oAuth2AuthorizationService(RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(this.jdbcTemplate, registeredClientRepository);
}
@Autowired
private OauthService oauthService;
/**
* / source
* key, 使
*
* @return JWKSource
* @since 3.0.0
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws IOException {
Resource resource = new ClassPathResource(KEYSTORE_NAME);
return JWKSourceBuilder.create(resource.getURL()).build();
}
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private UserService userDetailsService;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
// /*
// * JDBC TokenStore
// */
// @Bean
// public TokenStore tokenStore(DataSource dataSource) {
// return new JdbcTokenStore(dataSource);
// }
/*
* Redis TokenStore (Redis使)
*/
// @Bean
// public TokenStore tokenStore(RedisConnectionFactory connectionFactory) {
// final RedisTokenStore redisTokenStore = new RedisTokenStore(connectionFactory);
// //prefix
// redisTokenStore.setPrefix(RESOURCE_ID);
// return redisTokenStore;
// }
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
return new CustomJdbcClientDetailsService(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(tokenServices)
.tokenStore(tokenStore)
.authorizationCodeServices(authorizationCodeServices)
.userDetailsService(userDetailsService)
.userApprovalHandler(userApprovalHandler())
.authenticationManager(authenticationManager);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// real 值可自定义
oauthServer.realm("spring-oauth-server")
// 支持 client_credentials 的配置
.allowFormAuthenticationForClients();
}
@Bean
public OAuth2RequestFactory oAuth2RequestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService);
}
@Bean
public UserApprovalHandler userApprovalHandler() {
OauthUserApprovalHandler userApprovalHandler = new OauthUserApprovalHandler();
userApprovalHandler.setOauthService(oauthService);
userApprovalHandler.setTokenStore(tokenStore);
userApprovalHandler.setClientDetailsService(this.clientDetailsService);
userApprovalHandler.setRequestFactory(oAuth2RequestFactory());
return userApprovalHandler;
}
/**
* jwt id_token
*
* @since 3.0.0
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return new ClaimsOAuth2TokenCustomizer();
}

View File

@ -1,20 +1,17 @@
package com.monkeyk.sos.config;
import com.monkeyk.sos.service.UserService;
import com.monkeyk.sos.web.context.SOSContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/**
* 2016/4/3
@ -23,69 +20,93 @@ import org.springframework.security.crypto.password.PasswordEncoder;
*
* @author Shengzhao Li
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfigurer {
@Autowired
private UserService userService;
/**
* true
*
* @since 3.0.0
*/
@Value("${sos.spring.web.security.debug:false}")
private boolean springWebSecurityDebug;
@Override
/**
* Web
* <p>
* defaultSecurityFilterChain
*
* @throws Exception e
* @since 3.0.0
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
@Override
public void configure(WebSecurity web) throws Exception {
//Ignore, public
web.ignoring().antMatchers("/public/**", "/static/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/oauth/authorize", "/oauth/token", "/oauth/rest_token");
http.authorizeRequests()
// permitAll() 的URL路径属于公开访问不需要权限
.antMatchers("/public/**").permitAll()
.antMatchers("/static/**").permitAll()
.antMatchers("/oauth/rest_token*").permitAll()
.antMatchers("/login*").permitAll()
// /user/ 开头的URL需要 ADMIN 权限
.antMatchers("/user/**").hasAnyRole("ADMIN")
.antMatchers(HttpMethod.GET, "/login*").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/signin")
.failureUrl("/login?error=1")
.usernameParameter("oidc_user")
.passwordParameter("oidcPwd")
.and()
.logout()
.logoutUrl("/signout")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/")
.and()
.exceptionHandling();
http.authenticationProvider(authenticationProvider());
http.csrf(csrfConfigurer -> {
csrfConfigurer.ignoringRequestMatchers("/oauth2/rest_token");
});
http.authorizeHttpRequests(matcherRegistry -> {
// permitAll() 的URL路径属于公开访问不需要权限
matcherRegistry
.requestMatchers("/favicon.ico*", "/oauth2/rest_token*", "*.js", "*.css").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/login*").anonymous()
// /user/ 开头的URL需要 ADMIN 权限
.requestMatchers("/user/**").hasAnyRole("ADMIN")
// 所有以 /unity/ 开头的 URL属于 UNITY 权限
.requestMatchers("/unity/**").hasAnyRole("UNITY")
// 所有以 /m/ 开头的 URL属于 MOBILE 权限
.requestMatchers("/m/**").hasAnyRole("MOBILE")
// anyRequest() 放最后
.anyRequest().authenticated();
});
http.formLogin(formLoginConfigurer -> {
formLoginConfigurer
.loginPage("/login")
.loginProcessingUrl("/signin")
.failureUrl("/login?error_failed=true")
// .defaultSuccessUrl("/")
.usernameParameter("oidc_user")
.passwordParameter("oidcPwd");
});
http.logout(logoutConfigurer -> {
logoutConfigurer.logoutUrl("/signout")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/");
});
// http.sessionManagement(configurer -> {
// configurer.maximumSessions(1).maxSessionsPreventsLogin(true);
// });
// http.exceptionHandling(configurer -> {
// configurer.accessDeniedHandler((request, response, accessDeniedException) -> {
// response.sendRedirect("/access_denied");
// });
// });
return http.build();
}
/**
*
*
* @return WebSecurityCustomizer
* @since 3.0.0
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.debug(this.springWebSecurityDebug);
}

View File

@ -3,6 +3,7 @@ package com.monkeyk.sos.domain;
import com.monkeyk.sos.domain.shared.GuidGenerator;
import com.monkeyk.sos.infrastructure.DateUtils;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@ -11,11 +12,12 @@ import java.time.LocalDateTime;
*/
public abstract class AbstractDomain implements Serializable {
@Serial
private static final long serialVersionUID = 6569365774429340632L;
/**
* Database id
*/
protected int id;
protected long id;
protected boolean archived;
/**
@ -31,11 +33,11 @@ public abstract class AbstractDomain implements Serializable {
public AbstractDomain() {
}
public int id() {
public long id() {
return id;
}
public void id(int id) {
public void id(long id) {
this.id = id;
}

View File

@ -0,0 +1,80 @@
package com.monkeyk.sos.domain.oauth;
import com.monkeyk.sos.domain.shared.GuidGenerator;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.domain.user.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import java.util.Set;
/**
* 2023/10/17
* <p>
* jwt id_token claims
*
* @author Shengzhao Li
* @since 3.0.0
*/
public class ClaimsOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
private static final Logger LOG = LoggerFactory.getLogger(ClaimsOAuth2TokenCustomizer.class);
@Autowired
private UserRepository userRepository;
public ClaimsOAuth2TokenCustomizer() {
}
@Override
public void customize(JwtEncodingContext context) {
JwtClaimsSet.Builder claims = context.getClaims();
//jti
claims.id(GuidGenerator.generateNumber());
//根据不同的 scope 与 tokenType添加扩展属性
OAuth2TokenType tokenType = context.getTokenType();
if (!OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
//非 id_token 排除
return;
}
OAuth2Authorization authorization = context.getAuthorization();
if (authorization == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Null OAuth2Authorization, ignore customize");
}
return;
}
String username = authorization.getPrincipalName();
User user = userRepository.findProfileByUsername(username);
boolean nullUser = (user == null);
Set<String> scopes = context.getAuthorizedScopes();
if (scopes.contains(OidcScopes.ADDRESS)) {
String attrVal = nullUser ? null : user.address();
claims.claim(OidcScopes.ADDRESS, attrVal == null ? "" : attrVal);
}
if (scopes.contains(OidcScopes.EMAIL)) {
String attrVal = nullUser ? null : user.email();
claims.claim(OidcScopes.EMAIL, attrVal == null ? "" : attrVal);
}
if (scopes.contains(OidcScopes.PHONE)) {
String attrVal = nullUser ? null : user.phone();
claims.claim(OidcScopes.PHONE, attrVal == null ? "" : attrVal);
}
if (scopes.contains(OidcScopes.PROFILE)) {
String attrVal = nullUser ? null : user.nickname();
claims.claim("nickname", attrVal == null ? "" : attrVal);
claims.claim("updated_at", 0);
}
}
}

View File

@ -1,29 +0,0 @@
package com.monkeyk.sos.domain.oauth;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import javax.sql.DataSource;
/**
* Add <i>archived = 0</i> condition
*
* @author Shengzhao Li
*/
public class CustomJdbcClientDetailsService extends JdbcClientDetailsService {
/**
* SQL
* archived = 0
*/
private static final String SELECT_CLIENT_DETAILS_SQL = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, " +
"web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove " +
"from oauth_client_details where client_id = ? and archived = 0 ";
public CustomJdbcClientDetailsService(DataSource dataSource) {
super(dataSource);
setSelectClientDetailsSql(SELECT_CLIENT_DETAILS_SQL);
}
}

View File

@ -2,17 +2,32 @@ package com.monkeyk.sos.domain.oauth;
import com.monkeyk.sos.infrastructure.DateUtils;
import java.io.Serial;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDateTime;
/**
* table: oauth2_registered_client
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.client.RegisteredClient
* @since 1.0.0
*/
public class OauthClientDetails implements Serializable {
@Serial
private static final long serialVersionUID = -6947822646185526939L;
/**
* id
*
* @since 3.0.0
*/
private String id;
/**
*
*/
@ -24,79 +39,153 @@ public class OauthClientDetails implements Serializable {
private boolean archived = false;
private String clientId;
private String resourceIds;
/**
* Encrypted
* client
*
*
* @since 3.0.0
*/
private String clientName;
/**
* client
*
* @since 3.0.0
*/
private Instant clientIdIssuedAt = Instant.now();
/**
* Encrypted
*/
private String clientSecret;
/**
* Available values: read,write
*/
private String scope;
/**
* grant types include
* "authorization_code", "password", "assertion", and "refresh_token".
* Default value is "authorization_code,refresh_token".
* secret
* null
* 使
*
* @since 3.0.0
*/
private String authorizedGrantTypes = "authorization_code,refresh_token";
private Instant clientSecretExpiresAt;
/**
*
* : client_secret_basic,client_secret_post
*
* @see org.springframework.security.oauth2.core.ClientAuthenticationMethod
* @since 3.0.0
*/
private String clientAuthenticationMethods;
/**
* OIDC scope ,
* : openid,profile,email
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes
*/
private String scopes;
/**
* grant_type (OAuth2.1),
* : authorization_code,refresh_token
*
* @see org.springframework.security.oauth2.core.AuthorizationGrantType
*/
private String authorizationGrantTypes;
/**
* OAuth2 uri code,
* The re-direct URI(s) established during registration (optional, comma separated).
*/
private String webServerRedirectUri;
private String redirectUris;
/**
* Authorities that are granted to the client (comma-separated). Distinct from the authorities
* granted to the user on behalf of whom the client is acting.
* <p/>
* For example: ROLE_USER
* OAuth2 退 post uri
*
* client
*
* @since 3.0.0
*/
private String authorities;
private String postLogoutRedirectUris;
/**
* The access token validity period in seconds (optional).
* If unspecified a global default will be applied by the token services.
*
* PKCE(consent)
* {ClientSettings}
*
* @see org.springframework.security.oauth2.server.authorization.settings.ClientSettings
* @since 3.0.0
*/
private Integer accessTokenValidity;
private String clientSettings;
/**
* The refresh token validity period in seconds (optional).
* If unspecified a global default will be applied by the token services.
* token
* tokenrefresh_token
* {TokenSettings}
*
* @see org.springframework.security.oauth2.server.authorization.settings.TokenSettings
* @since 3.0.0
*/
private Integer refreshTokenValidity;
private String tokenSettings;
// optional
private String additionalInformation;
/**
* The client is trusted or not. If it is trust, will skip approve step
* default false.
*/
private boolean trusted = false;
/**
* Value is 'true' or 'false', default 'false'
*/
private String autoApprove;
public OauthClientDetails() {
}
public String autoApprove() {
return autoApprove;
/**
* @since 3.0.0
*/
public String id() {
return id;
}
public OauthClientDetails autoApprove(String autoApprove) {
this.autoApprove = autoApprove;
/**
* @since 3.0.0
*/
public OauthClientDetails id(String id) {
this.id = id;
return this;
}
public boolean trusted() {
return trusted;
/**
* @since 3.0.0
*/
public String tokenSettings() {
return tokenSettings;
}
/**
* @since 3.0.0
*/
public OauthClientDetails tokenSettings(String tokenSettings) {
this.tokenSettings = tokenSettings;
return this;
}
/**
* @since 3.0.0
*/
public String clientSettings() {
return clientSettings;
}
/**
* @since 3.0.0
*/
public OauthClientDetails clientSettings(String clientSettings) {
this.clientSettings = clientSettings;
return this;
}
public LocalDateTime createTime() {
return createTime;
}
@ -114,42 +203,112 @@ public class OauthClientDetails implements Serializable {
return clientId;
}
public String resourceIds() {
return resourceIds;
}
public String clientSecret() {
return clientSecret;
}
public String scope() {
return scope;
/**
* @since 3.0.0
*/
public String clientName() {
return clientName;
}
public String authorizedGrantTypes() {
return authorizedGrantTypes;
/**
* @since 3.0.0
*/
public OauthClientDetails clientName(String clientName) {
this.clientName = clientName;
return this;
}
public String webServerRedirectUri() {
return webServerRedirectUri;
/**
* @since 3.0.0
*/
public Instant clientIdIssuedAt() {
return clientIdIssuedAt;
}
public String authorities() {
return authorities;
/**
* @since 3.0.0
*/
public OauthClientDetails clientIdIssuedAt(Instant clientIdIssuedAt) {
this.clientIdIssuedAt = clientIdIssuedAt;
return this;
}
public Integer accessTokenValidity() {
return accessTokenValidity;
/**
* @since 3.0.0
*/
public Instant clientSecretExpiresAt() {
return clientSecretExpiresAt;
}
public Integer refreshTokenValidity() {
return refreshTokenValidity;
/**
* @since 3.0.0
*/
public OauthClientDetails clientSecretExpiresAt(Instant clientSecretExpiresAt) {
this.clientSecretExpiresAt = clientSecretExpiresAt;
return this;
}
public String additionalInformation() {
return additionalInformation;
/**
* @since 3.0.0
*/
public String clientAuthenticationMethods() {
return clientAuthenticationMethods;
}
/**
* @since 3.0.0
*/
public OauthClientDetails clientAuthenticationMethods(String clientAuthenticationMethods) {
this.clientAuthenticationMethods = clientAuthenticationMethods;
return this;
}
public String scopes() {
return scopes;
}
public OauthClientDetails scopes(String scopes) {
this.scopes = scopes;
return this;
}
public String authorizationGrantTypes() {
return authorizationGrantTypes;
}
public OauthClientDetails authorizationGrantTypes(String authorizationGrantTypes) {
this.authorizationGrantTypes = authorizationGrantTypes;
return this;
}
public String redirectUris() {
return redirectUris;
}
public OauthClientDetails redirectUris(String redirectUris) {
this.redirectUris = redirectUris;
return this;
}
/**
* @since 3.0.0
*/
public String postLogoutRedirectUris() {
return postLogoutRedirectUris;
}
/**
* @since 3.0.0
*/
public OauthClientDetails postLogoutRedirectUris(String postLogoutRedirectUris) {
this.postLogoutRedirectUris = postLogoutRedirectUris;
return this;
}
@Override
public String toString() {
@ -158,15 +317,15 @@ public class OauthClientDetails implements Serializable {
sb.append("{createTime=").append(createTime);
sb.append(", archived=").append(archived);
sb.append(", clientId='").append(clientId).append('\'');
sb.append(", resourceIds='").append(resourceIds).append('\'');
sb.append(", scope='").append(scope).append('\'');
sb.append(", authorizedGrantTypes='").append(authorizedGrantTypes).append('\'');
sb.append(", webServerRedirectUri='").append(webServerRedirectUri).append('\'');
sb.append(", authorities='").append(authorities).append('\'');
sb.append(", accessTokenValidity=").append(accessTokenValidity);
sb.append(", refreshTokenValidity=").append(refreshTokenValidity);
sb.append(", additionalInformation='").append(additionalInformation).append('\'');
sb.append(", trusted=").append(trusted);
sb.append(", clientName='").append(clientName).append('\'');
sb.append(", scopes='").append(scopes).append('\'');
sb.append(", authorizationGrantTypes='").append(authorizationGrantTypes).append('\'');
sb.append(", redirectUris='").append(redirectUris).append('\'');
sb.append(", clientIdIssuedAt='").append(clientIdIssuedAt).append('\'');
sb.append(", clientSettings=").append(clientSettings);
sb.append(", tokenSettings=").append(tokenSettings);
sb.append(", postLogoutRedirectUris='").append(postLogoutRedirectUris).append('\'');
sb.append(", clientAuthenticationMethods=").append(clientAuthenticationMethods);
sb.append('}');
return sb.toString();
}
@ -181,50 +340,6 @@ public class OauthClientDetails implements Serializable {
return this;
}
public OauthClientDetails resourceIds(String resourceIds) {
this.resourceIds = resourceIds;
return this;
}
public OauthClientDetails authorizedGrantTypes(String authorizedGrantTypes) {
this.authorizedGrantTypes = authorizedGrantTypes;
return this;
}
public OauthClientDetails scope(String scope) {
this.scope = scope;
return this;
}
public OauthClientDetails webServerRedirectUri(String webServerRedirectUri) {
this.webServerRedirectUri = webServerRedirectUri;
return this;
}
public OauthClientDetails authorities(String authorities) {
this.authorities = authorities;
return this;
}
public OauthClientDetails accessTokenValidity(Integer accessTokenValidity) {
this.accessTokenValidity = accessTokenValidity;
return this;
}
public OauthClientDetails refreshTokenValidity(Integer refreshTokenValidity) {
this.refreshTokenValidity = refreshTokenValidity;
return this;
}
public OauthClientDetails trusted(boolean trusted) {
this.trusted = trusted;
return this;
}
public OauthClientDetails additionalInformation(String additionalInformation) {
this.additionalInformation = additionalInformation;
return this;
}
public OauthClientDetails archived(boolean archived) {
this.archived = archived;

View File

@ -6,6 +6,7 @@ import java.util.List;
/**
* @author Shengzhao Li
* @since 1.0.0
*/
public interface OauthRepository extends Repository {

View File

@ -1,6 +1,7 @@
package com.monkeyk.sos.domain.shared;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.UUID;
@ -10,7 +11,7 @@ import java.util.UUID;
public abstract class GuidGenerator {
private static RandomValueStringGenerator defaultClientSecretGenerator = new RandomValueStringGenerator(32);
// private static RandomValueStringGenerator defaultClientSecretGenerator = new RandomValueStringGenerator(32);
/**
@ -19,12 +20,23 @@ public abstract class GuidGenerator {
private GuidGenerator() {
}
/**
* generate random number, length 32
*
* @return number
* @since 3.0.0
*/
public static String generateNumber() {
return RandomStringUtils.random(32, false, true);
}
public static String generate() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String generateClientSecret() {
return defaultClientSecretGenerator.generate();
return RandomStringUtils.random(32, true, true);
}
}

View File

@ -0,0 +1,37 @@
package com.monkeyk.sos.domain.shared;
/**
* 2023/9/23 18:54
*
* @author Shengzhao Li
* @since 3.0.0
*/
public interface SOSConstants {
/**
* device verification URI
*
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter
*/
String DEVICE_VERIFICATION_ENDPOINT_URI = "/oauth2/device_verification";
/**
* oauth2 consent page uri
*/
String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
/**
* oauth2 authorize uri
*
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter
*/
String AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
/**
* HS256
* MacAlgorithm.java
*/
String HS = "HS";
}

View File

@ -1,97 +1,40 @@
package com.monkeyk.sos.domain.shared.security;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.io.Serial;
/**
* @author Shengzhao Li
*/
public class SOSUserDetails implements UserDetails {
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
public class SOSUserDetails extends org.springframework.security.core.userdetails.User {
@Serial
private static final long serialVersionUID = 3957586021470480642L;
protected static final String ROLE_PREFIX = "ROLE_";
protected static final GrantedAuthority DEFAULT_USER_ROLE = new SimpleGrantedAuthority(ROLE_PREFIX + Privilege.USER.name());
public static final String ROLE_PREFIX = "ROLE_";
protected User user;
protected List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
public SOSUserDetails() {
}
public SOSUserDetails(User user) {
this.user = user;
initialAuthorities();
}
private void initialAuthorities() {
//Default, everyone have it
this.grantedAuthorities.add(DEFAULT_USER_ROLE);
final List<Privilege> privileges = user.privileges();
for (Privilege privilege : privileges) {
this.grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + privilege.name()));
}
}
public static final GrantedAuthority DEFAULT_USER_ROLE = new SimpleGrantedAuthority(ROLE_PREFIX + Privilege.USER.name());
/**
* Return authorities, more information see {@link #initialAuthorities()}
*
* @return Collection of GrantedAuthority
* @since 3.0.0
*/
@Override
public Collection<GrantedAuthority> getAuthorities() {
return this.grantedAuthorities;
}
@Override
public String getPassword() {
return user.password();
}
@Override
public String getUsername() {
return user.username();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public User user() {
return user;
}
protected String userGuid;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("{user=").append(user);
sb.append('}');
return sb.toString();
public SOSUserDetails(User user) {
super(user.username(), user.password(), user.enabled(),
true, true, true, user.generateAuthorities());
this.userGuid = user.guid();
}
public String getUserGuid() {
return userGuid;
}
}

View File

@ -4,10 +4,21 @@ package com.monkeyk.sos.domain.user;
* @author Shengzhao Li
*/
public enum Privilege {
/**
* Default privilege
*/
USER,
USER, //Default privilege
ADMIN, //admin
UNITY, //资源权限UNITY
MOBILE //资源权限MOBILE
/**
* //admin
*/
ADMIN,
/**
* //资源权限UNITY
*/
UNITY,
/**
* //资源权限MOBILE
*/
MOBILE
}

View File

@ -1,18 +1,24 @@
package com.monkeyk.sos.domain.user;
import com.monkeyk.sos.domain.AbstractDomain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.Serial;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.*;
import static com.monkeyk.sos.domain.shared.security.SOSUserDetails.DEFAULT_USER_ROLE;
import static com.monkeyk.sos.domain.shared.security.SOSUserDetails.ROLE_PREFIX;
/**
* table: user_
*
* @author Shengzhao Li
*/
public class User extends AbstractDomain {
@Serial
private static final long serialVersionUID = -2921689304753120556L;
@ -27,9 +33,22 @@ public class User extends AbstractDomain {
*/
private String password;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#PHONE
*/
private String phone;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#EMAIL
*/
private String email;
//Default user is initial when create database, do not delete
/**
* Default user is initial when create database, do not delete
*/
private boolean defaultUser = false;
/**
@ -42,6 +61,33 @@ public class User extends AbstractDomain {
*/
private List<Privilege> privileges = new ArrayList<>();
/**
* true
* false
*/
private boolean enabled = true;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#PROFILE
*/
private String nickname;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#ADDRESS
*/
private String address;
/**
*
*
* @since 3.0.0
*/
private long updatedAt;
public User() {
}
@ -124,4 +170,84 @@ public class User extends AbstractDomain {
this.password = password;
return this;
}
/**
* @since 3.0.0
*/
public boolean enabled() {
return enabled;
}
/**
* @since 3.0.0
*/
public User enabled(boolean enabled) {
this.enabled = enabled;
return this;
}
/**
* @since 3.0.0
*/
public String nickname() {
return nickname;
}
/**
* @since 3.0.0
*/
public User nickname(String nickname) {
this.nickname = nickname;
return this;
}
/**
* @since 3.0.0
*/
public String address() {
return address;
}
/**
* @since 3.0.0
*/
public User address(String address) {
this.address = address;
return this;
}
/**
* @since 3.0.0
*/
public long updatedAt() {
return updatedAt;
}
/**
* @since 3.0.0
*/
public User updatedAt(long updatedAt) {
this.updatedAt = updatedAt;
return this;
}
/**
*
*
* @return GrantedAuthority set
* @since 3.0.0
*/
public Set<GrantedAuthority> generateAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();
//Default, everyone include
authorities.add(DEFAULT_USER_ROLE);
final List<Privilege> privileges = this.privileges();
for (Privilege privilege : privileges) {
authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + privilege.name()));
}
return authorities;
}
}

View File

@ -18,5 +18,18 @@ public interface UserRepository extends Repository {
User findByUsername(String username);
/**
* User profile
* phone, email, address, nickname, updated_at
*
* @param username username
* @return User only have profile fields
* @since 3.0.0
*/
User findProfileByUsername(String username);
/**
*
*/
List<User> findUsersByUsername(String username);
}

View File

@ -0,0 +1,56 @@
package com.monkeyk.sos.infrastructure;
import org.apache.commons.lang3.RandomStringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 2023/10/16 22:45
* <p>
* PKCE tool:
*
* @author Shengzhao Li
* @since 3.0.0
*/
public abstract class PKCEUtils {
private static final String ALG = "SHA-256";
private PKCEUtils() {
}
/**
* 32 code_verifier
*
* @return code_verifier
*/
public static String generateCodeVerifier() {
// 1. 随机生成code_verifier
String codeVerifierVal = RandomStringUtils.random(32, true, true);
//2. 对 code_verifier 进行base64 encode
return Base64.getEncoder().encodeToString(codeVerifierVal.getBytes(StandardCharsets.UTF_8));
}
/**
* code_verifier code_challenge
*
* @param codeVerifier code_verifier
* @return code_challenge
*/
public static String generateCodeChallenge(String codeVerifier) {
MessageDigest md;
try {
md = MessageDigest.getInstance(ALG);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("JDK not found alg: '" + ALG + "' ??", e);
}
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -11,8 +11,6 @@ import org.springframework.security.crypto.password.PasswordEncoder;
public abstract class PasswordHandler {
// private PasswordEncoder passwordEncoder = SOSContextHolder.getBean(PasswordEncoder.class);
private PasswordHandler() {
}
@ -20,7 +18,6 @@ public abstract class PasswordHandler {
public static String encode(String password) {
PasswordEncoder passwordEncoder = SOSContextHolder.getBean(PasswordEncoder.class);
// BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
}
}

View File

@ -0,0 +1,108 @@
package com.monkeyk.sos.infrastructure;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.util.List;
import java.util.Map;
/**
* 2023/10/13 14:49
*
* @author Shengzhao Li
* @since 3.0.0
*/
public abstract class SettingsUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
// ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader();
ClassLoader classLoader = SettingsUtils.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
}
private SettingsUtils() {
}
/**
* text settings -> TokenSettings
*
* @param settings text
* @return TokenSettings
*/
public static TokenSettings buildTokenSettings(String settings) {
Map<String, Object> map = parseMap(settings);
TokenSettings.Builder builder = TokenSettings.withSettings(map);
if (!map.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
builder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED);
}
return builder.build();
}
/**
* TokenSettings -> text
*
* @param settings TokenSettings
* @return text
*/
public static String textTokenSettings(TokenSettings settings) {
Map<String, Object> map = settings.getSettings();
return writeMap(map);
}
/**
* ClientSettings -> text
*
* @param settings ClientSettings
* @return text
*/
public static String textClientSettings(ClientSettings settings) {
Map<String, Object> map = settings.getSettings();
return writeMap(map);
}
/**
* text settings -> ClientSettings
*
* @param settings text
* @return ClientSettings
*/
public static ClientSettings buildClientSettings(String settings) {
Map<String, Object> map = parseMap(settings);
return ClientSettings.withSettings(map)
.build();
}
private static Map<String, Object> parseMap(String data) {
try {
return objectMapper.readValue(data, new TypeReference<>() {
});
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
private static String writeMap(Map<String, Object> data) {
try {
return objectMapper.writeValueAsString(data);
} catch (Exception ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
}
}

View File

@ -16,9 +16,10 @@ import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.ZoneId;
import java.sql.Timestamp;
/**
* table: oauth2_registered_client
* 2015/11/16
*
* @author Shengzhao Li
@ -33,24 +34,28 @@ public class OauthClientDetailsRowMapper implements RowMapper<OauthClientDetails
public OauthClientDetails mapRow(ResultSet rs, int i) throws SQLException {
OauthClientDetails clientDetails = new OauthClientDetails();
clientDetails.id(rs.getString("id"));
clientDetails.archived(rs.getBoolean("archived"));
clientDetails.createTime(rs.getTimestamp("create_time").toLocalDateTime());
clientDetails.clientId(rs.getString("client_id"));
clientDetails.resourceIds(rs.getString("resource_ids"));
clientDetails.clientIdIssuedAt(rs.getTimestamp("client_id_issued_at").toInstant());
clientDetails.clientName(rs.getString("client_name"));
clientDetails.clientAuthenticationMethods(rs.getString("client_authentication_methods"));
clientDetails.clientSecret(rs.getString("client_secret"));
clientDetails.scope(rs.getString("scope"));
clientDetails.authorizedGrantTypes(rs.getString("authorized_grant_types"));
clientDetails.webServerRedirectUri(rs.getString("web_server_redirect_uri"));
clientDetails.scopes(rs.getString("scopes"));
clientDetails.authorizationGrantTypes(rs.getString("authorization_grant_types"));
clientDetails.redirectUris(rs.getString("redirect_uris"));
clientDetails.authorities(rs.getString("authorities"));
clientDetails.accessTokenValidity(getInteger(rs, "access_token_validity"));
clientDetails.refreshTokenValidity(getInteger(rs, "refresh_token_validity"));
clientDetails.postLogoutRedirectUris(rs.getString("post_logout_redirect_uris"));
clientDetails.clientSettings(rs.getString("client_settings"));
clientDetails.tokenSettings(rs.getString("token_settings"));
clientDetails.additionalInformation(rs.getString("additional_information"));
clientDetails.createTime(rs.getTimestamp("create_time").toLocalDateTime());
clientDetails.archived(rs.getBoolean("archived"));
clientDetails.trusted(rs.getBoolean("trusted"));
clientDetails.autoApprove(rs.getString("autoapprove"));
Timestamp secretExpiresAt = rs.getTimestamp("client_secret_expires_at");
if (secretExpiresAt != null) {
clientDetails.clientSecretExpiresAt(secretExpiresAt.toInstant());
}
return clientDetails;
}

View File

@ -17,6 +17,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.List;
/**
@ -28,8 +30,7 @@ import java.util.List;
public class OauthRepositoryJdbc implements OauthRepository {
private static OauthClientDetailsRowMapper oauthClientDetailsRowMapper = new OauthClientDetailsRowMapper();
private final OauthClientDetailsRowMapper oauthClientDetailsRowMapper = new OauthClientDetailsRowMapper();
@Autowired
@ -38,47 +39,50 @@ public class OauthRepositoryJdbc implements OauthRepository {
@Override
public OauthClientDetails findOauthClientDetails(String clientId) {
final String sql = " select * from oauth_client_details where client_id = ? ";
final List<OauthClientDetails> list = this.jdbcTemplate.query(sql, new Object[]{clientId}, oauthClientDetailsRowMapper);
final String sql = " select * from oauth2_registered_client where client_id = ? ";
final List<OauthClientDetails> list = this.jdbcTemplate.query(sql, oauthClientDetailsRowMapper, clientId);
return list.isEmpty() ? null : list.get(0);
}
@Override
public List<OauthClientDetails> findAllOauthClientDetails() {
final String sql = " select * from oauth_client_details where archived = 0 order by create_time desc ";
final String sql = " select * from oauth2_registered_client where archived = 0 order by create_time desc ";
return this.jdbcTemplate.query(sql, oauthClientDetailsRowMapper);
}
@Override
public void updateOauthClientDetailsArchive(String clientId, boolean archive) {
final String sql = " update oauth_client_details set archived = ? where client_id = ? ";
final String sql = " update oauth2_registered_client set archived = ? where client_id = ? ";
this.jdbcTemplate.update(sql, archive, clientId);
}
@Override
public void saveOauthClientDetails(final OauthClientDetails clientDetails) {
final String sql = " insert into oauth_client_details(client_id,resource_ids,client_secret,scope,authorized_grant_types,web_server_redirect_uri," +
" authorities,access_token_validity,refresh_token_validity,additional_information,trusted,autoapprove) values (?,?,?,?,?,?,?,?,?,?,?,?)";
final String sql = " insert into oauth2_registered_client(id,create_time,client_id,client_id_issued_at,client_secret,client_secret_expires_at," +
"client_name,client_authentication_methods,authorization_grant_types,redirect_uris," +
" post_logout_redirect_uris,scopes,client_settings,token_settings) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
this.jdbcTemplate.update(sql, ps -> {
ps.setString(1, clientDetails.clientId());
ps.setString(2, clientDetails.resourceIds());
int index = 1;
ps.setString(index++, clientDetails.id());
ps.setTimestamp(index++, Timestamp.valueOf(clientDetails.createTime()));
ps.setString(index++, clientDetails.clientId());
ps.setTimestamp(index++, Timestamp.from(clientDetails.clientIdIssuedAt()));
ps.setString(3, clientDetails.clientSecret());
ps.setString(4, clientDetails.scope());
ps.setString(index++, clientDetails.clientSecret());
Instant clientSecretExpiresAt = clientDetails.clientSecretExpiresAt();
ps.setTimestamp(index++, clientSecretExpiresAt != null ? Timestamp.from(clientSecretExpiresAt) : null);
ps.setString(index++, clientDetails.clientName());
ps.setString(5, clientDetails.authorizedGrantTypes());
ps.setString(6, clientDetails.webServerRedirectUri());
ps.setString(index++, clientDetails.clientAuthenticationMethods());
ps.setString(index++, clientDetails.authorizationGrantTypes());
ps.setString(index++, clientDetails.redirectUris());
ps.setString(7, clientDetails.authorities());
ps.setObject(8, clientDetails.accessTokenValidity());
ps.setObject(9, clientDetails.refreshTokenValidity());
ps.setString(10, clientDetails.additionalInformation());
ps.setBoolean(11, clientDetails.trusted());
ps.setString(12, clientDetails.autoApprove());
ps.setString(index++, clientDetails.postLogoutRedirectUris());
ps.setString(index++, clientDetails.scopes());
ps.setString(index++, clientDetails.clientSettings());
ps.setString(index++, clientDetails.tokenSettings());
});
}
}

View File

@ -0,0 +1,43 @@
package com.monkeyk.sos.infrastructure.jdbc;
import com.monkeyk.sos.domain.user.User;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* table: user_
* 2023/10/17
*
* @author Shengzhao Li
* @since 3.0.0
*/
public class UserProfileRowMapper implements RowMapper<User> {
public UserProfileRowMapper() {
}
@Override
public User mapRow(ResultSet rs, int i) throws SQLException {
User user = new User();
user.id(rs.getInt("id"));
user.guid(rs.getString("guid"));
user.archived(rs.getBoolean("archived"));
user.createTime(rs.getTimestamp("create_time").toLocalDateTime());
user.email(rs.getString("email"));
user.phone(rs.getString("phone"));
user.username(rs.getString("username"));
user.address(rs.getString("address"));
user.nickname(rs.getString("nickname"));
user.enabled(rs.getBoolean("enabled"));
user.updatedAt(rs.getLong("updated_at"));
return user;
}
}

View File

@ -14,7 +14,9 @@ package com.monkeyk.sos.infrastructure.jdbc;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.domain.user.UserRepository;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@ -33,8 +35,14 @@ import java.util.stream.Collectors;
@Repository("userRepositoryJdbc")
public class UserRepositoryJdbc implements UserRepository {
private static final Logger LOG = LoggerFactory.getLogger(UserRepositoryJdbc.class);
private static UserRowMapper userRowMapper = new UserRowMapper();
private final UserRowMapper userRowMapper = new UserRowMapper();
/**
* @since 3.0.0
*/
private final UserProfileRowMapper userProfileRowMapper = new UserProfileRowMapper();
@Autowired
private JdbcTemplate jdbcTemplate;
@ -42,7 +50,7 @@ public class UserRepositoryJdbc implements UserRepository {
@Override
public User findByGuid(String guid) {
final String sql = " select * from user_ where guid = ? ";
final List<User> list = this.jdbcTemplate.query(sql, new Object[]{guid}, userRowMapper);
final List<User> list = this.jdbcTemplate.query(sql, userRowMapper, guid);
User user = null;
if (!list.isEmpty()) {
@ -53,9 +61,9 @@ public class UserRepositoryJdbc implements UserRepository {
return user;
}
private Collection<Privilege> findPrivileges(int userId) {
private Collection<Privilege> findPrivileges(long userId) {
final String sql = " select privilege from user_privilege where user_id = ? ";
final List<String> strings = this.jdbcTemplate.queryForList(sql, new Object[]{userId}, String.class);
final List<String> strings = this.jdbcTemplate.queryForList(sql, String.class, userId);
List<Privilege> privileges = new ArrayList<>(strings.size());
privileges.addAll(strings.stream().map(Privilege::valueOf).collect(Collectors.toList()));
@ -64,8 +72,9 @@ public class UserRepositoryJdbc implements UserRepository {
@Override
public void saveUser(final User user) {
final String sql = " insert into user_(guid,archived,create_time,email,password,username,phone) " +
" values (?,?,?,?,?,?,?) ";
final String sql = " insert into user_(guid,archived,create_time,email,password,username,phone," +
"address,nickname,updated_at,enabled) " +
" values (?,?,?,?,?,?,?,?,?,?,?) ";
this.jdbcTemplate.update(sql, ps -> {
ps.setString(1, user.guid());
ps.setBoolean(2, user.archived());
@ -77,10 +86,15 @@ public class UserRepositoryJdbc implements UserRepository {
ps.setString(6, user.username());
ps.setString(7, user.phone());
// v3.0.0 added
ps.setString(8, user.address());
ps.setString(9, user.nickname());
ps.setLong(10, user.updatedAt());
ps.setBoolean(11, user.enabled());
});
//get user id
final Integer id = this.jdbcTemplate.queryForObject("select id from user_ where guid = ?", new Object[]{user.guid()}, Integer.class);
final Integer id = this.jdbcTemplate.queryForObject("select id from user_ where guid = ?", Integer.class, user.guid());
//insert privileges
for (final Privilege privilege : user.privileges()) {
@ -94,28 +108,55 @@ public class UserRepositoryJdbc implements UserRepository {
@Override
public void updateUser(final User user) {
final String sql = " update user_ set username = ?, password = ?, phone = ?,email = ? where guid = ? ";
this.jdbcTemplate.update(sql, ps -> {
final String sql = " update user_ set username = ?, password = ?, phone = ?,email = ?," +
"address = ?, nickname = ?, enabled = ? where guid = ? ";
int row = this.jdbcTemplate.update(sql, ps -> {
ps.setString(1, user.username());
ps.setString(2, user.password());
ps.setString(3, user.phone());
ps.setString(4, user.email());
// v3.0.0 added
ps.setString(5, user.address());
ps.setString(6, user.nickname());
ps.setBoolean(7, user.enabled());
ps.setString(5, user.guid());
ps.setString(8, user.guid());
});
}
@Override
public User findByUsername(String username) {
final String sql = " select * from user_ where username = ? and archived = 0 ";
final List<User> list = this.jdbcTemplate.query(sql, new Object[]{username}, userRowMapper);
final List<User> list = this.jdbcTemplate.query(sql, userRowMapper, username);
User user = null;
if (!list.isEmpty()) {
user = list.get(0);
user.privileges().addAll(findPrivileges(user.id()));
}
if (list.size() > 1) {
LOG.warn("Found {} user(s) by username: {}, checking duplicate data??", list.size(), username);
}
return user;
}
/**
* {@inheritDoc}
*/
@Override
public User findProfileByUsername(String username) {
final String sql = " select id, guid,create_time,archived, username,enabled,phone,email,address,nickname,updated_at from user_ where username = ? and archived = 0 ";
final List<User> list = this.jdbcTemplate.query(sql, userProfileRowMapper, username);
User user = null;
if (!list.isEmpty()) {
user = list.get(0);
}
if (list.size() > 1) {
LOG.warn("Found {} user profiles by username: {}, checking duplicate data??", list.size(), username);
}
return user;
}
@ -130,7 +171,7 @@ public class UserRepositoryJdbc implements UserRepository {
}
sql += " order by create_time desc ";
final List<User> list = this.jdbcTemplate.query(sql, params, userRowMapper);
final List<User> list = this.jdbcTemplate.query(sql, userRowMapper, params);
for (User user : list) {
user.privileges().addAll(findPrivileges(user.id()));
}

View File

@ -18,6 +18,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
/**
* table: user_
* 2015/11/16
*
* @author Shengzhao Li
@ -45,6 +46,11 @@ public class UserRowMapper implements RowMapper<User> {
user.username(rs.getString("username"));
user.lastLoginTime(rs.getTimestamp("last_login_time"));
//v3.0.0 added
user.address(rs.getString("address"));
user.nickname(rs.getString("nickname"));
user.enabled(rs.getBoolean("enabled"));
user.updatedAt(rs.getLong("updated_at"));
return user;
}

View File

@ -1,8 +1,6 @@
package com.monkeyk.sos.service.business;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
/**
* 2019/7/5
@ -19,10 +17,10 @@ public class ClientCredentialsInlineAccessTokenInvoker extends InlineAccessToken
public ClientCredentialsInlineAccessTokenInvoker() {
}
@Override
protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
return new ClientCredentialsTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
}
// @Override
// protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
// return new ClientCredentialsTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
// }
}

View File

@ -1,29 +1,23 @@
package com.monkeyk.sos.service.business;
import com.monkeyk.sos.service.dto.AccessTokenDto;
import com.monkeyk.sos.web.context.SOSContextHolder;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.util.Assert;
import java.util.Map;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.CLIENT_ID;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE;
import static org.springframework.security.oauth2.common.util.OAuth2Utils.SCOPE;
import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.*;
/**
* 2019/7/5
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter
* @since 2.0.1
*/
public abstract class InlineAccessTokenInvoker implements InitializingBean {
@ -32,11 +26,11 @@ public abstract class InlineAccessTokenInvoker implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(InlineAccessTokenInvoker.class);
protected transient AuthenticationManager authenticationManager = SOSContextHolder.getBean(AuthenticationManager.class);
// protected transient AuthenticationManager authenticationManager = SOSContextHolder.getBean(AuthenticationManager.class);
protected transient AuthorizationServerTokenServices tokenServices = SOSContextHolder.getBean(AuthorizationServerTokenServices.class);
;
protected transient ClientDetailsService clientDetailsService = SOSContextHolder.getBean(ClientDetailsService.class);
// protected transient AuthorizationServerTokenServices tokenServices = SOSContextHolder.getBean(AuthorizationServerTokenServices.class);
//
// protected transient ClientDetailsService clientDetailsService = SOSContextHolder.getBean(ClientDetailsService.class);
public InlineAccessTokenInvoker() {
@ -62,26 +56,27 @@ public abstract class InlineAccessTokenInvoker implements InitializingBean {
String clientId = validateParams(params);
final ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
LOG.warn("Not found ClientDetails by clientId: {}", clientId);
return null;
}
// final ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
// if (clientDetails == null) {
// LOG.warn("Not found ClientDetails by clientId: {}", clientId);
// return null;
// }
//
// OAuth2RequestFactory oAuth2RequestFactory = createOAuth2RequestFactory();
// TokenGranter tokenGranter = getTokenGranter(oAuth2RequestFactory);
// LOG.debug("Use TokenGranter: {}", tokenGranter);
OAuth2RequestFactory oAuth2RequestFactory = createOAuth2RequestFactory();
TokenGranter tokenGranter = getTokenGranter(oAuth2RequestFactory);
LOG.debug("Use TokenGranter: {}", tokenGranter);
TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(params, clientDetails);
final OAuth2AccessToken oAuth2AccessToken = tokenGranter.grant(getGrantType(params), tokenRequest);
if (oAuth2AccessToken == null) {
LOG.warn("TokenGranter: {} grant OAuth2AccessToken null", tokenGranter);
return null;
}
AccessTokenDto accessTokenDto = new AccessTokenDto(oAuth2AccessToken);
LOG.debug("Invoked accessTokenDto: {}", accessTokenDto);
return accessTokenDto;
// TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(params, clientDetails);
// final OAuth2AccessToken oAuth2AccessToken = tokenGranter.grant(getGrantType(params), tokenRequest);
//
// if (oAuth2AccessToken == null) {
// LOG.warn("TokenGranter: {} grant OAuth2AccessToken null", tokenGranter);
// return null;
// }
// AccessTokenDto accessTokenDto = new AccessTokenDto(oAuth2AccessToken);
// LOG.debug("Invoked accessTokenDto: {}", accessTokenDto);
// return accessTokenDto;
throw new UnsupportedOperationException("Not yet implements");
}
@ -125,40 +120,40 @@ public abstract class InlineAccessTokenInvoker implements InitializingBean {
}
/**
* Get TokenGranter implement
*
* @return TokenGranter
*/
protected abstract TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory);
/**
* Create OAuth2RequestFactory
*
* @return OAuth2RequestFactory instance
*/
protected OAuth2RequestFactory createOAuth2RequestFactory() {
return new DefaultOAuth2RequestFactory(this.clientDetailsService);
}
// /**
// * Get TokenGranter implement
// *
// * @return TokenGranter
// */
// protected abstract TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory);
//
// /**
// * Create OAuth2RequestFactory
// *
// * @return OAuth2RequestFactory instance
// */
// protected OAuth2RequestFactory createOAuth2RequestFactory() {
// return new DefaultOAuth2RequestFactory(this.clientDetailsService);
// }
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
// public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// this.authenticationManager = authenticationManager;
// }
public void setTokenServices(AuthorizationServerTokenServices tokenServices) {
this.tokenServices = tokenServices;
}
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
// public void setTokenServices(AuthorizationServerTokenServices tokenServices) {
// this.tokenServices = tokenServices;
// }
//
// public void setClientDetailsService(ClientDetailsService clientDetailsService) {
// this.clientDetailsService = clientDetailsService;
// }
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.authenticationManager, "authenticationManager is null");
Assert.notNull(this.tokenServices, "tokenServices is null");
// Assert.notNull(this.authenticationManager, "authenticationManager is null");
// Assert.notNull(this.tokenServices, "tokenServices is null");
Assert.notNull(this.clientDetailsService, "clientDetailsService is null");
// Assert.notNull(this.clientDetailsService, "clientDetailsService is null");
}
}

View File

@ -1,8 +1,6 @@
package com.monkeyk.sos.service.business;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
/**
* 2019/7/5
@ -19,10 +17,10 @@ public class PasswordInlineAccessTokenInvoker extends InlineAccessTokenInvoker {
public PasswordInlineAccessTokenInvoker() {
}
@Override
protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
return new ResourceOwnerPasswordTokenGranter(this.authenticationManager, this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
}
// @Override
// protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
// return new ResourceOwnerPasswordTokenGranter(this.authenticationManager, this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
// }

View File

@ -1,8 +1,6 @@
package com.monkeyk.sos.service.business;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
/**
* 2019/7/5
@ -19,10 +17,10 @@ public class RefreshTokenInlineAccessTokenInvoker extends InlineAccessTokenInvok
public RefreshTokenInlineAccessTokenInvoker() {
}
@Override
protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
return new RefreshTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
}
// @Override
// protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) {
// return new RefreshTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory);
// }
}

View File

@ -1,11 +1,14 @@
package com.monkeyk.sos.service.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import java.io.Serial;
import java.io.Serializable;
import java.time.temporal.ChronoField;
/**
* 2019/7/5
@ -16,6 +19,7 @@ import java.io.Serializable;
* @since 2.0.1
*/
public class AccessTokenDto implements Serializable {
@Serial
private static final long serialVersionUID = -8894979171517528312L;
@ -40,16 +44,24 @@ public class AccessTokenDto implements Serializable {
public AccessTokenDto(OAuth2AccessToken token) {
this.accessToken = token.getValue();
this.expiresIn = token.getExpiresIn();
this(token, null);
}
this.scope = StringUtils.join(token.getScope(), ",");
this.tokenType = token.getTokenType();
/**
* @since 3.0.0
*/
public AccessTokenDto(OAuth2AccessToken token, OAuth2RefreshToken refreshToken) {
this.accessToken = token.getTokenValue();
this.expiresIn = token.getExpiresAt().get(ChronoField.SECOND_OF_DAY);
final OAuth2RefreshToken oAuth2RefreshToken = token.getRefreshToken();
if (oAuth2RefreshToken != null) {
this.refreshToken = oAuth2RefreshToken.getValue();
}
this.scope = StringUtils.join(token.getScopes(), ",");
this.tokenType = token.getTokenType().getValue();
this.refreshToken = refreshToken != null ? refreshToken.getTokenValue() : null;
// final OAuth2RefreshToken oAuth2RefreshToken = token.getRefreshToken();
// if (oAuth2RefreshToken != null) {
// this.refreshToken = oAuth2RefreshToken.getValue();
// }
}

View File

@ -0,0 +1,132 @@
package com.monkeyk.sos.service.dto;
import com.monkeyk.sos.infrastructure.SettingsUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import java.io.Serial;
import java.io.Serializable;
import static com.monkeyk.sos.domain.shared.SOSConstants.HS;
import static org.springframework.security.oauth2.jose.jws.JwsAlgorithms.RS256;
/**
* 2023/10/13 11:52
* <p>
* .requireProofKey(false)
* .requireAuthorizationConsent(false);
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.settings.ClientSettings
* @since 3.0.0
*/
public class ClientSettingsDto implements Serializable {
@Serial
private static final long serialVersionUID = -7335241589844569340L;
/**
* PKCEtrue
* false
*/
private boolean requireProofKey;
/**
* true
* false
*/
private boolean requireAuthorizationConsent;
/**
* client jwk URL
* , jwt-bearer使(OAuth2.1)
*
* @since 3.0.0
*/
private String jwkSetUrl;
/**
* jwt token
* JwsAlgorithm
*
* @see JwsAlgorithm
*/
private String tokenEndpointAuthenticationSigningAlgorithm = RS256;
public ClientSettingsDto() {
}
public ClientSettingsDto(String clientSettings) {
ClientSettings settings = SettingsUtils.buildClientSettings(clientSettings);
this.requireAuthorizationConsent = settings.isRequireAuthorizationConsent();
this.requireProofKey = settings.isRequireProofKey();
JwsAlgorithm jAlg = settings.getTokenEndpointAuthenticationSigningAlgorithm();
if (jAlg != null) {
this.tokenEndpointAuthenticationSigningAlgorithm = jAlg.getName();
}
this.jwkSetUrl = settings.getJwkSetUrl();
}
public ClientSettings toSettings() {
ClientSettings.Builder builder = ClientSettings.builder()
.requireProofKey(requireProofKey)
.requireAuthorizationConsent(requireAuthorizationConsent);
//区分不同算法:对称/非对称
if (tokenEndpointAuthenticationSigningAlgorithm.startsWith(HS)) {
builder.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.valueOf(tokenEndpointAuthenticationSigningAlgorithm));
} else {
builder.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.valueOf(tokenEndpointAuthenticationSigningAlgorithm));
}
if (StringUtils.isNotBlank(jwkSetUrl)) {
builder.jwkSetUrl(jwkSetUrl);
}
return builder.build();
}
public boolean isRequireProofKey() {
return requireProofKey;
}
public void setRequireProofKey(boolean requireProofKey) {
this.requireProofKey = requireProofKey;
}
public boolean isRequireAuthorizationConsent() {
return requireAuthorizationConsent;
}
public void setRequireAuthorizationConsent(boolean requireAuthorizationConsent) {
this.requireAuthorizationConsent = requireAuthorizationConsent;
}
public String getJwkSetUrl() {
return jwkSetUrl;
}
public void setJwkSetUrl(String jwkSetUrl) {
this.jwkSetUrl = jwkSetUrl;
}
public String getTokenEndpointAuthenticationSigningAlgorithm() {
return tokenEndpointAuthenticationSigningAlgorithm;
}
public void setTokenEndpointAuthenticationSigningAlgorithm(String tokenEndpointAuthenticationSigningAlgorithm) {
this.tokenEndpointAuthenticationSigningAlgorithm = tokenEndpointAuthenticationSigningAlgorithm;
}
@Override
public String toString() {
return "{" +
"requireProofKey=" + requireProofKey +
", requireAuthorizationConsent=" + requireAuthorizationConsent +
// ", jwkSetUrl='" + jwkSetUrl + '\'' +
", tokenEndpointAuthenticationSigningAlgorithm='" + tokenEndpointAuthenticationSigningAlgorithm + '\'' +
'}';
}
}

View File

@ -4,44 +4,133 @@ import com.monkeyk.sos.domain.oauth.OauthClientDetails;
import com.monkeyk.sos.domain.shared.GuidGenerator;
import com.monkeyk.sos.infrastructure.DateUtils;
import com.monkeyk.sos.infrastructure.PasswordHandler;
import org.apache.commons.lang.StringUtils;
import com.monkeyk.sos.infrastructure.SettingsUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serial;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.security.oauth2.core.AuthorizationGrantType.*;
/**
* @author Shengzhao Li
* @since 1.0.0
*/
public class OauthClientDetailsDto implements Serializable {
@Serial
private static final long serialVersionUID = 4011292111995231569L;
/**
* id
*
* @since 3.0.0
*/
private String id;
private String createTime;
private boolean archived;
private String clientId = GuidGenerator.generate();
private String resourceIds;
/**
* client
*
*
* @since 3.0.0
*/
private String clientName;
/**
* client
*
* @since 3.0.0
*/
private String clientIdIssuedAt;
private String clientSecret = GuidGenerator.generateClientSecret();
private String scope;
private String authorizedGrantTypes;
/**
* secret
* null
* 使
*
* @since 3.0.0
*/
private String clientSecretExpiresAt;
private String webServerRedirectUri;
private String authorities;
/**
*
* : client_secret_basic,client_secret_post
*
* @see org.springframework.security.oauth2.core.ClientAuthenticationMethod
* @since 3.0.0
*/
private String clientAuthenticationMethods;
private Integer accessTokenValidity;
private Integer refreshTokenValidity;
/**
* OIDC scope ,
* : openid,profile,email
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes
*/
private String scopes;
// optional
private String additionalInformation;
/**
* grant_type (OAuth2.1),
* : authorization_code,refresh_token
*
* @see org.springframework.security.oauth2.core.AuthorizationGrantType
*/
private String authorizationGrantTypes;
/**
* OAuth2 uri code,
* The re-direct URI(s) established during registration (optional, comma separated).
*/
private String redirectUris;
/**
* OAuth2 退 post uri
*
* client
*
* @since 3.0.0
*/
private String postLogoutRedirectUris;
/**
*
* PKCE(consent)
* {ClientSettings}
*
* @see org.springframework.security.oauth2.server.authorization.settings.ClientSettings
* @since 3.0.0
*/
private ClientSettingsDto clientSettings;
/**
* token
* tokenrefresh_token
* {TokenSettings}
*
* @see org.springframework.security.oauth2.server.authorization.settings.TokenSettings
* @since 3.0.0
*/
private TokenSettingsDto tokenSettings;
private boolean trusted;
public OauthClientDetailsDto() {
}
@ -49,21 +138,26 @@ public class OauthClientDetailsDto implements Serializable {
public OauthClientDetailsDto(OauthClientDetails clientDetails) {
this.clientId = clientDetails.clientId();
this.clientSecret = clientDetails.clientSecret();
this.scope = clientDetails.scope();
this.scopes = clientDetails.scopes();
this.createTime = DateUtils.toDateTime(clientDetails.createTime());
this.archived = clientDetails.archived();
this.resourceIds = clientDetails.resourceIds();
this.postLogoutRedirectUris = clientDetails.postLogoutRedirectUris();
this.webServerRedirectUri = clientDetails.webServerRedirectUri();
this.authorities = clientDetails.authorities();
this.accessTokenValidity = clientDetails.accessTokenValidity();
this.redirectUris = clientDetails.redirectUris();
this.clientIdIssuedAt = clientDetails.clientIdIssuedAt().toString();
Instant clientSecretExpiresAt1 = clientDetails.clientSecretExpiresAt();
if (clientSecretExpiresAt1 != null) {
this.clientSecretExpiresAt = clientSecretExpiresAt1.toString();
}
this.refreshTokenValidity = clientDetails.refreshTokenValidity();
this.additionalInformation = clientDetails.additionalInformation();
this.trusted = clientDetails.trusted();
this.clientAuthenticationMethods = clientDetails.clientAuthenticationMethods();
this.clientName = clientDetails.clientName();
this.id = clientDetails.id();
this.authorizedGrantTypes = clientDetails.authorizedGrantTypes();
this.authorizationGrantTypes = clientDetails.authorizationGrantTypes();
this.clientSettings = new ClientSettingsDto(clientDetails.clientSettings());
this.tokenSettings = new TokenSettingsDto(clientDetails.tokenSettings());
}
@ -91,13 +185,6 @@ public class OauthClientDetailsDto implements Serializable {
this.clientId = clientId;
}
public String getResourceIds() {
return resourceIds;
}
public void setResourceIds(String resourceIds) {
this.resourceIds = resourceIds;
}
public String getClientSecret() {
return clientSecret;
@ -107,77 +194,6 @@ public class OauthClientDetailsDto implements Serializable {
this.clientSecret = clientSecret;
}
public String getScope() {
return scope;
}
public String getScopeWithBlank() {
if (scope != null && scope.contains(",")) {
return scope.replaceAll(",", " ");
}
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getAuthorizedGrantTypes() {
return authorizedGrantTypes;
}
public void setAuthorizedGrantTypes(String authorizedGrantTypes) {
this.authorizedGrantTypes = authorizedGrantTypes;
}
public String getWebServerRedirectUri() {
return webServerRedirectUri;
}
public void setWebServerRedirectUri(String webServerRedirectUri) {
this.webServerRedirectUri = webServerRedirectUri;
}
public String getAuthorities() {
return authorities;
}
public void setAuthorities(String authorities) {
this.authorities = authorities;
}
public Integer getAccessTokenValidity() {
return accessTokenValidity;
}
public void setAccessTokenValidity(Integer accessTokenValidity) {
this.accessTokenValidity = accessTokenValidity;
}
public Integer getRefreshTokenValidity() {
return refreshTokenValidity;
}
public void setRefreshTokenValidity(Integer refreshTokenValidity) {
this.refreshTokenValidity = refreshTokenValidity;
}
public String getAdditionalInformation() {
return additionalInformation;
}
public void setAdditionalInformation(String additionalInformation) {
this.additionalInformation = additionalInformation;
}
public boolean isTrusted() {
return trusted;
}
public void setTrusted(boolean trusted) {
this.trusted = trusted;
}
public static List<OauthClientDetailsDto> toDtos(List<OauthClientDetails> clientDetailses) {
List<OauthClientDetailsDto> dtos = new ArrayList<>(clientDetailses.size());
@ -189,51 +205,191 @@ public class OauthClientDetailsDto implements Serializable {
public boolean isContainsAuthorizationCode() {
return this.authorizedGrantTypes.contains("authorization_code");
if (!this.authorizationGrantTypes.contains(AUTHORIZATION_CODE.getValue())) {
return false;
}
if (clientSettings == null) {
return true;
}
return !clientSettings.isRequireProofKey();
}
/**
* PKCE flow
*
* @since 3.0.0
*/
public boolean isContainsAuthorizationCodeWithPKCE() {
if (!this.authorizationGrantTypes.contains(AUTHORIZATION_CODE.getValue())) {
return false;
}
return clientSettings != null && clientSettings.isRequireProofKey();
}
/**
* OAuth2.1
*
* @deprecated from OAuth2.1
*/
public boolean isContainsPassword() {
return this.authorizedGrantTypes.contains("password");
return this.authorizationGrantTypes.contains(PASSWORD.getValue());
}
public boolean isContainsImplicit() {
return this.authorizedGrantTypes.contains("implicit");
}
// public boolean isContainsImplicit() {
// return this.authorizationGrantTypes.contains("implicit");
// }
public boolean isContainsClientCredentials() {
return this.authorizedGrantTypes.contains("client_credentials");
return this.authorizationGrantTypes.contains(CLIENT_CREDENTIALS.getValue());
}
public boolean isContainsRefreshToken() {
return this.authorizedGrantTypes.contains("refresh_token");
return this.authorizationGrantTypes.contains(REFRESH_TOKEN.getValue());
}
/**
* @since 3.0.0
*/
public boolean isContainsDeviceCode() {
return this.authorizationGrantTypes.contains(DEVICE_CODE.getValue());
}
/**
* @since 3.0.0
*/
public boolean isContainsJwtBearer() {
return this.authorizationGrantTypes.contains(JWT_BEARER.getValue());
}
public OauthClientDetails createDomain() {
OauthClientDetails clientDetails = new OauthClientDetails()
.id(GuidGenerator.generateNumber())
.clientId(clientId)
.clientName(clientName)
// encrypted client secret
.clientSecret(PasswordHandler.encode(clientSecret))
.resourceIds(resourceIds)
.authorizedGrantTypes(authorizedGrantTypes)
.scope(scope);
.postLogoutRedirectUris(postLogoutRedirectUris)
.authorizationGrantTypes(authorizationGrantTypes)
.clientAuthenticationMethods(clientAuthenticationMethods)
.scopes(scopes);
if (StringUtils.isNotEmpty(webServerRedirectUri)) {
clientDetails.webServerRedirectUri(webServerRedirectUri);
if (StringUtils.isNotBlank(clientIdIssuedAt)) {
clientDetails.clientIdIssuedAt(Instant.parse(this.clientIdIssuedAt));
}
if (StringUtils.isNotEmpty(authorities)) {
clientDetails.authorities(authorities);
if (StringUtils.isNotBlank(clientSecretExpiresAt)) {
clientDetails.clientSecretExpiresAt(Instant.parse(this.clientSecretExpiresAt));
}
clientDetails.accessTokenValidity(accessTokenValidity)
.refreshTokenValidity(refreshTokenValidity)
.trusted(trusted);
if (StringUtils.isNotEmpty(additionalInformation)) {
clientDetails.additionalInformation(additionalInformation);
if (StringUtils.isNotEmpty(redirectUris)) {
clientDetails.redirectUris(redirectUris);
}
clientDetails.clientSettings(SettingsUtils.textClientSettings(this.clientSettings.toSettings()));
clientDetails.tokenSettings(SettingsUtils.textTokenSettings(this.tokenSettings.toSettings()));
return clientDetails;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getClientIdIssuedAt() {
return clientIdIssuedAt;
}
public void setClientIdIssuedAt(String clientIdIssuedAt) {
this.clientIdIssuedAt = clientIdIssuedAt;
}
public String getClientSecretExpiresAt() {
return clientSecretExpiresAt;
}
public void setClientSecretExpiresAt(String clientSecretExpiresAt) {
this.clientSecretExpiresAt = clientSecretExpiresAt;
}
public String getClientAuthenticationMethods() {
return clientAuthenticationMethods;
}
public void setClientAuthenticationMethods(String clientAuthenticationMethods) {
this.clientAuthenticationMethods = clientAuthenticationMethods;
}
public String getScopes() {
return scopes;
}
public void setScopes(String scopes) {
this.scopes = scopes;
}
public String getAuthorizationGrantTypes() {
return authorizationGrantTypes;
}
public void setAuthorizationGrantTypes(String authorizationGrantTypes) {
this.authorizationGrantTypes = authorizationGrantTypes;
}
public String getRedirectUris() {
return redirectUris;
}
public void setRedirectUris(String redirectUris) {
this.redirectUris = redirectUris;
}
public String getPostLogoutRedirectUris() {
return postLogoutRedirectUris;
}
public void setPostLogoutRedirectUris(String postLogoutRedirectUris) {
this.postLogoutRedirectUris = postLogoutRedirectUris;
}
public ClientSettingsDto getClientSettings() {
return clientSettings;
}
public void setClientSettings(ClientSettingsDto clientSettings) {
this.clientSettings = clientSettings;
}
public TokenSettingsDto getTokenSettings() {
return tokenSettings;
}
public void setTokenSettings(TokenSettingsDto tokenSettings) {
this.tokenSettings = tokenSettings;
}
/**
* , ' '
*
* @return scopes
*/
public String getScopesWithBlank() {
if (scopes != null && scopes.contains(",")) {
return scopes.replaceAll(",", " ");
}
return scopes;
}
}

View File

@ -0,0 +1,188 @@
package com.monkeyk.sos.service.dto;
import com.monkeyk.sos.infrastructure.SettingsUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.io.Serial;
import java.io.Serializable;
import java.time.Duration;
/**
* 2023/10/13 12:07
* <p>
* <p>
* .authorizationCodeTimeToLive(Duration.ofMinutes(5))
* .accessTokenTimeToLive(Duration.ofMinutes(5))
* .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
* .deviceCodeTimeToLive(Duration.ofMinutes(5))
* .reuseRefreshTokens(true)
* .refreshTokenTimeToLive(Duration.ofMinutes(60))
* .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.settings.TokenSettings
* @since 3.0.0
*/
public class TokenSettingsDto implements Serializable {
@Serial
private static final long serialVersionUID = -8918978047051059724L;
/**
* authorizationCode
* 300 5)
*/
private long authorizationCodeTimeToLive = 300L;
/**
* access_token
* 3600 1)
*/
private long accessTokenTimeToLive = 3600L;
/**
* token ,
* self-contained -> jwt
* reference -> uuid
*
* @see OAuth2TokenFormat
*/
private String accessTokenFormat = "self-contained";
/**
* device_code
* 300 5
*/
private long deviceCodeTimeToLive = 300L;
/**
* refresh_token
* true
*/
private boolean reuseRefreshTokens = true;
/**
* refresh_token
* 4320012
*/
private long refreshTokenTimeToLive = 43200L;
/**
* id_token使
* jwks
*
* @see SignatureAlgorithm
*/
private String idTokenSignatureAlgorithm;
public TokenSettingsDto() {
}
public TokenSettingsDto(String tokenSettings) {
TokenSettings settings = SettingsUtils.buildTokenSettings(tokenSettings);
this.accessTokenFormat = settings.getAccessTokenFormat().getValue();
this.idTokenSignatureAlgorithm = settings.getIdTokenSignatureAlgorithm().getName();
this.refreshTokenTimeToLive = settings.getRefreshTokenTimeToLive().toSeconds();
this.accessTokenFormat= settings.getAccessTokenFormat().getValue();
this.accessTokenTimeToLive=settings.getAccessTokenTimeToLive().toSeconds();
this.deviceCodeTimeToLive= settings.getDeviceCodeTimeToLive().toSeconds();
this.authorizationCodeTimeToLive= settings.getAuthorizationCodeTimeToLive().toSeconds();
}
public TokenSettings toSettings() {
TokenSettings.Builder builder = TokenSettings.builder()
.refreshTokenTimeToLive(Duration.ofSeconds(this.refreshTokenTimeToLive))
.accessTokenTimeToLive(Duration.ofSeconds(this.accessTokenTimeToLive))
.reuseRefreshTokens(this.reuseRefreshTokens)
.deviceCodeTimeToLive(Duration.ofSeconds(this.deviceCodeTimeToLive))
.authorizationCodeTimeToLive(Duration.ofSeconds(this.authorizationCodeTimeToLive));
if (StringUtils.isNotBlank(idTokenSignatureAlgorithm)) {
builder.idTokenSignatureAlgorithm(SignatureAlgorithm.valueOf(idTokenSignatureAlgorithm));
}
if (StringUtils.isNotBlank(accessTokenFormat)) {
builder.accessTokenFormat(new OAuth2TokenFormat(accessTokenFormat));
}
return builder.build();
}
public long getAuthorizationCodeTimeToLive() {
return authorizationCodeTimeToLive;
}
public void setAuthorizationCodeTimeToLive(long authorizationCodeTimeToLive) {
this.authorizationCodeTimeToLive = authorizationCodeTimeToLive;
}
public long getAccessTokenTimeToLive() {
return accessTokenTimeToLive;
}
public void setAccessTokenTimeToLive(long accessTokenTimeToLive) {
this.accessTokenTimeToLive = accessTokenTimeToLive;
}
public String getAccessTokenFormat() {
return accessTokenFormat;
}
public void setAccessTokenFormat(String accessTokenFormat) {
this.accessTokenFormat = accessTokenFormat;
}
public long getDeviceCodeTimeToLive() {
return deviceCodeTimeToLive;
}
public void setDeviceCodeTimeToLive(long deviceCodeTimeToLive) {
this.deviceCodeTimeToLive = deviceCodeTimeToLive;
}
public boolean isReuseRefreshTokens() {
return reuseRefreshTokens;
}
public void setReuseRefreshTokens(boolean reuseRefreshTokens) {
this.reuseRefreshTokens = reuseRefreshTokens;
}
public long getRefreshTokenTimeToLive() {
return refreshTokenTimeToLive;
}
public void setRefreshTokenTimeToLive(long refreshTokenTimeToLive) {
this.refreshTokenTimeToLive = refreshTokenTimeToLive;
}
public String getIdTokenSignatureAlgorithm() {
return idTokenSignatureAlgorithm;
}
public void setIdTokenSignatureAlgorithm(String idTokenSignatureAlgorithm) {
this.idTokenSignatureAlgorithm = idTokenSignatureAlgorithm;
}
@Override
public String toString() {
return "{" +
"authorizationCodeTimeToLive=" + authorizationCodeTimeToLive +
", accessTokenTimeToLive=" + accessTokenTimeToLive +
", accessTokenFormat='" + accessTokenFormat + '\'' +
", deviceCodeTimeToLive=" + deviceCodeTimeToLive +
", reuseRefreshTokens=" + reuseRefreshTokens +
", refreshTokenTimeToLive=" + refreshTokenTimeToLive +
", idTokenSignatureAlgorithm='" + idTokenSignatureAlgorithm + '\'' +
'}';
}
}

View File

@ -14,6 +14,7 @@ package com.monkeyk.sos.service.dto;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import java.io.Serial;
import java.io.Serializable;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@ -25,6 +26,7 @@ import java.util.List;
* @author Shengzhao Li
*/
public class UserDto implements Serializable {
@Serial
private static final long serialVersionUID = -2502329463915439215L;
@ -39,6 +41,30 @@ public class UserDto implements Serializable {
private List<Privilege> privileges = new ArrayList<>();
/**
* true
* false
*
* @since 3.0.0
*/
private boolean enabled = true;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#PROFILE
* @since 3.0.0
*/
private String nickname;
/**
*
*
* @see org.springframework.security.oauth2.core.oidc.OidcScopes#ADDRESS
* @since 3.0.0
*/
private String address;
public UserDto() {
}
@ -52,6 +78,35 @@ public class UserDto implements Serializable {
this.privileges = user.privileges();
this.createTime = user.createTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
this.enabled = user.enabled();
this.address = user.address();
this.nickname = user.nickname();
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCreateTime() {

View File

@ -4,12 +4,15 @@ import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.infrastructure.PasswordHandler;
import java.io.Serial;
/**
* 2016/3/25
*
* @author Shengzhao Li
*/
public class UserFormDto extends UserDto {
@Serial
private static final long serialVersionUID = 7959857016962260738L;
@ -38,6 +41,10 @@ public class UserFormDto extends UserDto {
.email(getEmail())
.password(PasswordHandler.encode(getPassword()));
user.privileges().addAll(getPrivileges());
//v3.0.0 added
user.address(getAddress())
.nickname(getNickname())
.enabled(isEnabled());
return user;
}
}

View File

@ -3,6 +3,7 @@ package com.monkeyk.sos.service.dto;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@ -12,7 +13,7 @@ import java.util.List;
*/
public class UserJsonDto implements Serializable {
@Serial
private static final long serialVersionUID = -704681024783524371L;
private String guid;

View File

@ -1,5 +1,6 @@
package com.monkeyk.sos.service.dto;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@ -10,6 +11,7 @@ import java.util.List;
* @author Shengzhao Li
*/
public class UserOverviewDto implements Serializable {
@Serial
private static final long serialVersionUID = 2023379587030489248L;

View File

@ -1,16 +1,14 @@
package com.monkeyk.sos.service.impl;
import com.monkeyk.sos.service.dto.OauthClientDetailsDto;
import com.monkeyk.sos.domain.oauth.OauthClientDetails;
import com.monkeyk.sos.domain.oauth.OauthRepository;
import com.monkeyk.sos.service.OauthService;
import com.monkeyk.sos.service.dto.OauthClientDetailsDto;
import com.monkeyk.sos.web.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@ -27,38 +25,42 @@ public class OauthServiceImpl implements OauthService {
private OauthRepository oauthRepository;
@Override
@Transactional(readOnly = true)
// @Transactional(readOnly = true)
public OauthClientDetails loadOauthClientDetails(String clientId) {
return oauthRepository.findOauthClientDetails(clientId);
}
@Override
@Transactional(readOnly = true)
// @Transactional(readOnly = true)
public List<OauthClientDetailsDto> loadAllOauthClientDetailsDtos() {
List<OauthClientDetails> clientDetailses = oauthRepository.findAllOauthClientDetails();
return OauthClientDetailsDto.toDtos(clientDetailses);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
// @Transactional(propagation = Propagation.REQUIRED)
public void archiveOauthClientDetails(String clientId) {
oauthRepository.updateOauthClientDetailsArchive(clientId, true);
LOG.debug("{}|Update OauthClientDetails(clientId: {}) archive = true", WebUtils.getIp(), clientId);
if (LOG.isDebugEnabled()) {
LOG.debug("{}|Update OauthClientDetails(clientId: {}) archive = true", WebUtils.getIp(), clientId);
}
}
@Override
@Transactional(readOnly = true)
// @Transactional(readOnly = true)
public OauthClientDetailsDto loadOauthClientDetailsDto(String clientId) {
final OauthClientDetails oauthClientDetails = oauthRepository.findOauthClientDetails(clientId);
return oauthClientDetails != null ? new OauthClientDetailsDto(oauthClientDetails) : null;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
// @Transactional(propagation = Propagation.REQUIRED)
public void registerClientDetails(OauthClientDetailsDto formDto) {
OauthClientDetails clientDetails = formDto.createDomain();
oauthRepository.saveOauthClientDetails(clientDetails);
LOG.debug("{}|Save OauthClientDetails: {}", WebUtils.getIp(), clientDetails);
if (LOG.isDebugEnabled()) {
LOG.debug("{}|Save OauthClientDetails: {}", WebUtils.getIp(), clientDetails);
}
}
}

View File

@ -1,28 +1,23 @@
package com.monkeyk.sos.service.impl;
import com.monkeyk.sos.service.dto.UserDto;
import com.monkeyk.sos.service.dto.UserFormDto;
import com.monkeyk.sos.service.dto.UserJsonDto;
import com.monkeyk.sos.service.dto.UserOverviewDto;
import com.monkeyk.sos.domain.shared.security.SOSUserDetails;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.domain.user.UserRepository;
import com.monkeyk.sos.service.UserService;
import com.monkeyk.sos.service.dto.UserDto;
import com.monkeyk.sos.service.dto.UserFormDto;
import com.monkeyk.sos.service.dto.UserJsonDto;
import com.monkeyk.sos.service.dto.UserOverviewDto;
import com.monkeyk.sos.web.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
/**
@ -38,8 +33,10 @@ public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
/**
*
*/
@Override
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null || user.archived()) {
@ -50,22 +47,23 @@ public class UserServiceImpl implements UserService {
}
@Override
@Transactional(readOnly = true)
public UserJsonDto loadCurrentUserJsonDto() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
final Object principal = authentication.getPrincipal();
if (authentication instanceof OAuth2Authentication &&
(principal instanceof String || principal instanceof org.springframework.security.core.userdetails.User)) {
return loadOauthUserJsonDto((OAuth2Authentication) authentication);
} else {
final SOSUserDetails userDetails = (SOSUserDetails) principal;
return new UserJsonDto(userRepository.findByGuid(userDetails.user().guid()));
}
// if (authentication instanceof OAuth2Authentication &&
// (principal instanceof String || principal instanceof org.springframework.security.core.userdetails.User)) {
// return loadOauthUserJsonDto((OAuth2Authentication) authentication);
// } else {
final SOSUserDetails userDetails = (SOSUserDetails) principal;
return new UserJsonDto(userRepository.findByGuid(userDetails.getUserGuid()));
// }
}
/**
*
*/
@Override
@Transactional(readOnly = true)
public UserOverviewDto loadUserOverviewDto(UserOverviewDto overviewDto) {
List<User> users = userRepository.findUsersByUsername(overviewDto.getUsername());
overviewDto.setUserDtos(UserDto.toDtos(users));
@ -73,14 +71,12 @@ public class UserServiceImpl implements UserService {
}
@Override
@Transactional(readOnly = true)
public boolean isExistedUsername(String username) {
final User user = userRepository.findByUsername(username);
return user != null;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public String saveUser(UserFormDto formDto) {
User user = formDto.newUser();
userRepository.saveUser(user);
@ -89,15 +85,15 @@ public class UserServiceImpl implements UserService {
}
private UserJsonDto loadOauthUserJsonDto(OAuth2Authentication oAuth2Authentication) {
UserJsonDto userJsonDto = new UserJsonDto();
userJsonDto.setUsername(oAuth2Authentication.getName());
final Collection<GrantedAuthority> authorities = oAuth2Authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
userJsonDto.getPrivileges().add(authority.getAuthority());
}
return userJsonDto;
}
// private UserJsonDto loadOauthUserJsonDto(OAuth2Authentication oAuth2Authentication) {
// UserJsonDto userJsonDto = new UserJsonDto();
// userJsonDto.setUsername(oAuth2Authentication.getName());
//
// final Collection<GrantedAuthority> authorities = oAuth2Authentication.getAuthorities();
// for (GrantedAuthority authority : authorities) {
// userJsonDto.getPrivileges().add(authority.getAuthority());
// }
//
// return userJsonDto;
// }
}

View File

@ -1,8 +1,8 @@
package com.monkeyk.sos.web;
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
/**
* @author Shengzhao Li
@ -16,7 +16,7 @@ public abstract class WebUtils {
/**
* Sync by pom.xml <version></version>
*/
public static final String VERSION = "2.1.0";
public static final String VERSION = "3.0.0";
private static ThreadLocal<String> ipThreadLocal = new ThreadLocal<>();

View File

@ -0,0 +1,22 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
* 2023/10/31 10:35
*
* @author Shengzhao Li
* @since 3.0.0
*/
public abstract class AbstractAuthenticationRestConverter implements AuthenticationRestConverter {
static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
protected void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
}

View File

@ -0,0 +1,24 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import java.util.Map;
/**
* 2023/10/31 10:27
*
* @author Shengzhao Li
* @see org.springframework.security.web.authentication.AuthenticationConverter
* @since 3.0.0
*/
public interface AuthenticationRestConverter {
/**
* Authentication
*
* @param parameters
* @return Authentication or null
*/
Authentication convert(Map<String, String> parameters);
}

View File

@ -0,0 +1,45 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 2023/10/31 10:30
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter
* @since 3.0.0
*/
public final class DelegatingAuthenticationRestConverter implements AuthenticationRestConverter {
private final List<AuthenticationRestConverter> converters;
/**
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
*
* @param converters a {@code List} of {@link AuthenticationConverter}(s)
*/
public DelegatingAuthenticationRestConverter(List<AuthenticationRestConverter> converters) {
Assert.notEmpty(converters, "converters cannot be empty");
this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
}
@Override
public Authentication convert(Map<String, String> parameters) {
Assert.notNull(parameters, "parameters cannot be null");
for (AuthenticationRestConverter converter : this.converters) {
Authentication authentication = converter.convert(parameters);
if (authentication != null) {
return authentication;
}
}
return null;
}
}

View File

@ -0,0 +1,69 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 2023/10/31 10:33
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter
* @since 3.0.0
*/
public final class OAuth2AuthorizationCodeAuthenticationRestConverter extends AbstractAuthenticationRestConverter {
@Override
public Authentication convert(Map<String, String> parameters) {
// grant_type (REQUIRED)
String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// code (REQUIRED)
String code = parameters.get(OAuth2ParameterNames.CODE);
if (!StringUtils.hasText(code)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.CODE,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// redirect_uri (REQUIRED)
// Required only if the "redirect_uri" parameter was included in the authorization request
String redirectUri = parameters.get(OAuth2ParameterNames.REDIRECT_URI);
if (!StringUtils.hasText(redirectUri)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.REDIRECT_URI,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.CODE) &&
!key.equals(OAuth2ParameterNames.REDIRECT_URI)) {
// additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
additionalParameters.put(key, value);
}
});
return new OAuth2AuthorizationCodeAuthenticationToken(
code, clientPrincipal, redirectUri, additionalParameters);
}
}

View File

@ -0,0 +1,60 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* 2023/10/31 10:33
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter
* @since 3.0.0
*/
public final class OAuth2ClientCredentialsAuthenticationRestConverter extends AbstractAuthenticationRestConverter {
@Override
public Authentication convert(Map<String, String> parameters) {
// grant_type (REQUIRED)
String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// scope (OPTIONAL)
String scope = parameters.get(OAuth2ParameterNames.SCOPE);
// if (StringUtils.hasText(scope) &&
// parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
// throwError(
// OAuth2ErrorCodes.INVALID_REQUEST,
// OAuth2ParameterNames.SCOPE,
// ACCESS_TOKEN_REQUEST_ERROR_URI);
// }
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value);
}
});
return new OAuth2ClientCredentialsAuthenticationToken(
clientPrincipal, requestedScopes, additionalParameters);
}
}

View File

@ -0,0 +1,56 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationToken;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 2023/10/31 10:33
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter
* @since 3.0.0
*/
public final class OAuth2DeviceCodeAuthenticationRestConverter extends AbstractAuthenticationRestConverter {
@Override
public Authentication convert(Map<String, String> parameters) {
// grant_type (REQUIRED)
String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.DEVICE_CODE.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// device_code (REQUIRED)
String deviceCode = parameters.get(OAuth2ParameterNames.DEVICE_CODE);
if (!StringUtils.hasText(deviceCode)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.DEVICE_CODE,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
!key.equals(OAuth2ParameterNames.DEVICE_CODE)) {
additionalParameters.put(key, value);
}
});
return new OAuth2DeviceCodeAuthenticationToken(deviceCode, clientPrincipal, additionalParameters);
}
}

View File

@ -0,0 +1,70 @@
package com.monkeyk.sos.web.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* 2023/10/31 10:33
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter
* @since 3.0.0
*/
public final class OAuth2RefreshTokenAuthenticationRestConverter extends AbstractAuthenticationRestConverter {
@Override
public Authentication convert(Map<String, String> parameters) {
// grant_type (REQUIRED)
String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// refresh_token (REQUIRED)
String refreshToken = parameters.get(OAuth2ParameterNames.REFRESH_TOKEN);
if (!StringUtils.hasText(refreshToken)) {
throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.REFRESH_TOKEN,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// scope (OPTIONAL)
String scope = parameters.get(OAuth2ParameterNames.SCOPE);
// if (!StringUtils.hasText(scope)) {
// throwError(
// OAuth2ErrorCodes.INVALID_REQUEST,
// OAuth2ParameterNames.SCOPE,
// ACCESS_TOKEN_REQUEST_ERROR_URI);
// }
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.REFRESH_TOKEN) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value);
}
});
return new OAuth2RefreshTokenAuthenticationToken(
refreshToken, clientPrincipal, requestedScopes, additionalParameters);
}
}

View File

@ -1,5 +1,6 @@
package com.monkeyk.sos.web.context;
import com.monkeyk.sos.web.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@ -7,7 +8,6 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.util.Assert;
/**
@ -82,9 +82,13 @@ public class SOSContextHolder implements BeanFactoryAware, InitializingBean {
public void afterPropertiesSet() throws Exception {
Assert.notNull(beanFactory, "beanFactory is null");
if (LOG.isDebugEnabled()) {
TokenStore tokenStore = getBean(TokenStore.class);
LOG.debug("{} use tokenStore: {}", this.applicationName, tokenStore);
// if (LOG.isDebugEnabled()) {
// TokenStore tokenStore = getBean(TokenStore.class);
// LOG.debug("{} use tokenStore: {}", this.applicationName, tokenStore);
// }
if (LOG.isInfoEnabled()) {
LOG.info("{} context initialized, version: {}", this.applicationName, WebUtils.VERSION);
}
}

View File

@ -0,0 +1,176 @@
package com.monkeyk.sos.web.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.security.Principal;
import java.util.*;
import static com.monkeyk.sos.domain.shared.SOSConstants.AUTHORIZATION_ENDPOINT_URI;
import static com.monkeyk.sos.domain.shared.SOSConstants.DEVICE_VERIFICATION_ENDPOINT_URI;
/**
* 2023/10/18
* <p>
* consent flow
*
* @author shengzhao Li
* @since 3.0.0
*/
@Controller
public class AuthorizationConsentController {
private static final Logger LOG = LoggerFactory.getLogger(AuthorizationConsentController.class);
@Autowired
private RegisteredClientRepository registeredClientRepository;
@Autowired
private OAuth2AuthorizationConsentService authorizationConsentService;
/**
* consent
*/
@GetMapping(value = "/oauth2/consent")
public String consent(Principal principal, Model model,
@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
@RequestParam(OAuth2ParameterNames.SCOPE) String scope,
@RequestParam(OAuth2ParameterNames.STATE) String state,
@RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {
//再次检查 client
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
if (LOG.isWarnEnabled()) {
LOG.warn("Not found RegisteredClient by clientId: {}", clientId);
}
model.addAttribute("error", "Invalid client_id: " + clientId);
return "consent_error";
}
// Remove scopes that were already approved
Set<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
Set<String> authorizedScopes;
if (currentAuthorizationConsent != null) {
authorizedScopes = currentAuthorizationConsent.getScopes();
} else {
authorizedScopes = Collections.emptySet();
}
for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
if (OidcScopes.OPENID.equals(requestedScope)) {
continue;
}
if (authorizedScopes.contains(requestedScope)) {
previouslyApprovedScopes.add(requestedScope);
} else {
scopesToApprove.add(requestedScope);
}
}
model.addAttribute("clientId", clientId);
model.addAttribute("state", state);
model.addAttribute("scopes", withDescription(scopesToApprove));
model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
model.addAttribute("principalName", principal.getName());
model.addAttribute("userCode", userCode);
if (StringUtils.hasText(userCode)) {
model.addAttribute("requestURI", DEVICE_VERIFICATION_ENDPOINT_URI);
} else {
model.addAttribute("requestURI", AUTHORIZATION_ENDPOINT_URI);
}
return "consent";
}
private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
for (String scope : scopes) {
scopeWithDescriptions.add(new ScopeWithDescription(scope));
}
return scopeWithDescriptions;
}
public static class ScopeWithDescription {
private static final String DEFAULT_DESCRIPTION
= "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
private static final Map<String, String> SCOPE_DESCRIPTIONS = new HashMap<>();
static {
SCOPE_DESCRIPTIONS.put(
OidcScopes.PROFILE,
"This application will be able to read your profile information."
);
SCOPE_DESCRIPTIONS.put(
OidcScopes.EMAIL,
"This application will be able to read your email information."
);
SCOPE_DESCRIPTIONS.put(
OidcScopes.PHONE,
"This application will be able to read your phone information."
);
SCOPE_DESCRIPTIONS.put(
OidcScopes.ADDRESS,
"This application will be able to read your address information."
);
SCOPE_DESCRIPTIONS.put(
"message.read",
"This application will be able to read your message."
);
SCOPE_DESCRIPTIONS.put(
"message.write",
"This application will be able to add new messages. It will also be able to edit and delete existing messages."
);
SCOPE_DESCRIPTIONS.put(
"other.scope",
"This is another scope example of a scope description."
);
}
public final String scope;
public final String description;
ScopeWithDescription(String scope) {
this.scope = scope;
this.description = SCOPE_DESCRIPTIONS.getOrDefault(scope, DEFAULT_DESCRIPTION);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ScopeWithDescription that = (ScopeWithDescription) o;
return Objects.equals(scope, that.scope) && Objects.equals(description, that.description);
}
@Override
public int hashCode() {
return Objects.hash(scope, description);
}
}
}

View File

@ -1,23 +1,25 @@
package com.monkeyk.sos.web.controller;
import com.monkeyk.sos.infrastructure.PKCEUtils;
import com.monkeyk.sos.service.dto.OauthClientDetailsDto;
import com.monkeyk.sos.service.OauthService;
import com.monkeyk.sos.web.oauth.OauthClientDetailsDtoValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Handle 'client_details' management
* <p>
* v3.0.0 'RegisteredClient', table: oauth2_registered_client
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.client.RegisteredClient
*/
@Controller
public class ClientDetailsController {
@ -38,39 +40,50 @@ public class ClientDetailsController {
}
/*
* Logic delete
* */
/**
* Logic delete
*/
@RequestMapping("archive_client/{clientId}")
public String archiveClient(@PathVariable("clientId") String clientId) {
oauthService.archiveOauthClientDetails(clientId);
return "redirect:../client_details";
}
/*
* Test client
* */
@RequestMapping("test_client/{clientId}")
/**
* Test client
*/
@GetMapping("test_client/{clientId}")
public String testClient(@PathVariable("clientId") String clientId, Model model) {
OauthClientDetailsDto clientDetailsDto = oauthService.loadOauthClientDetailsDto(clientId);
model.addAttribute("clientDetailsDto", clientDetailsDto);
//v3.0.0 added PKCE params
String codeVerifier = PKCEUtils.generateCodeVerifier();
String codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier);
model.addAttribute("codeVerifier", codeVerifier)
.addAttribute("codeChallenge", codeChallenge);
return "clientdetails/test_client";
}
/*
* Register client
* */
/**
* Register client
*/
@RequestMapping(value = "register_client", method = RequestMethod.GET)
public String registerClient(Model model) {
model.addAttribute("formDto", new OauthClientDetailsDto());
OauthClientDetailsDto formDto = new OauthClientDetailsDto();
//初始化 v3.0.0 added
formDto.setClientAuthenticationMethods("client_secret_post");
formDto.setScopes(OidcScopes.OPENID);
formDto.setAuthorizationGrantTypes(AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
model.addAttribute("formDto", formDto);
return "clientdetails/register_client";
}
/*
* Submit register client
* */
/**
* Submit register client
*/
@RequestMapping(value = "register_client", method = RequestMethod.POST)
public String submitRegisterClient(@ModelAttribute("formDto") OauthClientDetailsDto formDto, BindingResult result) {
clientDetailsDtoValidator.validate(formDto, result);

View File

@ -0,0 +1,59 @@
package com.monkeyk.sos.web.controller;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Map;
/**
* 2023/10/24 16:24
* <p>
* grant_type=jwt-bearer jwkSetUrl
* <p>
* todo: client
*
* @author Shengzhao Li
* @since 3.0.0
*/
@RestController
public class JwtBearerJwksController {
private static final Logger LOG = LoggerFactory.getLogger(JwtBearerJwksController.class);
/**
* RS256
* ? JwksTest.java
*/
public static final String RS256_KEY = "{\"p\":\"-Y5ymP0tAtOmpksf6y1rT-CsGUyklercT0vY0fMbkUyZH8igxUr0ZjXVr3Yzhlh8sJ5y5-0IEpPw7L4v7_OmCC-7t_M-ntf2-36rqIrK7AMhGf4mle4pMQhBeIJN0n91wMxmNXMwto4L3MWZ8f6K1QH1cirj3_BQsA4XXEgMMKE\",\"kty\":\"RSA\",\"q\":\"_HUwOfykJSjDkisyAK3QaNDFxik3HLTr7m0kU3UNLc1KRaNTIwPYuLaskGE4Se6Idy8TLc7NuEB96VSd9LaGakrDPBwh9ZcN8uBJVA162TCA1RUJjwO4k33uxkVo8gvNQ5ooBnEdT-rMhrjZa3ko-vLR5KCQHs6Gq6SWLBalth8\",\"d\":\"D65_9R01rDFuXc6qJKlNo8-x52jBYtDJJSxFoXW3Znek3fwTX7Le10lNKHf0EEJixnmXumIivl4hFCCBvlc-KP6P_OZZmU9JzC-gezUFdOuhfouMJh6VpbO272nqIfOU8UZJEXCxMSvOqJs-grekSqWMdEZpFytlG6hxNGVEJcy619rPdKL-xUlIliK0M4BItOn24u0Awd4msHyOz9F5UamDa8dnnuRlCJSnqUxBhvMicxP-k4ZXqx_csiVJt5GSkBU2-68T4NYPsTBqUufXsPVbThcoHI6COdWv8dQ5ovNI6P02aEUYA0-QlGVC4mPCmxo81Q8ukK5UUOvjFP7cAQ\",\"e\":\"AQAB\",\"kid\":\"jwt-bearer-demo-rsa-kid1\",\"key_ops\":[\"encrypt\",\"verify\",\"deriveKey\",\"sign\",\"decrypt\"],\"qi\":\"glJKxfNKRauPqt-yQBuiF6XyfIxSSts0ZZJRyf4CAvlXmruWlZdd2IwY4V67SPBvoOHm1o32zI0clQabPt1ovHS1fMfPuy1L2ytQUL3yVSVddhkG9otadaPQW8kuc86wLdKwUjpBREQjwNeaTxkuoJVPcbXlNsayA6h17ljceBc\",\"dp\":\"lXGWcsN6Ru0UKRVn4d_rGYSDywq4rQZeNCZJi0C4S4TBVeVBUaSXQvYOJurz5AntcZ8RVI3_fZCWgE9MSbdwwApFsdy6rUjLIMQ0a9PhvQAKvJQT60kZ5cD54_60N9AYZgKBWpTGoSvjMqwqil5SKUjpARtqJtq0lxl5J8wFcME\",\"alg\":\"RS256\",\"dq\":\"CiaAEOTKiL_x1Q-ti_9xELXMLeJ8V8gicEytGDntlLjbUp91eUPvU8XsfEWcaMSRchFPeRkGhnD5XwdK7orkLqPg46rR5rjzE5_W8u0z0kWz-F1HLBvfMPbwQcKKrKiy0RQCpfeoUQ1Euen2u-58KlLXA5U9FjABlCci7pTehss\",\"n\":\"9hp17DWgdCzJBq8T0hyV5F99-7_NtJu01yL95jZ9UF7bErGdqBtfw6_X5NmI1zMwmsAiksARr5_X7Hr3Gg2EbadLPymYAoGpaIwOZV04hHr_pJmqxNOaQU89_CDz-fmOhRoizZgxKAfWGCW1VLrKMaU3h4gs-G2gT0xQPDpkuXDV7WxYViqfLPhP94Cnk-geCeJpkY9q9BFZGkqW9mYeb2Ut1owlgY-Rfz-RID5gqGjL_AS3DYvvNf9_4eI8v3ahqRKDUXccw_sntEwBs95zWbRXQXBHgIKNIKp4ITnsN7OPc66QlJSpzqSkeOx0fvnCJ5bIh4fViqqLtp0akdFZfw\"}";
/**
* ES256
*/
public static final String ES256_KEY = "{\"kty\":\"EC\",\"d\":\"J6ZIiWeVp4fTXAp5W2w9nw7lACkGaAjOAuLOlrzATDo\",\"crv\":\"P-256\",\"kid\":\"jwt-bearer-demo-ecc-kid\",\"key_ops\":[\"sign\",\"verify\",\"encrypt\",\"deriveKey\",\"decrypt\"],\"x\":\"fJ4RA2IawTPMIWx7bqlYTzrjM8Gl4YQMNRaX4isqeDI\",\"y\":\"sBeszsJArg2sdc1AdrxIyDIgDIVw84KWF27FsnkQenc\",\"alg\":\"ES256\"}";
/**
* client jwks
* public-key
*
* @see org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter
*/
@GetMapping("/api/public/oauth2/jwt_bearer/demo_jwks")
public Map<String, Object> jwks() throws Exception {
JWK rsJwk = JWK.parse(RS256_KEY);
JWK esJwk = JWK.parse(ES256_KEY);
JWKSet jwkSet = new JWKSet(Arrays.asList(rsJwk, esJwk));
//注意:只返回 publicKey
return jwkSet.toJSONObject(true);
}
}

View File

@ -0,0 +1,35 @@
package com.monkeyk.sos.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import static com.monkeyk.sos.domain.shared.SOSConstants.DEVICE_VERIFICATION_ENDPOINT_URI;
/**
* 2023/10/17 18:49
* <p>
* Device code flow use
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter
* @since 3.0.0
*/
@Controller
public class OAuth2DeviceVerificationController {
/**
* Device verification page
*
* @return view
*/
@RequestMapping(value = DEVICE_VERIFICATION_ENDPOINT_URI, method = {RequestMethod.GET, RequestMethod.POST})
public String deviceVerification() {
return "device_verification";
}
}

View File

@ -11,38 +11,49 @@
*/
package com.monkeyk.sos.web.controller;
import com.monkeyk.sos.config.OAuth2ServerConfiguration;
import com.monkeyk.sos.web.WebUtils;
import com.monkeyk.sos.web.authentication.*;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.*;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestValidator;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Collections;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Map;
/**
@ -51,178 +62,217 @@ import java.util.Map;
* Restful OAuth API
*
* @author Shengzhao Li
* @see org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
* @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter
* @since 2.0.0
*/
@Controller
public class OAuthRestController implements InitializingBean, ApplicationContextAware {
public class OAuthRestController {
private static final Logger LOG = LoggerFactory.getLogger(OAuthRestController.class);
@Autowired
private ClientDetailsService clientDetailsService;
private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final String CLIENT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
private final AuthenticationRestConverter authenticationConverter;
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
private AuthenticationManager authenticationManager;
// consumerTokenServices,defaultAuthorizationServerTokenServices
@Autowired
@Qualifier("defaultAuthorizationServerTokenServices")
private AuthorizationServerTokenServices tokenServices;
private ApplicationContext applicationContext;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
private RegisteredClientRepository registeredClientRepository;
@Autowired
private PasswordEncoder passwordEncoder;
private AuthenticationManager authenticationManager;
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
@Autowired
private AuthorizationServerSettings authorizationServerSettings;
@RequestMapping(value = "/oauth/rest_token", method = RequestMethod.POST)
public OAuthRestController() {
this.authenticationConverter = new DelegatingAuthenticationRestConverter(
Arrays.asList(
new OAuth2AuthorizationCodeAuthenticationRestConverter(),
new OAuth2RefreshTokenAuthenticationRestConverter(),
new OAuth2ClientCredentialsAuthenticationRestConverter(),
new OAuth2DeviceCodeAuthenticationRestConverter()));
}
/**
* Replace OAuth2TokenEndpointFilter flow use restful API
*
* @param parameters request params
* @see org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider
*/
@PostMapping("/oauth2/rest_token")
@ResponseBody
public OAuth2AccessToken postAccessToken(@RequestBody Map<String, String> parameters) {
public void postAccessToken(@RequestBody Map<String, String> parameters, HttpServletResponse response)
throws OAuth2AuthenticationException, IOException {
//init OAuth2 contexts
initialOAuth2Contexts(parameters);
String clientId = getClientId(parameters);
ClientDetails authenticatedClient = clientDetailsService.loadClientByClientId(clientId);
// oauth2 flow start...
String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
if (grantType == null) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.GRANT_TYPE);
}
//validate client_secret
String clientSecret = getClientSecret(parameters);
if (clientSecret == null || clientSecret.equals("")) {
throw new InvalidClientException("Bad client credentials");
} else {
if (!this.passwordEncoder.matches(clientSecret, authenticatedClient.getClientSecret())) {
throw new InvalidClientException("Bad client credentials");
Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(parameters);
if (authorizationGrantAuthentication == null) {
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);
}
if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authorizationGrantAuthentication)
// .setDetails(this.authenticationDetailsSource.buildDetails(request));
.setDetails(new WebAuthenticationDetails(WebUtils.getIp(), null));
}
checkAndInitialAuthenticationManager();
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
this.sendAccessTokenResponse(response, accessTokenAuthentication);
}
private void initialOAuth2Contexts(Map<String, String> parameters) {
String clientId = parameters.get(OAuth2ParameterNames.CLIENT_ID);
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (registeredClient == null) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Retrieved registered client");
}
if (!registeredClient.getClientAuthenticationMethods().contains(
ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
throwInvalidClient("authentication_method");
}
String clientSecret = parameters.get(OAuth2ParameterNames.CLIENT_SECRET);
if (clientSecret == null) {
throwInvalidClient("credentials");
}
// String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}
if (registeredClient.getClientSecretExpiresAt() != null &&
Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
throwInvalidClient("client_secret_expires_at");
}
if (LOG.isTraceEnabled()) {
LOG.trace("Authenticated client secret");
}
OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
// init AuthorizationServerContext
AuthorizationServerContext authorizationServerContext = new AuthorizationServerContext() {
@Override
public String getIssuer() {
return authorizationServerSettings.getIssuer();
}
}
TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
@Override
public AuthorizationServerSettings getAuthorizationServerSettings() {
return authorizationServerSettings;
}
};
AuthorizationServerContextHolder.setContext(authorizationServerContext);
}
/**
*
*/
@ExceptionHandler(OAuth2AuthenticationException.class)
public void handleOAuth2AuthenticationException(OAuth2AuthenticationException ex, HttpServletResponse response) throws IOException {
SecurityContextHolder.clearContext();
if (LOG.isTraceEnabled()) {
LOG.trace("Token request failed: {}", ex.getError(), ex);
}
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
final String grantType = tokenRequest.getGrantType();
if (!StringUtils.hasText(grantType)) {
throw new InvalidRequestException("Missing grant type");
}
if (grantType.equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
LOG.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String>emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
OAuth2AccessToken token = getTokenGranter(grantType).grant(grantType, tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + grantType);
}
return token;
}
protected TokenGranter getTokenGranter(String grantType) {
if ("authorization_code".equals(grantType)) {
return new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, this.oAuth2RequestFactory);
} else if ("password".equals(grantType)) {
return new ResourceOwnerPasswordTokenGranter(getAuthenticationManager(), tokenServices, clientDetailsService, this.oAuth2RequestFactory);
} else if ("refresh_token".equals(grantType)) {
return new RefreshTokenGranter(tokenServices, clientDetailsService, this.oAuth2RequestFactory);
} else if ("client_credentials".equals(grantType)) {
return new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, this.oAuth2RequestFactory);
} else if ("implicit".equals(grantType)) {
return new ImplicitTokenGranter(tokenServices, clientDetailsService, this.oAuth2RequestFactory);
} else {
throw new UnsupportedGrantTypeException("Unsupport grant_type: " + grantType);
}
this.sendErrorResponse(response, ex);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(e);
}
@ExceptionHandler(ClientRegistrationException.class)
public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(new BadClientCredentialsException());
}
@ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
return getExceptionTranslator().translate(e);
}
private boolean isRefreshTokenRequest(Map<String, String> parameters) {
return "refresh_token".equals(parameters.get(OAuth2Utils.GRANT_TYPE)) && parameters.get("refresh_token") != null;
}
private boolean isAuthCodeRequest(Map<String, String> parameters) {
return "authorization_code".equals(parameters.get(OAuth2Utils.GRANT_TYPE)) && parameters.get("code") != null;
}
protected String getClientId(Map<String, String> parameters) {
return parameters.get(OAuth2Utils.CLIENT_ID);
}
protected String getClientSecret(Map<String, String> parameters) {
return parameters.get("client_secret");
}
private AuthenticationManager getAuthenticationManager() {
return this.authenticationManager;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
Assert.state(authenticationManager != null, "AuthenticationManager must be provided");
Assert.notNull(this.passwordEncoder, "PasswordEncoder is null");
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
protected WebResponseExceptionTranslator getExceptionTranslator() {
return providerExceptionHandler;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
private void checkAndInitialAuthenticationManager() {
if (this.authenticationManager == null) {
this.authenticationManager = (AuthenticationManager) applicationContext.getBean("authenticationManagerBean");
OAuth2ServerConfiguration serverConfiguration = applicationContext.getBean(OAuth2ServerConfiguration.class);
this.authenticationManager = serverConfiguration.authenticationManagerOAuth2();
Assert.notNull(this.authenticationManager, "authenticationManager cannot be null");
}
}
private void sendErrorResponse(HttpServletResponse response,
AuthenticationException exception) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder =
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
private static void throwError(String errorCode, String parameterName) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
private static void throwInvalidClient(String parameterName) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_CLIENT,
"Client authentication failed: " + parameterName,
CLIENT_ERROR_URI
);
throw new OAuth2AuthenticationException(error);
}
}

View File

@ -1,9 +1,11 @@
package com.monkeyk.sos.web.oauth;
package com.monkeyk.sos.web.controller;
import com.monkeyk.sos.service.dto.OauthClientDetailsDto;
import com.monkeyk.sos.service.OauthService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@ -31,17 +33,55 @@ public class OauthClientDetailsDtoValidator implements Validator {
validateClientSecret(clientDetailsDto, errors);
validateGrantTypes(clientDetailsDto, errors);
//v3.0.0 added
validateClientName(clientDetailsDto, errors);
validateScopes(clientDetailsDto, errors);
validateMethods(clientDetailsDto, errors);
}
/**
* @since 3.0.0
*/
private void validateMethods(OauthClientDetailsDto clientDetailsDto, Errors errors) {
String methods = clientDetailsDto.getClientAuthenticationMethods();
if (StringUtils.isBlank(methods)) {
errors.reject(null, "authentication_methods is required");
}
}
/**
* @since 3.0.0
*/
private void validateScopes(OauthClientDetailsDto clientDetailsDto, Errors errors) {
String scopes = clientDetailsDto.getScopes();
if (StringUtils.isBlank(scopes)) {
errors.reject(null, "scopes is required");
} else if (!scopes.contains(OidcScopes.OPENID)) {
errors.reject(null, "scopes [openid] must be selected");
}
}
/**
* @since 3.0.0
*/
private void validateClientName(OauthClientDetailsDto clientDetailsDto, Errors errors) {
String clientName = clientDetailsDto.getClientName();
if (StringUtils.isBlank(clientName)) {
errors.reject(null, "client_name is required");
}
}
private void validateGrantTypes(OauthClientDetailsDto clientDetailsDto, Errors errors) {
final String grantTypes = clientDetailsDto.getAuthorizedGrantTypes();
final String grantTypes = clientDetailsDto.getAuthorizationGrantTypes();
if (StringUtils.isEmpty(grantTypes)) {
errors.rejectValue("authorizedGrantTypes", null, "grant_type(s) is required");
errors.rejectValue("authorizationGrantTypes", null, "grant_type(s) is required");
return;
}
if ("refresh_token".equalsIgnoreCase(grantTypes)) {
errors.rejectValue("authorizedGrantTypes", null, "grant_type(s) 不能只是[refresh_token]");
errors.rejectValue("authorizationGrantTypes", null, "grant_type(s) 不能只是[refresh_token]");
}
}
@ -52,8 +92,8 @@ public class OauthClientDetailsDtoValidator implements Validator {
return;
}
if (clientSecret.length() < 8) {
errors.rejectValue("clientSecret", null, "client_secret 长度至少8位");
if (clientSecret.length() < 10) {
errors.rejectValue("clientSecret", null, "client_secret 长度至少10位");
}
}
@ -64,8 +104,8 @@ public class OauthClientDetailsDtoValidator implements Validator {
return;
}
if (clientId.length() < 5) {
errors.rejectValue("clientId", null, "client_id 长度至少5位");
if (clientId.length() < 10) {
errors.rejectValue("clientId", null, "client_id 长度至少10位");
return;
}

View File

@ -40,4 +40,16 @@ public class SOSController {
}
// /**
// * 403 无权限访问
// *
// * @return view
// * @since 3.0.0
// */
// @GetMapping("/access_denied")
// public String accessDenied() {
// return "access_denied";
// }
}

View File

@ -3,7 +3,7 @@ package com.monkeyk.sos.web.controller;
import com.monkeyk.sos.service.dto.UserFormDto;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
@ -48,6 +48,8 @@ public class UserFormDtoValidator implements Validator {
final String password = formDto.getPassword();
if (StringUtils.isEmpty(password)) {
errors.rejectValue("password", null, "Password is required");
} else if (password.length() < 10) {
errors.rejectValue("password", null, "Password length must be >= 10");
}
}

View File

@ -1,20 +1,22 @@
package com.monkeyk.sos.web.filter;
import com.monkeyk.sos.web.WebUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 2016/1/30
*
* @author Shengzhao Li
* @since 1.0.0
*/
public class CharacterEncodingIPFilter extends CharacterEncodingFilter {
@ -30,6 +32,8 @@ public class CharacterEncodingIPFilter extends CharacterEncodingFilter {
private void recordIP(HttpServletRequest request) {
final String ip = WebUtils.retrieveClientIp(request);
WebUtils.setIp(ip);
LOG.debug("Send request uri: {}, from IP: {}", request.getRequestURI(), ip);
if (LOG.isDebugEnabled()) {
LOG.debug("Send request uri: {}, from IP: {}", request.getRequestURI(), ip);
}
}
}

View File

@ -1,31 +0,0 @@
package com.monkeyk.sos.web.filter;
import org.sitemesh.builder.SiteMeshFilterBuilder;
import org.sitemesh.config.ConfigurableSiteMeshFilter;
/**
* 2018/2/3
* <p>
* Replace decorator.xml
* <p>
* Sitemesh
*
* @author Shengzhao Li
*/
public class SOSSiteMeshFilter extends ConfigurableSiteMeshFilter {
public SOSSiteMeshFilter() {
}
@Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
builder.addDecoratorPath("/*", "/WEB-INF/jsp/decorators/main.jsp")
.addExcludedPath("/static/**");
}
}

View File

@ -1,36 +0,0 @@
package com.monkeyk.sos.web.oauth;
import com.monkeyk.sos.domain.oauth.OauthClientDetails;
import com.monkeyk.sos.service.OauthService;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler;
/**
* @author Shengzhao Li
*/
public class OauthUserApprovalHandler extends TokenStoreUserApprovalHandler {
private OauthService oauthService;
public OauthUserApprovalHandler() {
}
public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
if (super.isApproved(authorizationRequest, userAuthentication)) {
return true;
}
if (!userAuthentication.isAuthenticated()) {
return false;
}
OauthClientDetails clientDetails = oauthService.loadOauthClientDetails(authorizationRequest.getClientId());
return clientDetails != null && clientDetails.trusted();
}
public void setOauthService(OauthService oauthService) {
this.oauthService = oauthService;
}
}

View File

@ -11,27 +11,16 @@ spring.datasource.password=andaily
#Datasource properties
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=2
#spring.datasource.hikari.minimum-idle=2
#
# MVC
spring.mvc.ignore-default-model-on-redirect=false
spring.http.encoding.enabled=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.mvc.locale=zh_CN
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false
#
server.port=8080
#
# Logging
#
logging.level.root=INFO
#
# Support deploy to a servlet-container
spring.jmx.enabled=false
#
#
spring.main.allow-bean-definition-overriding=true
# oauth2 custom issuer, since v3.0.0
spring.security.oauth2.authorizationserver.issuer=http://127.0.0.1:${server.port}
#
# Redis
#
@ -45,7 +34,7 @@ spring.main.allow-bean-definition-overriding=true
# Condition Config
# @since 2.1.0
# Available TokenStore value: jdbc, jwt
sos.token.store=jwt
#sos.token.store=jwt
# jwt key (length >= 16), optional
# @since 2.1.0
#sos.token.store.jwt.key=IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa

View File

@ -0,0 +1,40 @@
{
"keys": [
{
"kty": "EC",
"d": "X_gLHsJlSyK4gT_qeinb2gV7enJ1_2wq_Kxk-h3f-Mo",
"crv": "P-256",
"kid": "sos-ecc-kid1",
"key_ops": [
"sign",
"deriveKey",
"decrypt",
"encrypt",
"verify"
],
"x": "UyCuPXhC0_KLRqfWPNDU4ZljSx7lQ_vP7VbYDiOZmsk",
"y": "2HuQhn3bfkmYiB6BLQKlN8tkI8awkeOiKaNk1cu06ow",
"alg": "ES256"
},
{
"p": "1IKQCCAPhMgxUbgGa9Yjsowt3Q7rUjF68GBW0BF3QaY6zdrt1tGRLd_wVGq4uLBlb0jUUV591YOdYQHYpqgjozMfmpSG6UxikUGzzNihB0-9pczWxGe03hbLr5M3ueDIEBh81_aigSwnUGTTYCZhUPRewlJSkPg2SlXWfrB8tYU",
"kty": "RSA",
"q": "13hSjzOO8BjVbcjfa2QsyDMVLcclagFLeaTejBZG_ZDRpvvq6zL9MyghGc5q-qlMxZCZwci8WOCyPwKfvB7Ca_3fdKVL0U7VSyTuXTRX1OCpxoOj6IbxzuzWeFEAwEkL6PeRPYFz-bgWd955NdCCS5rL11SBQneIIavtYTKiKkk",
"d": "F6t-8VhYR5Sy_7rNo5S75wxLgxlKc_WMqGsd39xcebdCY7MQnFxHq0_GUOq-RQKmhqydJXKdC3rElopxeojUmbX1mlnznjlv8Yu5JTVq5kMELuzl0-MyqeyHCM027p_-gjShNSLhhR3I8_GUZGvt-6q6H4yaGGGx9t1bbAjnLYQK-4zzl2VcNqHETIDYwhi626FGy1uZCHIDsojeVgW7HQAx26HAGBIkPMbiFCINLQRf-cOsEX4ksKfrgbH5QOG16yZObYHy1Ulx0HKgP_GaaqliZ6C-6-w05Umv6V_KY9qQiehFAFVRJ82lZtQ3HV1Ivoxi4U-ptYSaMGkDOqij2Q",
"e": "AQAB",
"kid": "sos-rsa-kid2",
"key_ops": [
"deriveKey",
"verify",
"encrypt",
"decrypt",
"sign"
],
"qi": "jetZOG6EMEDAoeAy8RiJxHFnFJMOqGULd5wkPwAi6LV9wt8dgdxj_rocK0a4ksSfEu5EFeuJ8lPVpBwMJhZh5j2YJvmVzC7FxhH2sQ3FD-tu6hwU9IhnRLm2CeEaSG92upWUoZCRnLwVpKamOVJjJAk19TmL7FUGt93a3Gemb88",
"dp": "ry5mH1yWjmYdSflCydiAGuq10BpBYMNLTiaMyf7r6WFn7lTAZariXAfT7TMAzbcUFzXZWK5lWwKhVNuZxmCq6Bj3v40a3e1K_-VCm-YkcIuKkcgXb1byYXY3OKhKct9a7PHS0JEPCx7j1cEYApYA-SRJjTUhvUHwNz0lkdBZLaU",
"alg": "RS256",
"dq": "Wa4lxp5x9rKPWnNJsjvue6DvRq9lfhpt3IJncizvfSgianrdiukdA4bHSCNm2U9Pucb2h_ZRljhnV9xyuWygBSyULcuCo-pI0k7buwVHLT4Yy5wMw4Iu8K4Ykdk9E8sTXvJzjALuT1h0WY3KK0DOikMyZjww1IZFraYOVe8qGak",
"n": "st2IswiZyQXHy86KBYQdEYv3sAfWpyx-e4o0Dcqvpck0E1FpZfVcFzbLy9B7YHvXv1SseVcg93iiNYgGlPDeZxPllz4-oIisDvSmEJdAidhqQxxpMeSjeQzvVu4CKjGFG9jA68pTm-KDia3Y516b4tPyKhHGIUZq2yJrNIs2QjTikYbn5AxAQ244cDPTsuEV5yqdOdyWvdlrn4WSFLiPt31MboT6et7Hmm90fwbMDSaWWb2XNo2gOnzWFwlNO2s8zK_Z1IWhmreb_XH5mW9xirrT03nbnLTLcmLtZYHFKjP55zRFDgKsXeo9BQNG3dkCsWz0N8pURaN6cuXYoYGU7Q"
}
]
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>${spring.application.name}</contextName>
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
<!--输出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n</pattern>
</encoder>
</appender>
<!--按天生成日志-->
<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<prudent>true</prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/%d{yyyy-MM-dd}/sos-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>15</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy">
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n</pattern>
</encoder>
</appender>
<logger name="com.monkeyk.sos" level="DEBUG" additivity="false">
<appender-ref ref="fileAppender"/>
</logger>
<logger name="org.springframework.security" level="DEBUG" additivity="false">
<appender-ref ref="fileAppender"/>
</logger>
<!-- root配置放最后 -->
<root level="INFO">
<!-- <appender-ref ref="fileAppender"/>-->
<appender-ref ref="console"/>
</root>
</configuration>

View File

@ -1,14 +0,0 @@
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
# The configuration for Tomcat server
############################################################
org.apache.juli.FileHandler.level = FINE
org.apache.juli.FileHandler.directory = ${catalina.base}/logs
org.apache.juli.FileHandler.prefix = error-debug.
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

View File

@ -1,8 +0,0 @@
#JDBC configuration information
jdbc.driverClassName=com.mysql.jdbc.Driver
############
# localhost
############
jdbc.url=jdbc:mysql://localhost:3306/oauth2?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8
jdbc.username=andaily
jdbc.password=andaily

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<!--annotation configuration -->
<context:annotation-config/>
<context:component-scan base-package="com.monkeyk.sos"/>
<!-- property configuration -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:spring-oauth-server.properties</value>
</list>
</property>
</bean>
<!--dataSource-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="validationQuery" value="SELECT 1"/>
<property name="testOnReturn" value="false"/>
<property name="testOnBorrow" value="true"/>
<!--Based on 100 connected user -->
<property name="maxActive" value="20"/>
<property name="maxIdle" value="5"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>

View File

@ -1,196 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.0.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!--<debug/>-->
<!--static url pattern-->
<!--<http pattern="/resources/**" security="none"/>-->
<!--
Oauth server start.............
https://github.com/spring-projects/spring-security-oauth/blob/master/docs/oauth2.md
-->
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
entry-point-ref="oauth2AuthenticationEntryPoint" use-expressions="false">
<intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY"/>
<anonymous enabled="false"/>
<http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
<csrf disabled="true"/>
</http>
<!--unity http configuration-->
<http pattern="/unity/**" create-session="never" entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager" use-expressions="false">
<anonymous enabled="false"/>
<intercept-url pattern="/unity/**" access="ROLE_UNITY,SCOPE_READ"/>
<custom-filter ref="unityResourceServer" before="PRE_AUTH_FILTER"/>
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
<csrf disabled="true"/>
</http>
<!--mobile http configuration-->
<http pattern="/m/**" create-session="never" entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager" use-expressions="false">
<anonymous enabled="false"/>
<intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>
<custom-filter ref="mobileResourceServer" before="PRE_AUTH_FILTER"/>
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
<csrf disabled="true"/>
</http>
<beans:bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<beans:property name="authenticationManager" ref="oauth2AuthenticationManager"/>
</beans:bean>
<!--unity resource server filter-->
<oauth2:resource-server id="unityResourceServer" resource-id="unity-resource" token-services-ref="tokenServices"/>
<!--mobile resource server filter-->
<oauth2:resource-server id="mobileResourceServer" resource-id="mobile-resource" token-services-ref="tokenServices"/>
<!--Config ClientDetailsService-->
<!--<oauth2:client-details-service id="clientDetailsService">-->
<!--&lt;!&ndash;unity client&ndash;&gt;-->
<!--<oauth2:client client-id="unity-client" resource-ids="unity-resource"-->
<!--authorized-grant-types="password,authorization_code,refresh_token,implicit"-->
<!--secret="unity" authorities="ROLE_UNITY" scope="read,write"/>-->
<!--&lt;!&ndash;mobile client&ndash;&gt;-->
<!--<oauth2:client client-id="mobile-client" resource-ids="mobile-resource"-->
<!--authorized-grant-types="password,authorization_code,refresh_token,implicit"-->
<!--secret="mobile" authorities="ROLE_MOBILE" scope="read,write"/>-->
<!--</oauth2:client-details-service>-->
<beans:bean id="clientDetailsService" class="com.monkeyk.sos.domain.oauth.CustomJdbcClientDetailsService">
<beans:constructor-arg index="0" ref="dataSource"/>
</beans:bean>
<!--Config token services-->
<!--<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.InMemoryTokenStore"/>-->
<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
<beans:constructor-arg index="0" ref="dataSource"/>
</beans:bean>
<beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<beans:property name="tokenStore" ref="tokenStore"/>
<beans:property name="clientDetailsService" ref="clientDetailsService"/>
<beans:property name="supportRefreshToken" value="true"/>
</beans:bean>
<!--<global-method-security pre-post-annotations="enabled" proxy-target-class="true">-->
<!--<expression-handler ref="oauth2ExpressionHandler"/>-->
<!--</global-method-security>-->
<!--<oauth2:expression-handler id="oauth2ExpressionHandler"/>-->
<!--<oauth2:web-expression-handler id="oauth2WebExpressionHandler"/>-->
<beans:bean class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory"
id="oAuth2RequestFactory">
<beans:constructor-arg name="clientDetailsService" ref="clientDetailsService"/>
</beans:bean>
<beans:bean id="oauthUserApprovalHandler" class="com.monkeyk.sos.web.oauth.OauthUserApprovalHandler">
<beans:property name="tokenStore" ref="tokenStore"/>
<beans:property name="clientDetailsService" ref="clientDetailsService"/>
<beans:property name="requestFactory" ref="oAuth2RequestFactory"/>
<beans:property name="oauthService" ref="oauthService"/>
</beans:bean>
<beans:bean id="jdbcAuthorizationCodeServices"
class="org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices">
<beans:constructor-arg index="0" ref="dataSource"/>
</beans:bean>
<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="oauthUserApprovalHandler"
user-approval-page="oauth_approval"
error-page="oauth_error">
<oauth2:authorization-code authorization-code-services-ref="jdbcAuthorizationCodeServices"/>
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password/>
</oauth2:authorization-server>
<beans:bean id="oauth2AuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<beans:bean id="oauth2ClientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<beans:constructor-arg ref="clientDetailsService"/>
</beans:bean>
<authentication-manager id="oauth2AuthenticationManager">
<authentication-provider user-service-ref="oauth2ClientDetailsUserService"/>
</authentication-manager>
<beans:bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<beans:constructor-arg>
<beans:list>
<beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="oauth2AccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
<http disable-url-rewriting="true" use-expressions="false"
authentication-manager-ref="authenticationManager">
<intercept-url pattern="/oauth/**" access="ROLE_USER,ROLE_UNITY,ROLE_MOBILE"/>
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<form-login authentication-failure-url="/login.jsp?authentication_error=1" default-target-url="/index.jsp"
login-page="/login.jsp" login-processing-url="/login.do"/>
<logout logout-success-url="/index.jsp" logout-url="/logout.do"/>
<access-denied-handler error-page="/login.jsp?authorization_error=2"/>
<anonymous/>
<csrf disabled="true"/>
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="userService">
<password-encoder hash="md5"/>
</authentication-provider>
</authentication-manager>
<!--
Oauth server end.............
-->
</beans:beans>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<!--aop-->
<aop:config>
<aop:advisor advice-ref="applicationAdvisor" pointcut="execution(* com.monkeyk.sos.service.*.*(..))"/>
</aop:config>
<!--advisor-->
<tx:advice id="applicationAdvisor" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
<tx:method name="load*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
</beans>

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="shortcut icon" href="../../static/favicon.ico" th:href="@{/favicon.ico}"/>
<title>client_details - Spring Security&OAuth2.1</title>
<th:block th:insert="~{fragments/main::header-css}"/>
<style>
.list-group li:hover {
background-color: #f9f9f9;
}
</style>
</head>
<body class="container">
<a th:href="@{/}">Home</a>
<div class="row">
<div class="col-md-10">
<h3>client_details</h3>
</div>
<div class="col-md-2">
<div class="pull-right">
<a th:href="@{register_client}" class="btn btn-success btn-sm">注册client</a>
</div>
</div>
</div>
<hr/>
<div>
<ul class="list-group">
<li th:each="cli:${clientDetailsDtoList}" class="list-group-item">
<div class="pull-right">
<div th:if="${not cli.archived}">
<a th:href="@{'test_client/' + ${cli.clientId}}">test</a>
<a th:href="@{'archive_client/' + ${cli.clientId}}" class="text-danger"
onclick="return confirm('Are you sure archive the client ?')">archive</a>
</div>
<strong th:if="${cli.archived}" class="text-muted">Archived</strong>
</div>
<h3 class="list-group-item-heading">
[[${cli.clientId}]] - <span th:text="${cli.clientName}" class="text-primary"></span>
<small th:text="${cli.authorizationGrantTypes}"></small>
</h3>
<div class="list-group-item-text text-muted">
client_id: <span class="text-danger" th:text="${cli.clientId}"></span>&nbsp;
client_secret: <span class="text-primary">***</span>&nbsp;
<br/>
grant_types: <span class="text-primary" th:text="${cli.authorizationGrantTypes}"></span>&nbsp;
authentication_methods: <span class="text-primary" th:text="${cli.clientAuthenticationMethods}"></span>&nbsp;
<br/>
scopes: <span class="text-primary" th:text="${cli.scopes}"></span>&nbsp;
redirect_uri: <span class="text-primary" th:text="${cli.redirectUris}"></span>&nbsp;
<br/>
client_id_issued: <span class="text-primary" th:text="${cli.clientIdIssuedAt}"></span>&nbsp;
client_secret_expires: <span class="text-primary" th:text="${cli.clientSecretExpiresAt}"></span>
<br/>
client_settings: <span class="text-primary" th:text="${cli.clientSettings}"></span>
<br/>
token_settings: <span class="text-primary" th:text="${cli.tokenSettings}"></span>
<br/>
create_time: <span class="text-primary" th:text="${cli.createTime}"></span>&nbsp;
archived: <strong th:class="${cli.archived?'text-warning':'text-primary'}"
th:text="${cli.archived}"></strong>&nbsp;
</div>
</li>
</ul>
<div class="help-block">
每一个item对应<code>oauth2_registered_client</code>表中的一条数据; 共<strong
th:text="${clientDetailsDtoList.size()}"></strong>条数据.
<br/>
对数据库表的详细说明请访问
<a href="https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html" target="_blank">https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html</a>
(或访问项目others目录的db_table_description_3.0.0.html文件)
</div>
</div>
<div th:replace="~{fragments/main :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,396 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="shortcut icon" href="../../static/favicon.ico" th:href="@{/favicon.ico}"/>
<title>注册client - Spring Security&OAuth2.1</title>
<th:block th:insert="~{fragments/main::header-css}"/>
<script th:src="@{/angular.min.js}" src="../../static/angular.min.js"></script>
</head>
<body class="container">
<a th:href="@{/}">Home</a>
<h2>注册client</h2>
<div ng-app>
<div class="alert alert-info">
若对OAuth2.1的<code>client_details</code>中的属性及作用不清楚,
建议你先查看项目中的<code>db_table_description.html</code>文件(位于others目录)中对表<code>oauth2_registered_client</code>的说明,
或在线访问<a href="https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html" target="_blank">db_table_description.html</a>;
因为注册client实际上是向该表中按不同的条件添加数据.
</div>
<div ng-controller="RegisterClientCtrl">
<form th:object="${formDto}" th:action="@{register_client}" th:method="post" class="form-horizontal">
<div class="form-group">
<label for="clientId" class="col-sm-2 control-label">client_id<em class="text-danger">*</em></label>
<div class="col-sm-10">
<input th:name="clientId" class="form-control" id="clientId" placeholder="client_id"
required="required" th:field="*{clientId}" minlength="10"/>
<p class="help-block">client_id必须输入,且必须唯一,长度至少10位; 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.</p>
</div>
</div>
<div class="form-group">
<label for="clientName" class="col-sm-2 control-label">client_name<em class="text-danger">*</em></label>
<div class="col-sm-10">
<input th:name="clientName" class="form-control" id="clientName" placeholder="client_name"
required="required" th:field="*{clientName}" minlength="4"/>
<em class="label label-success">OAuth2.1新增</em>
<p class="help-block">Client有意义的名称.</p>
</div>
</div>
<div class="form-group">
<label for="clientSecret" class="col-sm-2 control-label">client_secret<em
class="text-danger">*</em></label>
<div class="col-sm-10">
<input th:name="clientSecret" class="form-control" id="clientSecret" minlength="10"
placeholder="client_secret" required="required" th:field="*{clientSecret}"/>
<p class="help-block">client_secret必须输入,且长度至少10位; 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.
<br/>
<strong class="label label-warning">注意: </strong> 由于client_secret 会加密存储, 请先复制并保留client_secret值
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">authentication_methods<em
class="text-danger">*</em></label>
<div class="col-sm-10">
<label class="checkbox-inline">
<input type="checkbox" th:name="clientAuthenticationMethods" th:value="client_secret_post"
th:field="*{clientAuthenticationMethods}"/> client_secret_post
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="clientAuthenticationMethods" th:value="client_secret_basic"
th:field="*{clientAuthenticationMethods}"/> client_secret_basic
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="clientAuthenticationMethods" th:value="client_secret_jwt"
th:field="*{clientAuthenticationMethods}"/> client_secret_jwt
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="clientAuthenticationMethods" th:value="private_key_jwt"
th:field="*{clientAuthenticationMethods}"/> private_key_jwt
</label>
<br/>
<em class="label label-success">OAuth2.1新增</em>
<p class="help-block">
选择在认证时支持传递<em>client_secret</em>参数的方式;在正式环境中,此值一般不需要选择而是由后台创建时根据业务设置即可;
<br/>
<code>client_secret_jwt</code><code>private_key_jwt</code>只在grant_type=jwt-bearer中会使用到
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">scopes<em class="text-danger">*</em></label>
<div class="col-sm-10">
<label class="checkbox-inline">
<input type="checkbox" th:name="scopes" th:value="openid" th:field="*{scopes}"/> openid
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="scopes" th:value="profile" th:field="*{scopes}"/> profile
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="scopes" th:value="email" th:field="*{scopes}"/> email
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="scopes" th:value="address" th:field="*{scopes}"/> address
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="scopes" th:value="phone" th:field="*{scopes}"/> phone
</label>
<p class="help-block">scopes值由OIDC 1.0协议中定义(详见<a
href="https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims" target="_blank">#ScopeClaims</a>)<em>openid</em>必须选择;在正式环境中,此值一般不需要选择而是由后台创建时根据业务设置即可
</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">grant_type(s)<em class="text-danger">*</em></label>
<div class="col-sm-10">
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes" th:value="authorization_code"
th:field="*{authorizationGrantTypes}"/> authorization_code
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes" th:value="password"
th:field="*{authorizationGrantTypes}"/> password <em
class="label label-danger">OAuth2.1不支持</em>
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes"
value="urn:ietf:params:oauth:grant-type:device_code"
th:field="*{authorizationGrantTypes}"/> device_code <em class="label label-success">OAuth2.1新增</em>
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes"
value="urn:ietf:params:oauth:grant-type:jwt-bearer"
th:field="*{authorizationGrantTypes}"/> jwt-bearer <em class="label label-success">OAuth2.1新增</em>
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes" th:value="client_credentials"
th:field="*{authorizationGrantTypes}"/> client_credentials
</label>
<label class="checkbox-inline">
<input type="checkbox" th:name="authorizationGrantTypes" th:value="refresh_token"
th:field="*{authorizationGrantTypes}"/> refresh_token
</label>
<p class="help-block">
至少勾选一项grant_type(s), 且不能只单独勾选<code>refresh_token</code>; 当勾选<code>jwt-bearer</code>时需要将'authentication_methods'中的
<em>client_secret_jwt</em><em>private_key_jwt</em>至少勾选一个
</p>
</div>
</div>
<div class="form-group">
<label for="redirectUris" class="col-sm-2 control-label">redirect_uris</label>
<div class="col-sm-10">
<input th:name="redirectUris" id="redirectUris" placeholder="https://..." class="form-control"
th:field="*{redirectUris}"/>
<p class="help-block"><code>grant_type</code>包括<em>authorization_code</em>, 则必须输入至少一个 redirect_uri
(多个uri用半角逗号分隔)</p>
</div>
</div>
<div class="form-group">
<label for="postLogoutRedirectUris" class="col-sm-2 control-label">logout_redirect_uris</label>
<div class="col-sm-10">
<input th:name="postLogoutRedirectUris" id="postLogoutRedirectUris" placeholder="https://..."
class="form-control"
th:field="*{postLogoutRedirectUris}"/>
<p class="help-block">OAuth2 退出时<em>post</em>的客户端重定向 uri (多个uri用半角逗号分隔),可选</p>
<em class="label label-success">OAuth2.1新增</em>
</div>
</div>
<div class="form-group">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<a href="javascript:void(0);" ng-click="showMore()"><strong>更多选项...</strong></a>
</div>
</div>
<div ng-show="visible">
<div class="form-group">
<label class="col-sm-2 control-label">require_proof_key(PKCE)</label>
<div class="col-sm-10">
<label class="checkbox-inline">
<input type="radio" th:name="clientSettings.requireProofKey" th:value="true"
th:field="*{clientSettings.requireProofKey}"/> 支持PKCE
</label>
<label class="checkbox-inline">
<input type="radio" th:name="clientSettings.requireProofKey" th:value="false"
th:field="*{clientSettings.requireProofKey}"/> 不支持PKCE
</label>
<p class="help-block">是否在<em>authorization_code</em>流程中支持PKCE(Proof Key for Code Exchange)</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">授权需要用户确认</label>
<div class="col-sm-10">
<label class="checkbox-inline">
<input type="radio" th:name="clientSettings.requireAuthorizationConsent" th:value="true"
th:field="*{clientSettings.requireAuthorizationConsent}"/> Yes
</label>
<label class="checkbox-inline">
<input type="radio" th:name="clientSettings.requireAuthorizationConsent" th:value="false"
th:field="*{clientSettings.requireAuthorizationConsent}"/> No
</label>
<p class="help-block">是否在<em>authorization_code</em>流程中授权时需要用户进行确认</p>
</div>
</div>
<div class="form-group">
<label for="easa" class="col-sm-2 control-label">认证jwt签名算法</label>
<div class="col-sm-10">
<select id="easa" th:name="clientSettings.tokenEndpointAuthenticationSigningAlgorithm"
th:field="*{clientSettings.tokenEndpointAuthenticationSigningAlgorithm}"
class="form-control">
<option th:value="RS256">RS256</option>
<option th:value="ES256">ES256</option>
<option th:value="HS256">HS256</option>
</select>
<p class="help-block">选择在调用<em>/oauth2/token</em>
API时使用的签名算法(当grant_type为jwt-bearer时会用到)<code>HS256</code>是对称算法(secret是client_secret加密后的值),
<code>RS256</code><code>ES256</code>是非对称算法(public-key由提供的<em>jwk_set_url</em>获取)
</p>
</div>
</div>
<div class="form-group">
<label for="jwkSetUrl" class="col-sm-2 control-label">jwk_set_url</label>
<div class="col-sm-10">
<input type="text" class="form-control" th:name="clientSettings.jwkSetUrl"
id="jwkSetUrl" placeholder="https://..." th:field="*{clientSettings.jwkSetUrl}"/>
<p class="help-block">
设置client提供的获取jwk的URL地址(当grant_type为jwt-bearer且'认证jwt签名算法'选择<code>RS256</code><code>ES256</code>时会用到);
此URL返回的jwk格式要与<em>spring-oauth-server</em>提供的<a th:href="@{/oauth2/jwks}" target="_blank">jwks</a>一致
(参考实现类<code>JwtBearerJwksController.java</code>)
</p>
</div>
</div>
<hr/>
<div class="form-group">
<label for="authorizationCodeTimeToLive" class="col-sm-2 control-label">code有效时长</label>
<div class="col-sm-10">
<input type="number" class="form-control" th:name="tokenSettings.authorizationCodeTimeToLive"
id="authorizationCodeTimeToLive"
placeholder="300" th:field="*{tokenSettings.authorizationCodeTimeToLive}"/>
<p class="help-block">设定<em>authorization_code</em>流程中<em>code</em>的有效时长(单位:秒)默认3005分钟</p>
</div>
</div>
<div class="form-group">
<label for="deviceCodeTimeToLive" class="col-sm-2 control-label">device_code有效时长</label>
<div class="col-sm-10">
<input type="number" class="form-control" th:name="tokenSettings.deviceCodeTimeToLive"
id="deviceCodeTimeToLive"
placeholder="300" th:field="*{tokenSettings.deviceCodeTimeToLive}"/>
<p class="help-block">设定<em>device_code</em>流程中<em>code</em>的有效时长(单位:秒)默认3005分钟</p>
<em class="label label-success">OAuth2.1新增</em>
</div>
</div>
<div class="form-group">
<label for="accessTokenTimeToLive" class="col-sm-2 control-label">access_token有效时长</label>
<div class="col-sm-10">
<input type="number" class="form-control" th:name="tokenSettings.accessTokenTimeToLive"
id="accessTokenTimeToLive"
placeholder="3600" th:field="*{tokenSettings.accessTokenTimeToLive}"/>
<p class="help-block">设定客户端<em>access_token</em>的有效时长(单位:秒),可选, 若不设定值则使用默认的有效时间值(3600秒);
若设定则必须是大于0的整数值(推荐不小于60)</p>
</div>
</div>
<div class="form-group">
<label for="refreshTokenTimeToLive" class="col-sm-2 control-label">refresh_token有效时长</label>
<div class="col-sm-10">
<input type="number" class="form-control" th:name="tokenSettings.refreshTokenTimeToLive"
id="refreshTokenTimeToLive"
placeholder="43200" th:field="*{tokenSettings.refreshTokenTimeToLive}"/>
<p class="help-block">设定客户端<em>refresh_token</em>的有效时长(单位:秒),可选, 若不设定值则使用默认的有效时间值(43200秒);
若设定则必须是大于0的整数值且不能小于<em>access_token</em>有效时长</p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">复用refresh_token</label>
<div class="col-sm-10">
<label class="radio-inline">
<input type="radio" th:name="tokenSettings.reuseRefreshTokens"
th:field="*{tokenSettings.reuseRefreshTokens}" th:value="true"/> Yes
</label>
<label class="radio-inline">
<input type="radio" th:name="tokenSettings.reuseRefreshTokens"
th:field="*{tokenSettings.reuseRefreshTokens}" th:value="false"/> No
</label>
<p class="help-block">当调用refresh token API后是否继续使用之前的<em>refresh_token</em>Yes则继续使用No则每次调用refresh
token API后会返回一新的<em>refresh_token</em>值(即refresh_token只能使用一次,安全性更高)</p>
<em class="label label-success">OAuth2.1新增</em>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">access_token格式</label>
<div class="col-sm-10">
<label class="radio-inline">
<input type="radio" th:name="tokenSettings.accessTokenFormat"
th:field="*{tokenSettings.accessTokenFormat}" th:value="self-contained"/>
self-contained(jwt)
</label>
<label class="radio-inline">
<input type="radio" th:name="tokenSettings.accessTokenFormat"
th:field="*{tokenSettings.accessTokenFormat}" th:value="reference"/> reference(uuid)
</label>
<p class="help-block">设置<em>access_token</em>值的格式,<code>self-contained</code>使用JWT格式(默认)<code>reference</code>使用类UUID格式
</p>
<em class="label label-success">OAuth2.1新增</em>
</div>
</div>
<div class="form-group">
<label for="idTokenSignatureAlgorithm" class="col-sm-2 control-label">id_token签名使用的算法</label>
<div class="col-sm-10">
<select id="idTokenSignatureAlgorithm" th:name="tokenSettings.idTokenSignatureAlgorithm"
th:field="*{tokenSettings.idTokenSignatureAlgorithm}"
class="form-control">
<option th:value="RS256">RS256</option>
<option th:value="ES256">ES256</option>
</select>
<p class="help-block">选择生成<em>id_token</em>时使用的算法;注意:支持的算法要有对应用<em>jwk</em> (jwks.json
文件)判断支持哪些key可访问
<a th:href="@{/.well-known/openid-configuration}" target="_blank">/.well-known/openid-configuration</a>进行查看
</p>
<em class="label label-success">OAuth2.1新增</em>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<span class="text-danger" th:errors="*"></span>
</div>
</div>
<div class="form-group">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">注册</button>
<a th:href="@{client_details}">取消</a>
</div>
</div>
</form>
</div>
</div>
<script>
var RegisterClientCtrl = ["$scope", function ($scope) {
$scope.visible = false;
$scope.showMore = function () {
$scope.visible = !$scope.visible;
};
}];
</script>
<div th:replace="~{fragments/main :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,666 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="shortcut icon" href="../../static/favicon.ico" th:href="@{/favicon.ico}"/>
<title>Test [[${clientDetailsDto.clientId}]] - Spring Security&OAuth2.1</title>
<th:block th:insert="~{fragments/main::header-css}"/>
<script th:src="@{/angular.min.js}" src="../../static/angular.min.js"></script>
</head>
<body class="container">
<div ng-app>
<a th:href="@{/}">Home</a>
<h2>Test [[${clientDetailsDto.clientId}]]</h2>
<p>
针对不同的<code>grant_type</code>提供不同的测试URL,
完整的OAuth测试请访问<a href="https://gitee.com/mkk/spring-oauth-client" target="_blank">spring-oauth-client</a>项目.
</p>
<div ng-controller="TestClientCtrl">
<div class="alert alert-danger">
请先输入client_secret: <input type="text" value="" placeholder="client_secret" ng-model="clientSecret"
size="100" required="required"/>
</div>
<hr/>
<div th:if="${clientDetailsDto.containsAuthorizationCode}" class="panel panel-default">
<div class="panel-heading">Test [authorization_code]</div>
<div class="panel-body">
<p class="text-muted">输入每一步必要的信息后点击其下面的按钮地址.</p>
<ol>
<li>
<div>
<code>从 spring-oauth-server获取 'code'</code>
<br/>
<form th:action="@{/oauth2/authorize}" th:method="get" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>redirect_uri</td>
<td>
<input type="text" name="redirect_uri" size="70"
value="{{redirectUri}}" placeholder="https://..."/>
<p class="help-block">若配置有多个<code>redirect_uri</code>可自行修改(默认使用第一个)</p>
</td>
</tr>
<tr>
<td>scope</td>
<td>
<input type="text" readonly="readonly" name="scope" size="70"
value="{{scope}}"/>
</td>
</tr>
<tr>
<td>state</td>
<td>
<input readonly="readonly" name="state" size="70" value="{{state}}"/>
<p class="help-block">每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)</p>
</td>
</tr>
<tr>
<td>response_type</td>
<td>
<input readonly="readonly" name="response_type" size="70" value="code"/>
<p class="help-block">固定值</p>
</td>
</tr>
</table>
<button class="btn btn-info" type="submit">/oauth2/authorize</button>
<span class="label label-info">GET</span>
</form>
</div>
</li>
<li>
<code>用 'code' 换取 'access_token'</code>
<br/>
输入第一步获取的'code'并点击按钮链接地址.
<br/>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input readonly="readonly" name="client_secret" size="70" required="required"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>redirect_uri</td>
<td>
<input type="text" name="redirect_uri" size="70"
value="{{redirectUri}}" placeholder="https://..."/>
<p class="help-block">若配置有多个<code>redirect_uri</code>可自行修改(默认使用第一个)</p>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input readonly="readonly" name="grant_type" size="70"
value="authorization_code"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>code</td>
<td>
<input type="text" name="code" value="" ng-model="code"
placeholder="Laulaadi78kB0DkQKv..." size="70" required="required"/>
<p class="help-block">请输入code值</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
</form>
</li>
</ol>
</div>
</div>
<div th:if="${clientDetailsDto.containsAuthorizationCodeWithPKCE}" class="panel panel-default">
<div class="panel-heading">Test [authorization_code + PKCE]</div>
<div class="panel-body">
<p class="text-muted">输入每一步必要的信息后点击其下面的链接地址.</p>
<ol>
<li>
<div>
<code>从 spring-oauth-server获取 'code'</code>
<div class="text-muted">
PKCE流程在开始前需要先通过代码生成<code>code_verifier</code><code>code_challenge</code> (如何生成详见工具类
<mark>PKCEUtils.java</mark>
);
<br/>
生成后在获取'code'时要在已有的参数基础上再增加两个参数:
<table class="table table-bordered">
<tr>
<td><code>code_challenge</code></td>
<td>对 code_verifier 使用指定算法进行计算(digest)并base encode的值</td>
</tr>
<tr>
<td><code>code_challenge_method</code></td>
<td>固定值S256</td>
</tr>
</table>
</div>
<br/>
<form th:action="@{/oauth2/authorize}" th:method="get" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>redirect_uri</td>
<td>
<input type="text" name="redirect_uri" size="70"
value="{{redirectUri}}" placeholder="https://..."/>
<p class="help-block">若配置有多个<code>redirect_uri</code>可自行修改(默认使用第一个)</p>
</td>
</tr>
<tr>
<td>scope</td>
<td>
<input type="text" readonly="readonly" name="scope" size="70"
value="{{scope}}"/>
</td>
</tr>
<tr>
<td>state</td>
<td>
<input readonly="readonly" name="state" size="70" value="{{state}}"/>
<p class="help-block">每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)</p>
</td>
</tr>
<tr>
<td>response_type</td>
<td>
<input readonly="readonly" name="response_type" size="70" value="code"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>code_challenge_method</td>
<td>
<input readonly="readonly" name="code_challenge_method" size="70" value="S256"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>code_challenge</td>
<td>
<input type="text" value="" ng-model="codeChallenge" size="70"
readonly="readonly" name="code_challenge"/>
<p class="help-block">(后台代码生成,不可修改)</p>
</td>
</tr>
</table>
<button class="btn btn-info" type="submit">/oauth2/authorize</button>
<span class="label label-info">GET</span>
</form>
</div>
</li>
<li>
<code>用 'code' 换取 'access_token'</code>
<br/>
输入第一步获取的code并点击按钮地址.
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input readonly="readonly" name="client_secret" size="70" required="required"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>redirect_uri</td>
<td>
<input type="text" name="redirect_uri" size="70"
value="{{redirectUri}}" placeholder="https://..."/>
<p class="help-block">若配置有多个<code>redirect_uri</code>可自行修改(默认使用第一个)</p>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input readonly="readonly" name="grant_type" size="70"
value="authorization_code"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>code</td>
<td>
<input type="text" name="code" value="" ng-model="code"
placeholder="Laulaadi78kB0DkQKv..." size="70" required="required"/>
<p class="help-block">请输入code值</p>
</td>
</tr>
<tr>
<td>code_verifier</td>
<td>
<input type="text" name="code_verifier" value="" ng-model="codeVerifier"
readonly="readonly" size="70" />
<p class="help-block">(后台代码生成,不可修改)</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
</form>
</li>
</ol>
</div>
</div>
<div th:if="${clientDetailsDto.containsPassword}" class="panel panel-default">
<div class="panel-heading">Test [password] <em class="label label-danger">OAuth2.1不支持</em></div>
<div class="panel-body">
<p class="text-muted">输入username, password 后点击链接地址.</p>
username: <input type="text" required="required" ng-model="username"/>
<br/>
password: <input type="text" required="required" ng-model="password"/>
<br/>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<input type="hidden" name="client_id" value="{{clientId}}"/>
<input type="hidden" name="client_secret" value="{{clientSecret}}"/>
<input type="hidden" name="username" value="{{username}}"/>
<input type="hidden" name="password" value="{{password}}"/>
<input type="hidden" name="scope" value="{{scope}}"/>
<input type="hidden" name="grant_type" value="password"/>
<button class="btn btn-link" type="submit">
/oauth2/token?client_id={{clientId}}&client_secret={{clientSecret}}&grant_type=password&scope={{scope}}&username={{username}}&password={{password}}
</button>
<span class="label label-warning">POST</span>
</form>
</div>
</div>
<div th:if="${clientDetailsDto.containsDeviceCode}" class="panel panel-default">
<div class="panel-heading">Test [device_code] <em class="label label-success">OAuth2.1新增</em></div>
<div class="panel-body">
<ol>
<li>
<p>设备上请求 <code>/oauth2/device_authorization</code>获取 <em>user_code</em>,
<em>device_code</em>,<em>verification_uri</em></p>
<form th:action="@{/oauth2/device_authorization}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input type="text" readonly="readonly" name="client_secret" size="70"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>scope</td>
<td>
<input type="text" readonly="readonly" name="scope" size="70" value="{{scope}}"/>
</td>
</tr>
</table>
<button class="btn btn-info" type="submit">/oauth2/device_authorization</button>
<span class="label label-warning">POST</span>
</form>
<p class="help-block">一般此步骤是在设备上通过代码来完成, 此处只作演示流程</p>
</li>
<li>
<p>在设备上展示<em>user_code</em>或显示一个二维码(内容为<em>verification_uri_complete</em> URL)</p>
<p>用已经登录成功的浏览器(或另一个已经认证的设备)访问<em>verification_uri_complete</em> URL(可通过扫码等方式获取内容)</p>
<p class="text-success">
此处方便演示, 请点击<a th:href="@{/oauth2/device_verification}" target="_blank">/oauth2/device_verification</a>并输入上一步获取到的<em>user_code</em>
(若未认证将跳转到登录)
</p>
<p class="help-block">提示:此步骤必须在有效时间内完成, <em>user_code</em>的有效时长在上一步中返回的数据<em>expires_in</em>来决定(单位:秒,
默认5分钟)</p>
</li>
<li>
<p>
在第2步进行的同时,
设备上后台将定时(如每隔5秒)向<code>spring-oauth-server</code>发起获取token的请求<code>/oauth2/token</code>
(需要使用第1步中获取到 <em>device_code</em> 的值),
<br/>
直到获取成功(即第2步操作完成授权设备登录)或超时(即设备轮询请求等待的时长超出第1步返回的时间<em>expires_in</em>)
</p>
<p class="help-block">请输入device_code后点击按钮地址.</p>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input type="text" readonly="readonly" name="client_secret" size="70"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input type="text" readonly="readonly" name="grant_type" size="70" value="urn:ietf:params:oauth:grant-type:device_code"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>device_code</td>
<td>
<input type="text" ng-model="deviceCode" name="device_code" required="required"
placeholder="GQ-K6n5kwLfu3XpDja-b3SlPbTfqYir..." size="70"/>
<p class="help-block">请输入device_code</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
</form>
<p class="help-block">提示在第2步进行过程中调用第3步获取token API时会响应等待授权的结果(Http状态码 400,
error='authorization_pending')</p>
</li>
</ol>
</div>
</div>
<div th:if="${clientDetailsDto.containsJwtBearer}" class="panel panel-default">
<div class="panel-heading">Test [jwt-bearer] <em class="label label-success">OAuth2.1新增</em></div>
<div class="panel-body">
<ul>
<li>
<p><code>jwt-bearer</code>是一类增强client端请求安全性的断言(assertion)实现;
通过类似'双向SSL'的机制来让server端验证client端的签名实现强安全性.</p>
</li>
<li>
<p>当注册或添加client端时需要填写一个jwk URL地址(用来获取验签的公钥), 指定认证jwt签名算法(如RS256),
设置methods为<code>client_secret_jwt</code>(对称算法,
使用client_secret为MacKey)或<code>private_key_jwt</code>(非对称算法)</p>
<p class="text-warning">注意: grant_type不能只有jwt-bearer, 无实用意义</p>
</li>
</ul>
<div>
cURL示例:
<pre>curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn"' \
--form 'client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"' \
--form 'scope="openid"' \
--form 'grant_type="client_credentials"' \
--form 'client_assertion="eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkb2ZPeDZoanhsV3c5..."' \
--form 'client_secret="dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn"'</pre>
增加两个请求参数:
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_assertion_type</td>
<td>固定值: <em>urn:ietf:params:oauth:client-assertion-type:jwt-bearer</em></td>
</tr>
<tr>
<td>client_assertion</td>
<td>
使用提供的 jwk URL中的 private_key进行签名生成的 JWT(如何生成详见: <code>JwtBearerFlowTest.java</code>)
</td>
</tr>
</table>
</div>
<p class="text-muted">输入<code>client_assertion</code>值, 点击按钮地址即可测试</p>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input type="text" readonly="readonly" name="client_secret" size="70"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>scope</td>
<td>
<input type="text" readonly="readonly" name="scope" size="70" value="{{scope}}"/>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input readonly="readonly" name="grant_type" size="70" value="client_credentials"/>
<p class="help-block">grant_type根据需要值可以是<code>authorization_code</code> <code>refresh_token</code>
</p>
</td>
</tr>
<tr>
<td>client_assertion_type</td>
<td>
<input readonly="readonly" size="70" name="client_assertion_type"
value="urn:ietf:params:oauth:client-assertion-type:jwt-bearer"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>client_assertion</td>
<td>
<input name="client_assertion" size="70" value="{{clientAssertion}}"
placeholder="eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkb2ZPeDZo..."/>
<p class="help-block">如何生成client_assertion, 详见示例类: <code>JwtBearerFlowTest.java</code>
</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
</form>
</div>
</div>
<div th:if="${clientDetailsDto.containsClientCredentials}" class="panel panel-default">
<div class="panel-heading">Test [client_credentials]</div>
<div class="panel-body">
<p class="text-muted">点击按钮地址即可测试</p>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input type="text" readonly="readonly" name="client_secret" size="70" required="required"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>scope</td>
<td>
<input type="text" readonly="readonly" name="scope" size="70" value="{{scope}}"/>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input readonly="readonly" name="grant_type" size="70" value="client_credentials"/>
<p class="help-block">固定值</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
</form>
</div>
</div>
<div th:if="${clientDetailsDto.containsRefreshToken}" class="panel panel-default">
<div class="panel-heading">Test [refresh_token]</div>
<div class="panel-body">
<p class="text-muted">输入refresh_token 后点击链接地址.</p>
<form th:action="@{/oauth2/token}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>client_secret</td>
<td>
<input type="text" readonly="readonly" name="client_secret" size="70" required="required"
value="{{clientSecret}}" placeholder="请先在页面最上面输入client_secret"/>
</td>
</tr>
<tr>
<td>grant_type</td>
<td>
<input readonly="readonly" name="grant_type" size="70" value="refresh_token"/>
<p class="help-block">固定值</p>
</td>
</tr>
<tr>
<td>refresh_token</td>
<td>
<input type="text" ng-model="refreshToken" placeholder="xYCsaPu7YV_hB6TfLb..."
size="70" name="refresh_token" required="required"/>
<p class="help-block">请输入 refresh_token 值</p>
</td>
</tr>
</table>
<button class="btn btn-primary" type="submit">/oauth2/token</button>
<span class="label label-warning">POST</span>
<br/>
<span class="text-muted">复用refresh_token: <span class="text-info"
th:text="${clientDetailsDto.tokenSettings.reuseRefreshTokens}"></span></span>
</form>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">Test OIDC-Logout <em class="label label-success">OAuth2.1新增</em></div>
<div class="panel-body">
<p class="text-muted">将'spring-oauth-server'退出并重定向会指定的uri(添加client端时的字段<code>logout_redirect_uris</code>), 由client端通过浏览器发起调用.</p>
<form th:action="@{/connect/logout}" th:method="post" target="_blank">
<table class="table table-striped table-bordered table-hover">
<tr>
<td>client_id</td>
<td>
<input type="text" readonly="readonly" name="client_id" size="70" value="{{clientId}}"/>
</td>
</tr>
<tr>
<td>id_token_hint</td>
<td>
<input name="id_token_hint" size="70" required="required"
value="" placeholder="请输入已经签发且有效的id_token值"/>
<p class="help-block">填写一个已经签发且有效的id_token</p>
</td>
</tr>
<tr>
<td>post_logout_redirect_uri</td>
<td>
<input name="post_logout_redirect_uri" size="70" value="{{post_logout_redirect_uri}}" required="required" placeholder="https://...."/>
<p class="help-block">退出后通过post重定向的uri</p>
</td>
</tr>
<tr>
<td>state</td>
<td>
<input readonly="readonly" name="state" size="70" value="{{state}}"/>
<p class="help-block">每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)</p>
</td>
</tr>
</table>
<button class="btn btn-success" type="submit">/connect/logout</button>
<span class="label label-warning">POST</span>
</form>
</div>
</div>
<div class="text-center">
<a th:href="@{/client_details}" class="btn btn-default">Back</a>
</div>
</div>
</div>
<script th:inline="javascript">
var TestClientCtrl = ["$scope", function ($scope) {
$scope.clientId = [[${clientDetailsDto.clientId}]];
$scope.clientSecret = "";
$scope.deviceCode = "";
$scope.scope = [[${clientDetailsDto.scopesWithBlank}]];
$scope.codeChallenge = [[${codeChallenge}]];
$scope.codeVerifier = [[${codeVerifier}]];
var redirectUri = [[${clientDetailsDto.redirectUris}]];
if (redirectUri === '') {
$scope.implicitRedirectUri = location.href;
$scope.redirectUri = "http://localhost:8080/unity/dashboard";
} else {
$scope.implicitRedirectUri = [[${clientDetailsDto.redirectUris}]];
$scope.redirectUri = [[${clientDetailsDto.redirectUris}]];
}
$scope.username = "mobile";
$scope.password = "mobile";
//a temp value
$scope.refreshToken = "";
$scope.clientAssertion = "";
$scope.post_logout_redirect_uri = [[${clientDetailsDto.postLogoutRedirectUris}]];
$scope.state = Math.floor(Math.random() * 1000000000).toString();
}];
</script>
<div th:replace="~{fragments/main :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>授权确认 - Spring Security&OAuth2.1</title>
<link rel="stylesheet" href="../static/bootstrap.min.css" th:href="@{/bootstrap.min.css}"/>
</head>
<body>
<div class="container">
<div class="row py-5">
<h1 class="text-center text-primary">授权确认</h1>
</div>
<div class="row">
<div class="col text-center">
<p>
The application
<span class="fw-bold text-primary" th:text="${clientId}"></span>
wants to access your account
<span class="fw-bold" th:text="${principalName}"></span>
</p>
</div>
</div>
<div th:if="${userCode}" class="row">
<div class="col text-center">
<p class="alert alert-warning">
You have provided the code
<span class="fw-bold" th:text="${userCode}"></span>.
Verify that this code matches what is shown on your device.
</p>
</div>
</div>
<div class="row pb-3">
<div class="col text-center">
<p>
The following permissions are requested by the above app.<br/>
Please review these and consent if you approve.
</p>
</div>
</div>
<div class="row">
<div class="col text-center">
<form name="consent_form" method="post" th:action="${requestURI}">
<input type="hidden" name="client_id" th:value="${clientId}">
<input type="hidden" name="state" th:value="${state}">
<input th:if="${userCode}" type="hidden" name="user_code" th:value="${userCode}">
<div th:each="scope: ${scopes}" class="form-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
name="scope"
th:value="${scope.scope}"
th:id="${scope.scope}">
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}"
th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">
You have already granted the following permissions to the above app:
</p>
<div th:each="scope: ${previouslyApprovedScopes}" class="form-check py-1">
<input class="form-check-input"
style="float: none"
type="checkbox"
th:id="${scope.scope}"
disabled
checked>
<label class="form-check-label fw-bold px-2" th:for="${scope.scope}"
th:text="${scope.scope}"></label>
<p class="text-primary" th:text="${scope.description}"></p>
</div>
<div class="pt-3">
<button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
Submit Consent
</button>
</div>
<div class="pt-3">
<a class="btn btn-link regular" type="button" id="cancel-consent" onclick="history.back()">
Cancel
</a>
</div>
</form>
</div>
</div>
<div class="row pt-4">
<div class="col text-center">
<p>
<small>
Your consent to provide access is required.<br/>
If you do not approve, click Cancel, in which case no information will be shared with the app.
</small>
</p>
</div>
</div>
</div>
<div th:replace="~{fragments/main :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="shortcut icon" href="../static/favicon.ico" th:href="@{/favicon.ico}"/>
<title>Consent Error - Spring Security&OAuth2.1</title>
<th:block th:insert="~{fragments/main::header-css}"/>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 400px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-danger">Consent Error</h1>
<p class="text-muted">Message: <span th:text="${error}"></span></p>
</div>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Device Login - Spring Security&OAuth2.1</title>
<link rel="stylesheet" href="../static/bootstrap.min.css" th:href="@{/bootstrap.min.css}"/>
</head>
<body>
<div class="container">
<div class="row py-5">
<h1 class="text-center text-primary">Device Login</h1>
</div>
<div class="row">
<div class="col text-center">
<form name="device_form" method="post" action="">
<div class="form-group">
<label for="user_code">Device User-Code</label>
<input type="text" class="form-control" id="user_code" name="user_code" placeholder="XXXX-XXXX"
required/>
<p class="help-block">Please type device user code</p>
</div>
<div class="form-group">
<button class="btn btn-primary btn-lg" type="submit" id="submit-">Submit</button>
</div>
<div class="pt-3">
<a class="btn btn-link regular" id="cancel-" th:href="@{/}">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<div th:replace="~{fragments/main :: footer}"/>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,user-scalable=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<link rel="shortcut icon" href="../../static/favicon.ico" th:href="@{/favicon.ico}"/>
<title>403 - Spring Security&OAuth2.1</title>
<th:block th:insert="~{fragments/main::header-css}"/>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 400px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-danger">403 - Access Denied</h1>
<p class="text-muted">Sorry, you do not have permission to access this resource.</p>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More