commit
18c861b785
Binary file not shown.
|
@ -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
231
README.md
|
@ -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,false;true重复使用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 — OAuth</a>, OAuth2.0官方网站
|
||||
<a href="https://oauth.net/2/">OAuth 2.0 — 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_token,refresh_token的各类配置与使用场景FAQ</a>
|
||||
<a href="https://andaily.com/blog/?p=19884">OAuth2中 access_token,refresh_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/>
|
||||
|
||||
|
||||
|
|
|
@ -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 "$@"
|
|
@ -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.
Binary file not shown.
|
@ -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]}');
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
|
|
|
@ -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">
|
||||
© 2013 - 2023 <a href="https://gitee.com/shengzhao/spring-oauth-server" target="_blank">spring-oauth-server</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
- URL:http://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/
|
||||
|
|
@ -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
396
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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[]
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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的各类设置
|
||||
* 如 token有效期,refresh_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;
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.util.List;
|
|||
|
||||
/**
|
||||
* @author Shengzhao Li
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface OauthRepository extends Repository {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 支持PKCE为true
|
||||
* 默认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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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的各类设置
|
||||
* 如 token有效期,refresh_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;
|
||||
}
|
||||
}
|
|
@ -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 有效时长,单位:秒
|
||||
* 默认 43200(12小时)
|
||||
*/
|
||||
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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
// }
|
||||
}
|
|
@ -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<>();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -40,4 +40,16 @@ public class SOSController {
|
|||
}
|
||||
|
||||
|
||||
// /**
|
||||
// * 403 无权限访问
|
||||
// *
|
||||
// * @return view
|
||||
// * @since 3.0.0
|
||||
// */
|
||||
// @GetMapping("/access_denied")
|
||||
// public String accessDenied() {
|
||||
// return "access_denied";
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/**");
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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">-->
|
||||
|
||||
<!--<!–unity client–>-->
|
||||
<!--<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"/>-->
|
||||
|
||||
<!--<!–mobile client–>-->
|
||||
<!--<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>
|
|
@ -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
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -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>
|
||||
client_secret: <span class="text-primary">***</span>
|
||||
<br/>
|
||||
grant_types: <span class="text-primary" th:text="${cli.authorizationGrantTypes}"></span>
|
||||
authentication_methods: <span class="text-primary" th:text="${cli.clientAuthenticationMethods}"></span>
|
||||
<br/>
|
||||
scopes: <span class="text-primary" th:text="${cli.scopes}"></span>
|
||||
redirect_uri: <span class="text-primary" th:text="${cli.redirectUris}"></span>
|
||||
<br/>
|
||||
client_id_issued: <span class="text-primary" th:text="${cli.clientIdIssuedAt}"></span>
|
||||
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>
|
||||
archived: <strong th:class="${cli.archived?'text-warning':'text-primary'}"
|
||||
th:text="${cli.archived}"></strong>
|
||||
</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>
|
|
@ -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>的有效时长(单位:秒);默认300(5分钟)</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>的有效时长(单位:秒);默认300(5分钟)</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue