如何刷新access_token(refresh_token), 在通过客户端(如移动设备)登录成功后返回的数据如下
-
{"access_token":"3420d0e0-ed77-45e1-8370-2b55af0a62e8","token_type":"bearer","refresh_token":"b36f4978-a172-4aa8-af89-60f58abe3ba1","expires_in":43199,"scope":"read write"}
-
+
{"access_token":"eyJraWQiOiJteW9pZGMta2V5aWQiLCJhbGciOiJSUzI1...","token_type":"bearer","refresh_token":"UCFNxUj4ytr241KzwJJgnMno1RfmoLs0GKVxNWPjW5VZ7d4U4YsDM7...","expires_in":43199,"scope":"openid"}
若需要刷新获取新的token(一般在 expires_in 有效期时间快到时), 请求的URL类似如下
-
http://localhost:8080/oauth/token?client_id=mobile-client&client_secret=mobile&grant_type=refresh_token&refresh_token=b36f4978-a172-4aa8-af89-60f58abe3ba1
-
+
http://localhost:8080/oauth2/token?client_id=mobile-client&client_secret=mobile&grant_type=refresh_token&refresh_token=UCFNxUj4ytr241KzwJJgnMno1RfmoLs0GKVxNWPjW5VZ7
注意: refresh_token 参数值必须与登录成功后获取的 refresh_token 一致, 且grant_type = refresh_token
@@ -200,14 +187,28 @@ config-redis
-
- Version: 2.1.1 [pending]
+ Version: 3.0.0 [finished]
+
+ Date: 2023-10-10 / 2023-10-31
+
+
+ 底层安全架构升级:jdk升级17, spring6.x, springboot3.x, thymeleaf替换servlet/jsp
+ 全面升级支持 OAuth2.1协议与 OIDC1.0协议
+ 构建包由war换成jar, SQL相应调整
+ 用spring-security-oauth2-authorization-server升级替换spring-security-oauth2, 详见背景说明
+ 界面使用说明按OAuth2.1进行友好设计并更新各提示语句
+ 增加spring-restdocs文档支持, 自动生成API相关文档
+
+
+ -
+
+ Version: 2.1.1 [canceled]
Date: 2022-05-05 / ---
尝试升级替换spring-security-oauth2, 详见背景说明
-
-
@@ -343,7 +344,7 @@ config-redis
#73 - Upgrade 'spring-security-oauth2' version to '2.0.6.RELEASE' (current: 1.0.5.RELEASE) [CANCELED]
#74 - oauth mysql ddl add create_time, default is now()
- #75 - Add user information API, for spring-oauth-client project use
+ #75 - Add user information API, for spring-oauth-client project use
URL: /unity/user_info
Login: Yes (ROLE_UNITY)
@@ -352,7 +353,7 @@ config-redis
Login: Yes (ROLE_MOBILE)
Data Format: JSON
-
+
#77 - User add Privilege domain.
Addition initial two user: unityuser(ROLE_UNITY),mobileuser("ROLE_MOBILE).
@@ -371,9 +372,10 @@ config-redis
数据库表字段说明
- 在0.3版本中添加了db_table_description.html文件(位于/others目录), 用来说明数据库脚本文件oauth.ddl中各表,各字段的用途及使用场合.
+ 在0.3版本中添加了db_table_description.html文件(位于/others目录, 3.0.0版本db_table_description_3.0.0.html),
+用来说明数据库脚本文件oauth.ddl中各表,各字段的用途及使用场合.
- 也可在线访问http://andaily.com/spring-oauth-server/db_table_description.html.
+ 也可在线访问db_table_description_3.0.0.html.
@@ -402,6 +404,8 @@ config-redis
2019-08-04 发布 2.0.1 版本
2020-06-04 发布 2.0.2 版本
2022-05-01 发布 2.1.0 版本
+ 2023-10-10 开始全新大版本 3.0.0 开发
+ 2023-10-31 发布 3.0.0 全新版本
@@ -412,76 +416,84 @@ config-redis
-
- RFC 6749 - The OAuth 2.0 Authorization Framework, OAuth2.0协议(英文)
+ RFC 6749 - The OAuth 2.0 Authorization Framework, OAuth2.0协议(英文)
-
- OAuth 2.0 — OAuth, OAuth2.0官方网站
+ OAuth 2.0 — OAuth, OAuth2.0官方网站
-
- OAuth2核心参数说明, 重点介绍了grant_type 与 response_type 以及示例
+ OAuth2核心参数说明, 重点介绍了grant_type 与 response_type 以及示例
-
- OAuth2 flows, 详细介绍OAuth2的流程,各类错误发生时的响应
+ OAuth2 flows, 详细介绍OAuth2的流程,各类错误发生时的响应
-
- OAuth 2 开发人员指南(Spring security oauth2), 翻译OAuth 2 Developers Guide(spring security oauth2)
+ OAuth 2 开发人员指南(Spring security oauth2), 翻译OAuth 2 Developers Guide(spring security oauth2)
-
- 理解OAuth 2.0, 介绍OAuth2各类grant_type的使用
+ 理解OAuth 2.0, 介绍OAuth2各类grant_type的使用
-
- OAuth2:隐式授权(Implicit Grant)类型的开放授权, 介绍grant_type='implicit'模式
+ OAuth2:隐式授权(Implicit Grant)类型的开放授权, 介绍grant_type='implicit'模式
-
- Apache Oltu, Java版的 OAuth2参考实现, 建议去了解了解
+ Apache Oltu, Java版的 OAuth2参考实现, 建议去了解了解
-
- OIDC–基于OAuth2的下一代身份认证授权协议
+ OIDC–基于OAuth2的下一代身份认证授权协议
-
- 在spring-oauth-server中将AccessToken存入Redis的配置
+ 在spring-oauth-server中将AccessToken存入Redis的配置
-
- 如何通过代码生成AccessToken
+ 如何通过代码生成AccessToken
-
- OAuth2中 access_token,refresh_token的各类配置与使用场景FAQ
+ OAuth2中 access_token,refresh_token的各类配置与使用场景FAQ
+
+ -
+
OAuth2.1 介绍
+
+ -
+
OAuth 2.1的状态与主要特征, 个人总结
+
+ -
+
OIDC官方网站
-
-
-
+
+
+ 与项目相关的技术文章请访问 http://andaily.com/blog/?cat=19 (不断更新与OAuth2相关的文章)
+
@@ -502,23 +514,22 @@ config-redis
捐助
-
+
支付宝: monkeyking1987@126.com (**钊)
-
- 快意江湖 -- 100元
-
- yufan -- 100元
+
+- 快意江湖 -- 100元
+- yufan -- 100元
其他...
- 关注更多开源项目请访问 http://andaily.com/my_projects.html
+ 关注更多开源项目请访问 https://andaily.com/my_projects.html
欢迎联系作者 sz@monkeyk.com 进行探讨
-
+
diff --git a/mvnw b/mvnw
deleted file mode 100644
index 5bf251c0774593ca4f5335acf0f7483eaa162e8f..0000000000000000000000000000000000000000
--- a/mvnw
+++ /dev/null
@@ -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 "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
deleted file mode 100644
index 019bd74d766ebd4c033528112148d866555b5c9e..0000000000000000000000000000000000000000
--- a/mvnw.cmd
+++ /dev/null
@@ -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%
diff --git a/others/boot/spring-boot-reference(2.0.0).pdf b/others/boot/spring-boot-reference(2.0.0).pdf
deleted file mode 100644
index a547cdb6a6d9c89c78d1a0da8de9a6500657c6fb..0000000000000000000000000000000000000000
Binary files a/others/boot/spring-boot-reference(2.0.0).pdf and /dev/null differ
diff --git a/others/boot/spring-oauth-server.zip b/others/boot/spring-oauth-server.zip
deleted file mode 100644
index 9823610ebcec44062bd1ba8c1a729e0637e0c79e..0000000000000000000000000000000000000000
Binary files a/others/boot/spring-oauth-server.zip and /dev/null differ
diff --git a/others/database/initial_data.ddl b/others/database/initial_data.ddl
index 134aff6d50fa189d6974ac32556b0bccd1d11bc9..ba182ce3493e860adf07ea8e52624d908010781c 100644
--- a/others/database/initial_data.ddl
+++ b/others/database/initial_data.ddl
@@ -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]}');
+
+
diff --git a/others/database/initial_db.ddl b/others/database/initial_db.ddl
index a488364fcbbb4560d3f710e59665dd7380502988..bea1df7822eeac5ce68aba7e8397bce8f7d3af6b 100644
--- a/others/database/initial_db.ddl
+++ b/others/database/initial_db.ddl
@@ -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;
diff --git a/others/database/oauth.ddl b/others/database/oauth.ddl
index 397a0e2e592d588a9e37854e5c01f1f2101797dd..9dfd0ba3bee1943f98196386cedd82be560953a7 100644
--- a/others/database/oauth.ddl
+++ b/others/database/oauth.ddl
@@ -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;
+-- 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;
-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;
-
-
-
--- 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);
diff --git a/others/db_table_description_3.0.0.html b/others/db_table_description_3.0.0.html
new file mode 100644
index 0000000000000000000000000000000000000000..06c120e913ad4ddd88e4ce3c763556d8e25d709d
--- /dev/null
+++ b/others/db_table_description_3.0.0.html
@@ -0,0 +1,506 @@
+
+
+
+
+
+
+ 数据库表说明 - spring-oauth-server
+
+
+
+
+
+
+
以下对spring-oauth-server项目中的
+ oauth.ddl initial_db.ddl文件(位于/others/database目录)中的表字及段进行说明,
+ 内容包括字段说明与使用场景等
+
+
+
+ | 表名 |
+ 字段名 |
+ 字段类型 |
+ 字段说明 |
+
+
+
+
+ | oauth2_registered_client |
+ id |
+ varchar |
+ 主键,系统自动生成 |
+
+
+ | archived |
+ tinyint |
+
+ 用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档).
+
+ 对该字段的具体使用请参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上archived
+ = 0条件 (扩展字段)
+ |
+
+
+ | create_time |
+ datetime |
+ 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
+
+
+ | updated_time |
+ timestamp |
+ 数据的最后更新时间,由数据库自行更新维护 |
+
+
+ | client_id |
+ varchar |
+
+ 唯一,不能为空.
+
+ 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成).
+
+ 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.
+ |
+
+
+ | client_id_issued_at |
+ timestamp |
+ client_id的签发时间, 默认为数据创建时间 |
+
+
+ | client_secret |
+ varchar |
+
+ 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成),加密保存.
+
+ 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.
+ |
+
+
+ | client_secret_expires_at |
+ datetime |
+ client_secret的过期时间,null表示永不过期 |
+
+
+ | client_name |
+ varchar |
+ 客户端(client)的名称,一般是一个有业务意义的名称 |
+
+
+ | client_authentication_methods |
+ varchar |
+ 认证支持的方式,多个由逗号分隔; 如: client_secret_basic,client_secret_post; 一般指认证时传递client_secret支持哪些方式 |
+
+
+ | authorization_grant_types |
+ varchar |
+
+ 指定客户端支持的grant_type,可选值包括authorization_code,urn:ietf:params:oauth:grant-type:device_code,refresh_token,
+ urn:ietf:params:oauth:grant-type:jwt-bearer,client_credentials,
+ 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,refresh_token".
+
+ 在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有:
+ "authorization_code,refresh_token"(针对通过浏览器访问的客户端);
+ "client_credentials"(针对另一个服务端的场景,不需要用户参与).
+
+ urn:ietf:params:oauth:grant-type:device_code与urn:ietf:params:oauth:grant-type:jwt-bearer是OAuth2.1中新增.
+ |
+
+
+ | redirect_uris |
+ varchar |
+
+ OAuth2 认证后回调uri, 一般传递code, 多个由逗号分隔;
+ 可为空, 当grant_type为authorization_code时,
+ 在OAuth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:
+
+ -
+ 当grant_type=
authorization_code时, 第一步 从 spring-oauth-server获取
+ 'code'时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与
+ web_server_redirect_uri的值一致. 第二步 用 'code' 换取 'access_token'
+ 时客户也必须传递相同的redirect_uri.
+
+ 在实际应用中, redirect_uris在注册时是必须填写的, 一般用来处理服务器返回的code,
+ 验证state是否合法与通过code去换取access_token值.
+
+ 在spring-oauth-client项目中,
+ 可具体参考AuthorizationCodeController.java中的authorizationCodeCallback方法.
+
+
+ |
+
+
+ | post_logout_redirect_uris |
+ varchar |
+ OAuth2 退出时 post 的客户端重定向 uri; 可选 多个由逗号分隔, 一般在client注册时可填写 |
+
+
+ | scopes |
+ varchar |
+
+ 指定客户端申请的权限范围,可选值在OIDC协议中定义,
+ 包括openid,profile,email,address,phone;若有多个值用逗号(,)分隔,如:
+ "openid,email".
+
+ openid是必须有的,其他值若有则在获取的id_token中会包含对应的值.
+
+ 在实际应该中, 该值一般由服务端指定, 常用的值为openid.
+ |
+
+
+ | client_settings |
+ varchar |
+
+ 客户端的各类设置, 如是否支持PKCE,用户授权(consent)确认是否必须等; 详见代码ClientSettings.java;
+ 此字段存储JSON格式的数据值.
+ |
+
+
+ | token_settings |
+ varchar |
+
+ 对token的各类设置; 如 token有效期, refresh_token有效期等; 详见代码TokenSettings.java;
+ 此字段存储JSON格式的数据值.
+ |
+
+
+ |
+
+
+ 在项目中,主要操作oauth2_registered_client表的类是ClientDetailsController.java,
+ OauthClientDetails.java更多的细节请参考该类; 也可以根据实际的需要,去扩展或修改该类的实现.
+
+ |
+
+
+
+ | oauth2_authorization |
+ id |
+ varchar |
+ 主键 |
+
+
+ | registered_client_id |
+ varchar |
+
+ 外键, 关联oauth2_registered_client的id字段
+ |
+
+
+ | principal_name |
+ varchar |
+ 认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段 |
+
+
+ | authorization_grant_type |
+ varchar |
+ OAuth2的 grant_type 类型 |
+
+
+ | authorized_scopes |
+ varchar |
+ 此次授权的范围(scope) |
+
+
+ | attributes |
+ blob |
+ 进行认证授权的各类信息,JSON格式 |
+
+
+ | state |
+ varchar |
+ 认证请求中传递的 state 值 |
+
+
+ | authorization_code_value |
+ blob |
+ authorization_code流程中的code值 |
+
+
+ | authorization_code_issued_at |
+ datetime |
+ authorization_code流程中的code签发时间 |
+
+
+ | authorization_code_expires_at |
+ datetime |
+ authorization_code流程中的code过期时间 |
+
+
+ | authorization_code_metadata |
+ blob |
+ authorization_code流程中的code的属性设置, 如值是否有效 |
+
+
+ | access_token_value |
+ blob |
+ access_token 值 |
+
+
+ | access_token_issued_at |
+ datetime |
+ access_token 签发时间 |
+
+
+ | access_token_expires_at |
+ datetime |
+ access_token 过期时间 |
+
+
+ | access_token_metadata |
+ blob |
+ access_token 属性设置, 如各类claims中的属性与值 |
+
+
+ | access_token_type |
+ varchar |
+ access_token 类型, 一般是Bearer |
+
+
+ | access_token_scopes |
+ varchar |
+ 此次授权的scope范围值,如: openid,profile |
+
+
+ | oidc_id_token_value |
+ blob |
+ OIDC中id_token 值 |
+
+
+ | oidc_id_token_issued_at |
+ datetime |
+ id_token 签发时间 |
+
+
+ | oidc_id_token_expires_at |
+ datetime |
+ id_token 过期时间 |
+
+
+ | oidc_id_token_metadata |
+ blob |
+ id_token 属性设置, 如各类claims中的属性与值 |
+
+
+ | refresh_token_value |
+ blob |
+ refresh_token 值 |
+
+
+ | refresh_token_issued_at |
+ datetime |
+ refresh_token 签发时间 |
+
+
+ | refresh_token_expires_at |
+ datetime |
+ refresh_token 过期时间 |
+
+
+ | refresh_token_metadata |
+ blob |
+ refresh_token 属性设置, 如是否复用(reuse) |
+
+
+ | user_code_value |
+ blob |
+ device_code流程中的user_code值 |
+
+
+ | user_code_issued_at |
+ datetime |
+ user_code 签发时间 |
+
+
+ | user_code_expires_at |
+ datetime |
+ user_code 过期时间 |
+
+
+ | user_code_metadata |
+ blob |
+ user_code 属性设置, 如是否已经验证 |
+
+
+ | device_code_value |
+ blob |
+ device_code流程中的device_code值 |
+
+
+ | device_code_issued_at |
+ datetime |
+ device_code 签发时间 |
+
+
+ | device_code_expires_at |
+ datetime |
+ device_code 过期时间 |
+
+
+ | device_code_metadata |
+ blob |
+ device_code 属性设置, 如是否已经验证 |
+
+
+ | updated_time |
+ timestamp |
+ 数据的最后修改时间, 由数据库自动维护更新 |
+
+
+ |
+
+ 该表用于存储在OAuth2.1授权过程中各类信息数据,
+ 支持各类grant_type场景;
+ 对oauth2_authorization表的主要操作在JdbcOAuth2AuthorizationService.java类中,
+ 更多的细节请参考该类.
+
+ 注意: 若对性能有要求, 此表的数据存储设计需要进行优化(如存redis或利用JWT特性简化一些不必要的存储字段).
+
+ |
+
+
+
+ | oauth2_authorization_consent |
+ registered_client_id |
+ varchar |
+ 外键, 关联oauth2_registered_client表的id字段 |
+
+
+ | principal_name |
+ varchar |
+ 认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段 |
+
+
+ | authorities |
+ varchar |
+ 授权确认过期中的属性, 如scope范围 |
+
+
+ | updated_time |
+ timestamp |
+ 数据的最后修改时间, 由数据库自动维护更新 |
+
+
+ |
+
+ 该表主要存储在授权过程中需要用户进行确认(consent)的信息;
+ 在项目中,主要操作oauth2_authorization_consent表的对象是JdbcOAuth2AuthorizationConsentService.java.
+ 更多的细节请参考该类.
+
+ |
+
+
+
+ | user_ |
+ id |
+ int |
+ 主键, 自增长, 数据库自动生成 |
+
+
+ | guid |
+ varchar |
+ 唯一, 业务id |
+
+
+ | create_time |
+ datetime |
+ 数据创建时间 |
+
+
+ | updated_time |
+ timestamp |
+ 数据的最后修改时间, 由数据库自动维护更新 |
+
+
+ | username |
+ varchar |
+ 用户名, 非空, 唯一 |
+
+
+ | password |
+ varchar |
+ 密码, 加密存储, 非空 |
+
+
+ | enabled |
+ tinyint |
+ 是否启用, 默认1(即启用) |
+
+
+ | phone |
+ varchar |
+ 手机号 |
+
+
+ | email |
+ varchar |
+ 邮箱地址 |
+
+
+ | address |
+ varchar |
+ 个人地址 |
+
+
+ | nickname |
+ varchar |
+ 用户昵称, 别名 |
+
+
+ | updated_at |
+ int |
+ 最后数据更新时间值 |
+
+
+ | default_user |
+ tinyint |
+ 是否默认用户, 默认0(不是); 只用在初始化数据时使用 |
+
+
+ | last_login_time |
+ datetime |
+ 最后登录时间 |
+
+
+ |
+
+ 在项目中,主要使用user_表的对象是UserServiceImpl.java;
+ 对应的实体是User.java;
+ 在Spring Security中, 此表存储的数据对应UserDetails.java类.
+
+ |
+
+
+
+ | user_privilege |
+ user_id |
+ int |
+ 外键, 关联user_的id字段 |
+
+
+ | privilege |
+ varchar |
+ 权限值, 如: ROLE_USER |
+
+
+ |
+
+ 此表存储用户的权限值, 一个用户可以有多个权限值.
+
+ |
+
+
+
+
+
+
+
+ © 2013 - 2023 spring-oauth-server
+
+
+
+
+
+
\ No newline at end of file
diff --git a/others/how_to_use.txt b/others/how_to_use.txt
index 242a45de412870205f9aeb9834b4b84f0cf23e28..d533abc91cbe1719697b87ea84a050c2ceef90ab 100644
--- a/others/how_to_use.txt
+++ b/others/how_to_use.txt
@@ -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
diff --git a/others/oauth2.1-flow.md b/others/oauth2.1-flow.md
new file mode 100644
index 0000000000000000000000000000000000000000..1b261e17debb1d7d7f24445c99bdde27b4281369
--- /dev/null
+++ b/others/oauth2.1-flow.md
@@ -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/
+
diff --git a/others/oauth_test.txt b/others/oauth_test.txt
index dc40028f8aa18809a734afb8db452d4ca3ac0572..366e3aa09f476fe0b12f9940da0e87cb18e00a7c 100644
--- a/others/oauth_test.txt
+++ b/others/oauth_test.txt
@@ -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
diff --git a/pom.xml b/pom.xml
index 9189364e5631380c259e44435aae5d7baff822ed..6516b62fdf7c482d6765b96590e8c71cb3022547 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,205 +1,191 @@
-
-
- 4.0.0
-
- com.monkeyk
- spring-oauth-server
- 2.1.0
- war
-
- ${project.artifactId}
- Spring OAuth Server (Spring Boot)
-
-
- org.springframework.boot
- spring-boot-starter-parent
- 2.1.4.RELEASE
-
-
-
-
- UTF-8
- UTF-8
- 1.8
-
- 2.3.8.RELEASE
- 1.1.1.RELEASE
- false
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-tomcat
- provided
-
-
- org.apache.tomcat.embed
- tomcat-embed-jasper
- provided
-
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
-
- org.springframework.boot
- spring-boot-starter-validation
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- org.springframework.boot
- spring-boot-starter-jdbc
-
-
-
-
- org.springframework.security.oauth
- spring-security-oauth2
- ${spring.security.oauth.version}
-
-
-
-
- org.springframework.security
- spring-security-jwt
- ${spring.security.jwt.version}
-
-
-
-
-
-
-
-
-
-
- org.springframework.security
- spring-security-taglibs
- 4.2.3.RELEASE
-
-
- org.springframework.security
- spring-security-acl
-
-
- org.springframework
- spring-beans
-
-
- org.springframework
- spring-core
-
-
- org.springframework
- spring-expression
-
-
-
-
-
- mysql
- mysql-connector-java
- runtime
-
-
-
-
- commons-lang
- commons-lang
- 2.6
-
-
-
- com.zaxxer
- HikariCP
-
-
-
- org.sitemesh
- sitemesh
- 3.0.1
-
-
-
- javax.servlet
- jstl
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
- org.springframework.security
- spring-security-test
- test
-
-
-
-
-
- ${project.artifactId}
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
- maven-war-plugin
-
- false
- */classes/application.properties
-
-
- false
-
- ${project.version}
- spring-oauth-server(boot)
- ${project.version}
- http://monkeyk.com
- Not Vendor Yet, Inc.
-
-
-
- true
-
-
-
-
-
-
- maven-surefire-plugin
-
- ${test.skip}
- none
-
- **/*Test.java
-
-
-
-
-
-
-
-
- shengzhao
- shengzhao@shengzhaoli.com
-
-
-
-
-
+
+
+ 4.0.0
+
+ com.monkeyk
+ spring-oauth-server
+ 3.0.0
+ jar
+
+ ${project.artifactId}
+ Spring OAuth Server (Spring Boot)
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.2
+
+
+
+
+ UTF-8
+ UTF-8
+ 17
+
+ false
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-authorization-server
+
+
+ org.apache.logging.log4j
+ log4j-to-slf4j
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.thymeleaf.extras
+ thymeleaf-extras-springsecurity6
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+
+
+
+
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+ com.mysql
+ mysql-connector-j
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.apache.logging.log4j
+ log4j-to-slf4j
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+ org.bitbucket.b_c
+ jose4j
+ 0.9.3
+ test
+
+
+
+
+
+
+ ${project.artifactId}
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ 2.2.1
+
+
+ generate-docs
+ prepare-package
+
+ process-asciidoc
+
+
+ html
+ book
+
+ ${project.version}
+ ${project.artifactId}
+
+
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-asciidoctor
+ ${spring-restdocs.version}
+
+
+
+
+
+ maven-jar-plugin
+
+
+ false
+
+ ${project.version}
+ spring-oauth-server(boot)
+ ${project.version}
+ https://monkeyk.com
+ CloudJac, Inc.
+
+
+ true
+
+
+
+
+
+
+ maven-surefire-plugin
+
+ ${test.skip}
+
+ **/*Test.java
+
+
+
+
+
+
+
+
+ shengzhao
+ shengzhao@shengzhaoli.com
+
+
+
+
+
diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc
new file mode 100644
index 0000000000000000000000000000000000000000..79088f5fa9bfdb5d571d238e61c86c132b942d56
--- /dev/null
+++ b/src/docs/asciidoc/index.adoc
@@ -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[]
+
diff --git a/src/main/java/com/monkeyk/sos/SpringOauthServerApplication.java b/src/main/java/com/monkeyk/sos/SpringOauthServerApplication.java
index a954c9c8a24f18a47dfc961d06977942cf2a861d..78bc4251967b33c278dd39350fe34d06f5f307c2 100644
--- a/src/main/java/com/monkeyk/sos/SpringOauthServerApplication.java
+++ b/src/main/java/com/monkeyk/sos/SpringOauthServerApplication.java
@@ -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
diff --git a/src/main/java/com/monkeyk/sos/SpringOauthServerServletInitializer.java b/src/main/java/com/monkeyk/sos/SpringOauthServerServletInitializer.java
deleted file mode 100644
index be871eb3bab521037c30e10e6cee0b25490483fb..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/SpringOauthServerServletInitializer.java
+++ /dev/null
@@ -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);
- }
-
-}
diff --git a/src/main/java/com/monkeyk/sos/config/JWTTokenStoreConfiguration.java b/src/main/java/com/monkeyk/sos/config/JWTTokenStoreConfiguration.java
deleted file mode 100644
index 25cec0699cdd8cf7474e616ddd1e5f00fe62dfed..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/config/JWTTokenStoreConfiguration.java
+++ /dev/null
@@ -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
- *
- *
- * JWT TokenStore config
- *
- * 使用时配置参数
- *
sos.token.store=jwt
- *
- * @author Shengzhao Li
- * @since 2.1.0
- */
-@Configuration
-@ConditionalOnProperty(name = "sos.token.store", havingValue = "jwt")
-public class JWTTokenStoreConfiguration {
-
-
- /**
- * 不同的系统用不同的jwtKey;不推荐共用一样的
- *
- * 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;
- }
-
-}
diff --git a/src/main/java/com/monkeyk/sos/config/JdbcTokenStoreConfiguration.java b/src/main/java/com/monkeyk/sos/config/JdbcTokenStoreConfiguration.java
deleted file mode 100644
index b3e430d0b75dd12fa675929c0e6db6c0ce6093a4..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/config/JdbcTokenStoreConfiguration.java
+++ /dev/null
@@ -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
- *
- *
- * JDBC TokenStore config
- * 使用时配置参数
- *
sos.token.store=jdbc
(默认)
- *
- * @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;
- }
-
-}
diff --git a/src/main/java/com/monkeyk/sos/config/MVCConfiguration.java b/src/main/java/com/monkeyk/sos/config/MVCConfiguration.java
index f6ba5e72c792885bf9a6a68d6cfb373fea34ef55..6c53f8de96b70d87000a0b3844c40813d5bc15b9 100644
--- a/src/main/java/com/monkeyk/sos/config/MVCConfiguration.java
+++ b/src/main/java/com/monkeyk/sos/config/MVCConfiguration.java
@@ -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;
*
*
* @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> 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 encodingFilter() {
FilterRegistrationBean 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 registrationBean = new FilterRegistrationBean<>();
- registrationBean.setFilter(new SOSSiteMeshFilter());
- registrationBean.addUrlPatterns("/*");
- //注意: 在 spring security filter之后
- registrationBean.setOrder(8899);
- return registrationBean;
- }
-
-
}
diff --git a/src/main/java/com/monkeyk/sos/config/OAuth2MethodSecurityConfiguration.java b/src/main/java/com/monkeyk/sos/config/OAuth2MethodSecurityConfiguration.java
deleted file mode 100644
index fa6c8c2af27b3f63a3e1992cda726ef791f839ac..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/config/OAuth2MethodSecurityConfiguration.java
+++ /dev/null
@@ -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();
- }
-
-}
diff --git a/src/main/java/com/monkeyk/sos/config/OAuth2ServerConfiguration.java b/src/main/java/com/monkeyk/sos/config/OAuth2ServerConfiguration.java
index 76e10c2d102d30bdcbf98a46715deb0d66320397..df1708b185ce193f2ca0391ee5979d6d6e0f3b1a 100644
--- a/src/main/java/com/monkeyk/sos/config/OAuth2ServerConfiguration.java
+++ b/src/main/java/com/monkeyk/sos/config/OAuth2ServerConfiguration.java
@@ -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 javax.sql.DataSource;
+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 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 {
-
- @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()
- // 所有以 /unity/ 开头的 URL属于此资源
- .requestMatchers().antMatchers("/unity/**")
- .and()
- .authorizeRequests()
- .antMatchers("/unity/**").access("#oauth2.hasScope('read') and hasRole('UNITY')");
-
- }
-
- }
+ public static String KEYSTORE_NAME = "jwks.json";
/**
- * // mobile resource
- * MOBILE 资源的访问权限配置
+ * @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')");
-
- }
-
- }
-
- @Configuration
- @EnableAuthorizationServer
- protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
-
-
- @Autowired
- private TokenStore tokenStore;
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
- @Autowired
- private DefaultTokenServices tokenServices;
-
- @Autowired
- private ClientDetailsService clientDetailsService;
-
-
- @Autowired
- private OauthService oauthService;
-
-
- @Autowired
- private AuthorizationCodeServices authorizationCodeServices;
-
-
- @Autowired
- private UserService userDetailsService;
-
-
- @Autowired
- @Qualifier("authenticationManagerBean")
- private AuthenticationManager authenticationManager;
+ /**
+ * @since 3.0.0
+ */
+ private AuthenticationManager authenticationManagerOAuth2;
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
+ /**
+ * authorizationServerSecurityFilterChain
+ *
+ * @since 3.0.0
+ */
+ @Bean
+ @Order(1)
+ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
- clients.withClientDetails(clientDetailsService);
- }
+ http.sessionManagement(sessionManagementConfigurer -> {
+ sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
+ });
+ http.authorizeHttpRequests(registry -> {
+ registry
+ // 所有以 /unity/ 开头的 URL属于此资源
+ .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;
+ }
-// /*
-// * 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;
-// }
+ /**
+ * 获取 OAuth2流程中的 AuthenticationManager
+ *
+ * @return AuthenticationManager
+ * @since 3.0.0
+ */
+ public AuthenticationManager authenticationManagerOAuth2() {
+ return authenticationManagerOAuth2;
+ }
+ /**
+ * 扩展 oidc 的默认能力配置项
+ *
+ * @since 3.0.0
+ */
+ private Consumer 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);
+ });
+ };
+ }
- @Bean
- public ClientDetailsService clientDetailsService(DataSource dataSource) {
- return new CustomJdbcClientDetailsService(dataSource);
- }
+ /**
+ * 注册客户端管理
+ *
+ * @return RegisteredClientRepository
+ * @since 3.0.0
+ */
+ @Bean
+ public RegisteredClientRepository registeredClientRepository() {
+// return new InMemoryRegisteredClientRepository(client);
+ return new JdbcRegisteredClientRepository(this.jdbcTemplate);
+ }
- @Bean
- public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
- return new JdbcAuthorizationCodeServices(dataSource);
- }
+ /**
+ * 授权准许存储配置, jdbc实现
+ *
+ * @since 3.0.0
+ */
+ @Bean
+ public OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(RegisteredClientRepository registeredClientRepository) {
+ return new JdbcOAuth2AuthorizationConsentService(this.jdbcTemplate, registeredClientRepository);
+ }
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.tokenServices(tokenServices)
- .tokenStore(tokenStore)
- .authorizationCodeServices(authorizationCodeServices)
- .userDetailsService(userDetailsService)
- .userApprovalHandler(userApprovalHandler())
- .authenticationManager(authenticationManager);
- }
+ /**
+ * 授权信息存储配置, jdbc实现
+ *
+ * @since 3.0.0
+ */
+ @Bean
+ public OAuth2AuthorizationService oAuth2AuthorizationService(RegisteredClientRepository registeredClientRepository) {
+ return new JdbcOAuth2AuthorizationService(this.jdbcTemplate, registeredClientRepository);
+ }
- @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);
- }
+ /**
+ * 提供加密/解密的 source
+ * 可多个 key, 根据不同的需要来选择使用
+ *
+ * @return JWKSource
+ * @since 3.0.0
+ */
+ @Bean
+ public JWKSource jwkSource() throws IOException {
+ Resource resource = new ClassPathResource(KEYSTORE_NAME);
+ return JWKSourceBuilder.create(resource.getURL()).build();
+ }
- @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 jwtCustomizer() {
+ return new ClaimsOAuth2TokenCustomizer();
}
diff --git a/src/main/java/com/monkeyk/sos/config/WebSecurityConfigurer.java b/src/main/java/com/monkeyk/sos/config/WebSecurityConfigurer.java
index 6a0bb47c24236480c78d051a998ffe064d494876..514b76b52247441601bb35b5cb49b49a45288eac 100644
--- a/src/main/java/com/monkeyk/sos/config/WebSecurityConfigurer.java
+++ b/src/main/java/com/monkeyk/sos/config/WebSecurityConfigurer.java
@@ -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安全配置项
+ *
+ * defaultSecurityFilterChain
+ *
+ * @throws Exception e
+ * @since 3.0.0
+ */
@Bean
- public AuthenticationManager authenticationManagerBean() throws Exception {
- return super.authenticationManagerBean();
- }
-
- @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());
+ @Order(2)
+ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
+
+ 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);
}
diff --git a/src/main/java/com/monkeyk/sos/domain/AbstractDomain.java b/src/main/java/com/monkeyk/sos/domain/AbstractDomain.java
index 37299640fe562c8b54361389dfaeff5efeaf3e48..7912716a28973589eff3c24b399ea066f985477a 100644
--- a/src/main/java/com/monkeyk/sos/domain/AbstractDomain.java
+++ b/src/main/java/com/monkeyk/sos/domain/AbstractDomain.java
@@ -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;
}
diff --git a/src/main/java/com/monkeyk/sos/domain/oauth/ClaimsOAuth2TokenCustomizer.java b/src/main/java/com/monkeyk/sos/domain/oauth/ClaimsOAuth2TokenCustomizer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7c4030b0674c14723d2dad68c357494cc4289ef
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/domain/oauth/ClaimsOAuth2TokenCustomizer.java
@@ -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
+ *
+ * 扩展 jwt id_token claims 属性生成
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+public class ClaimsOAuth2TokenCustomizer implements OAuth2TokenCustomizer {
+
+ 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 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);
+ }
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/domain/oauth/CustomJdbcClientDetailsService.java b/src/main/java/com/monkeyk/sos/domain/oauth/CustomJdbcClientDetailsService.java
deleted file mode 100644
index a0cbc8c9e22a5cd39b630d3ef1e78e16cd416445..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/domain/oauth/CustomJdbcClientDetailsService.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.monkeyk.sos.domain.oauth;
-
-import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
-
-import javax.sql.DataSource;
-
-/**
- * Add archived = 0 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);
- }
-
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/domain/oauth/OauthClientDetails.java b/src/main/java/com/monkeyk/sos/domain/oauth/OauthClientDetails.java
index b98fbe926f0267d2a163aeb21cf4715edac8748d..25aa54348733cf2b30b55134a22018495dded7ff 100644
--- a/src/main/java/com/monkeyk/sos/domain/oauth/OauthClientDetails.java
+++ b/src/main/java/com/monkeyk/sos/domain/oauth/OauthClientDetails.java
@@ -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
+ * secret 过期时间,
+ * null则无过期;
+ * 可用于一些临时签发使用
+ *
+ * @since 3.0.0
*/
- private String scope;
+ private Instant clientSecretExpiresAt;
+
/**
- * grant types include
- * "authorization_code", "password", "assertion", and "refresh_token".
- * Default value is "authorization_code,refresh_token".
+ * 认证支持的方式,多个由逗号分隔
+ * 如: client_secret_basic,client_secret_post
+ *
+ * @see org.springframework.security.oauth2.core.ClientAuthenticationMethod
+ * @since 3.0.0
*/
- private String authorizedGrantTypes = "authorization_code,refresh_token";
+ private String clientAuthenticationMethods;
+
/**
- * The re-direct URI(s) established during registration (optional, comma separated).
+ * OIDC scope 值, 多个由逗号分隔
+ * 如: openid,profile,email
+ *
+ * @see org.springframework.security.oauth2.core.oidc.OidcScopes
*/
- private String webServerRedirectUri;
+ private String scopes;
+
/**
- * 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.
- *
- * For example: ROLE_USER
+ * 授权支持的 grant_type (OAuth2.1), 多个由逗号分隔
+ * 如: authorization_code,refresh_token
+ *
+ * @see org.springframework.security.oauth2.core.AuthorizationGrantType
*/
- private String authorities;
+ private String authorizationGrantTypes;
/**
- * The access token validity period in seconds (optional).
- * If unspecified a global default will be applied by the token services.
+ * OAuth2 认证后回调uri, 一般传递code, 多个由逗号分隔
+ * The re-direct URI(s) established during registration (optional, comma separated).
*/
- private Integer accessTokenValidity;
+ private String redirectUris;
+
/**
- * The refresh token validity period in seconds (optional).
- * If unspecified a global default will be applied by the token services.
+ * OAuth2 退出时 post 的客户端重定向 uri,可选
+ * 多个由逗号分隔
+ * 在client注册时可填写
+ *
+ * @since 3.0.0
*/
- private Integer refreshTokenValidity;
+ private String postLogoutRedirectUris;
- // optional
- private String additionalInformation;
/**
- * The client is trusted or not. If it is trust, will skip approve step
- * default false.
+ * 客户端的各类设置
+ * 如是否支持PKCE,用户授权(consent)确认是否必须
+ * 必须由 {ClientSettings} 生成的字符串
+ *
+ * @see org.springframework.security.oauth2.server.authorization.settings.ClientSettings
+ * @since 3.0.0
*/
- private boolean trusted = false;
+ private String clientSettings;
/**
- * Value is 'true' or 'false', default 'false'
+ * token的各类设置
+ * 如 token有效期,refresh_token有效期
+ * 必须由 {TokenSettings} 生成的字符串
+ *
+ * @see org.springframework.security.oauth2.server.authorization.settings.TokenSettings
+ * @since 3.0.0
*/
- private String autoApprove;
+ private String tokenSettings;
+
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,118 +203,144 @@ 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;
}
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append("OauthClientDetails");
- 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('}');
- return sb.toString();
+ /**
+ * @since 3.0.0
+ */
+ public OauthClientDetails clientAuthenticationMethods(String clientAuthenticationMethods) {
+ this.clientAuthenticationMethods = clientAuthenticationMethods;
+ return this;
}
- public OauthClientDetails clientId(String clientId) {
- this.clientId = clientId;
- return this;
+ public String scopes() {
+ return scopes;
}
- public OauthClientDetails clientSecret(String clientSecret) {
- this.clientSecret = clientSecret;
+ public OauthClientDetails scopes(String scopes) {
+ this.scopes = scopes;
return this;
}
- public OauthClientDetails resourceIds(String resourceIds) {
- this.resourceIds = resourceIds;
- return this;
+ public String authorizationGrantTypes() {
+ return authorizationGrantTypes;
}
- public OauthClientDetails authorizedGrantTypes(String authorizedGrantTypes) {
- this.authorizedGrantTypes = authorizedGrantTypes;
+ public OauthClientDetails authorizationGrantTypes(String authorizationGrantTypes) {
+ this.authorizationGrantTypes = authorizationGrantTypes;
return this;
}
- public OauthClientDetails scope(String scope) {
- this.scope = scope;
- return this;
+ public String redirectUris() {
+ return redirectUris;
}
- public OauthClientDetails webServerRedirectUri(String webServerRedirectUri) {
- this.webServerRedirectUri = webServerRedirectUri;
+ public OauthClientDetails redirectUris(String redirectUris) {
+ this.redirectUris = redirectUris;
return this;
}
- public OauthClientDetails authorities(String authorities) {
- this.authorities = authorities;
- return this;
+ /**
+ * @since 3.0.0
+ */
+ public String postLogoutRedirectUris() {
+ return postLogoutRedirectUris;
}
- public OauthClientDetails accessTokenValidity(Integer accessTokenValidity) {
- this.accessTokenValidity = accessTokenValidity;
+ /**
+ * @since 3.0.0
+ */
+ public OauthClientDetails postLogoutRedirectUris(String postLogoutRedirectUris) {
+ this.postLogoutRedirectUris = postLogoutRedirectUris;
return this;
}
- public OauthClientDetails refreshTokenValidity(Integer refreshTokenValidity) {
- this.refreshTokenValidity = refreshTokenValidity;
- return this;
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("OauthClientDetails");
+ sb.append("{createTime=").append(createTime);
+ sb.append(", archived=").append(archived);
+ sb.append(", clientId='").append(clientId).append('\'');
+ 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();
}
- public OauthClientDetails trusted(boolean trusted) {
- this.trusted = trusted;
+ public OauthClientDetails clientId(String clientId) {
+ this.clientId = clientId;
return this;
}
- public OauthClientDetails additionalInformation(String additionalInformation) {
- this.additionalInformation = additionalInformation;
+ public OauthClientDetails clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
return this;
}
+
public OauthClientDetails archived(boolean archived) {
this.archived = archived;
return this;
diff --git a/src/main/java/com/monkeyk/sos/domain/oauth/OauthRepository.java b/src/main/java/com/monkeyk/sos/domain/oauth/OauthRepository.java
index ebdd100d3e84433394be102ca794e13a7e83e653..e73885fa24e2671935e25b02158279d25fdbcbd4 100644
--- a/src/main/java/com/monkeyk/sos/domain/oauth/OauthRepository.java
+++ b/src/main/java/com/monkeyk/sos/domain/oauth/OauthRepository.java
@@ -6,6 +6,7 @@ import java.util.List;
/**
* @author Shengzhao Li
+ * @since 1.0.0
*/
public interface OauthRepository extends Repository {
diff --git a/src/main/java/com/monkeyk/sos/domain/shared/GuidGenerator.java b/src/main/java/com/monkeyk/sos/domain/shared/GuidGenerator.java
index 6c561f0a1f32650c5bccb2f2755c40685fe0c02c..54911a9b726ef5fbfd3016b57990f36575ace833 100644
--- a/src/main/java/com/monkeyk/sos/domain/shared/GuidGenerator.java
+++ b/src/main/java/com/monkeyk/sos/domain/shared/GuidGenerator.java
@@ -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);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/domain/shared/SOSConstants.java b/src/main/java/com/monkeyk/sos/domain/shared/SOSConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ffefccbca5f533c3cdb4e31d114d692c75dc671
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/domain/shared/SOSConstants.java
@@ -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";
+
+}
diff --git a/src/main/java/com/monkeyk/sos/domain/shared/security/SOSUserDetails.java b/src/main/java/com/monkeyk/sos/domain/shared/security/SOSUserDetails.java
index 47718d258aba359120b3c5c16c5cee16548e75f8..8eaf1489c385428d4658130e8399182437ba15e5 100644
--- a/src/main/java/com/monkeyk/sos/domain/shared/security/SOSUserDetails.java
+++ b/src/main/java/com/monkeyk/sos/domain/shared/security/SOSUserDetails.java
@@ -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 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 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 getAuthorities() {
- return this.grantedAuthorities;
- }
-
- @Override
- public String getPassword() {
- return user.password();
- }
-
- @Override
- public String getUsername() {
- return user.username();
- }
+ protected String userGuid;
- @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 SOSUserDetails(User user) {
+ super(user.username(), user.password(), user.enabled(),
+ true, true, true, user.generateAuthorities());
+ this.userGuid = user.guid();
}
- public User user() {
- return user;
+ public String getUserGuid() {
+ return userGuid;
}
-
- @Override
- public String toString() {
- final StringBuilder sb = new StringBuilder();
- sb.append("{user=").append(user);
- sb.append('}');
- return sb.toString();
- }
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/domain/user/Privilege.java b/src/main/java/com/monkeyk/sos/domain/user/Privilege.java
index 3068fbc5d2a2b298bda2b4f497b5b37f38e37e0f..0c5933a603a70bcabab1f745a62ed3fa1b07d3c7 100644
--- a/src/main/java/com/monkeyk/sos/domain/user/Privilege.java
+++ b/src/main/java/com/monkeyk/sos/domain/user/Privilege.java
@@ -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
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/domain/user/User.java b/src/main/java/com/monkeyk/sos/domain/user/User.java
index 586a27f9f558deb0e55824207772dca84383397d..c7435fae766b01aeb879fbc55daa811ce667537a 100644
--- a/src/main/java/com/monkeyk/sos/domain/user/User.java
+++ b/src/main/java/com/monkeyk/sos/domain/user/User.java
@@ -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 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 generateAuthorities() {
+ Set authorities = new HashSet<>();
+ //Default, everyone include
+ authorities.add(DEFAULT_USER_ROLE);
+
+ final List privileges = this.privileges();
+ for (Privilege privilege : privileges) {
+ authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + privilege.name()));
+ }
+ return authorities;
+ }
+
+
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/domain/user/UserRepository.java b/src/main/java/com/monkeyk/sos/domain/user/UserRepository.java
index eaf2c57b6d9dd2c83649c1d248e03066bf5f2d75..3211bc015373449cbeb0ee4b312773e19416ae19 100644
--- a/src/main/java/com/monkeyk/sos/domain/user/UserRepository.java
+++ b/src/main/java/com/monkeyk/sos/domain/user/UserRepository.java
@@ -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 findUsersByUsername(String username);
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/PKCEUtils.java b/src/main/java/com/monkeyk/sos/infrastructure/PKCEUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..4bceb207e6592cb48cb30414dd91a1f5919415ff
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/infrastructure/PKCEUtils.java
@@ -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
+ *
+ * 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);
+ }
+
+
+}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/PasswordHandler.java b/src/main/java/com/monkeyk/sos/infrastructure/PasswordHandler.java
index 4224052383d1d8b29b40515cb15eb0d6a218da7b..67160d62ac6068e268ec12d995374b318574c8a3 100644
--- a/src/main/java/com/monkeyk/sos/infrastructure/PasswordHandler.java
+++ b/src/main/java/com/monkeyk/sos/infrastructure/PasswordHandler.java
@@ -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);
}
}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/SettingsUtils.java b/src/main/java/com/monkeyk/sos/infrastructure/SettingsUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0a057aab79663d9e06807aa42405841f1e556d1
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/infrastructure/SettingsUtils.java
@@ -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 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 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 map = settings.getSettings();
+ return writeMap(map);
+ }
+
+
+ /**
+ * ClientSettings -> text
+ *
+ * @param settings ClientSettings
+ * @return text
+ */
+ public static String textClientSettings(ClientSettings settings) {
+ Map map = settings.getSettings();
+ return writeMap(map);
+ }
+
+
+ /**
+ * text settings -> ClientSettings
+ *
+ * @param settings text
+ * @return ClientSettings
+ */
+ public static ClientSettings buildClientSettings(String settings) {
+ Map map = parseMap(settings);
+ return ClientSettings.withSettings(map)
+ .build();
+ }
+
+
+ private static Map parseMap(String data) {
+ try {
+ return objectMapper.readValue(data, new TypeReference<>() {
+ });
+ } catch (Exception ex) {
+ throw new IllegalArgumentException(ex.getMessage(), ex);
+ }
+ }
+
+ private static String writeMap(Map data) {
+ try {
+ return objectMapper.writeValueAsString(data);
+ } catch (Exception ex) {
+ throw new IllegalArgumentException(ex.getMessage(), ex);
+ }
+ }
+
+}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthClientDetailsRowMapper.java b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthClientDetailsRowMapper.java
index 43910f77374c54731d6736f34fa1e8ba0f0485e4..82ea384b2b4afde8fa11597ff42d2b23098799ee 100644
--- a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthClientDetailsRowMapper.java
+++ b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthClientDetailsRowMapper.java
@@ -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 list = this.jdbcTemplate.query(sql, new Object[]{clientId}, oauthClientDetailsRowMapper);
+ final String sql = " select * from oauth2_registered_client where client_id = ? ";
+ final List list = this.jdbcTemplate.query(sql, oauthClientDetailsRowMapper, clientId);
return list.isEmpty() ? null : list.get(0);
}
@Override
public List 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());
-
- ps.setString(3, clientDetails.clientSecret());
- ps.setString(4, clientDetails.scope());
-
- ps.setString(5, clientDetails.authorizedGrantTypes());
- ps.setString(6, clientDetails.webServerRedirectUri());
-
- 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());
-
+ 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(index++, clientDetails.clientSecret());
+ Instant clientSecretExpiresAt = clientDetails.clientSecretExpiresAt();
+ ps.setTimestamp(index++, clientSecretExpiresAt != null ? Timestamp.from(clientSecretExpiresAt) : null);
+ ps.setString(index++, clientDetails.clientName());
+
+ ps.setString(index++, clientDetails.clientAuthenticationMethods());
+ ps.setString(index++, clientDetails.authorizationGrantTypes());
+ ps.setString(index++, clientDetails.redirectUris());
+
+ ps.setString(index++, clientDetails.postLogoutRedirectUris());
+ ps.setString(index++, clientDetails.scopes());
+ ps.setString(index++, clientDetails.clientSettings());
+
+ ps.setString(index++, clientDetails.tokenSettings());
});
}
}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserProfileRowMapper.java b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserProfileRowMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..29d0f326d98249718621d164eb73ad2c793da595
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserProfileRowMapper.java
@@ -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 {
+
+
+ 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;
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbc.java b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbc.java
index 7f91a5a6583a9fcdddf697c595f43a815988ca20..1b4819935b2337b8a9f989e3414c317ada60dba2 100644
--- a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbc.java
+++ b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbc.java
@@ -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 list = this.jdbcTemplate.query(sql, new Object[]{guid}, userRowMapper);
+ final List 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 findPrivileges(int userId) {
+ private Collection findPrivileges(long userId) {
final String sql = " select privilege from user_privilege where user_id = ? ";
- final List strings = this.jdbcTemplate.queryForList(sql, new Object[]{userId}, String.class);
+ final List strings = this.jdbcTemplate.queryForList(sql, String.class, userId);
List 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 list = this.jdbcTemplate.query(sql, new Object[]{username}, userRowMapper);
+ final List 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 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 list = this.jdbcTemplate.query(sql, params, userRowMapper);
+ final List list = this.jdbcTemplate.query(sql, userRowMapper, params);
for (User user : list) {
user.privileges().addAll(findPrivileges(user.id()));
}
diff --git a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRowMapper.java b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRowMapper.java
index 4b60427dc5af285b55a79f193bd87a68c448db0f..c1592a55434664933870e94a63bdd334f3256304 100644
--- a/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRowMapper.java
+++ b/src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRowMapper.java
@@ -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.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;
}
diff --git a/src/main/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvoker.java b/src/main/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvoker.java
index 8f26763e55fc4f334b361d0ee58309f8881287c0..8d50052e2411f91c20a3dc62819674bccc6dce29 100644
--- a/src/main/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvoker.java
+++ b/src/main/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvoker.java
@@ -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);
+// }
}
diff --git a/src/main/java/com/monkeyk/sos/service/business/InlineAccessTokenInvoker.java b/src/main/java/com/monkeyk/sos/service/business/InlineAccessTokenInvoker.java
index a79a62467ecebc7ab73939a8fa834f3e9dca1699..99b2f84a871d60b0abf37d57c61da1a3f983d2ba 100644
--- a/src/main/java/com/monkeyk/sos/service/business/InlineAccessTokenInvoker.java
+++ b/src/main/java/com/monkeyk/sos/service/business/InlineAccessTokenInvoker.java
@@ -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;
- }
-
- 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;
+// 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);
+
+// 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);
- }
-
-
- public void setAuthenticationManager(AuthenticationManager authenticationManager) {
- this.authenticationManager = authenticationManager;
- }
-
- public void setTokenServices(AuthorizationServerTokenServices tokenServices) {
- this.tokenServices = tokenServices;
- }
-
- public void setClientDetailsService(ClientDetailsService clientDetailsService) {
- this.clientDetailsService = 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 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");
}
}
diff --git a/src/main/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvoker.java b/src/main/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvoker.java
index aaf5ddfc7fd73364cd975f6ce52c6056887fc4ac..b2b7a46643ac0cee34867e0aae595ac04bc14e1c 100644
--- a/src/main/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvoker.java
+++ b/src/main/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvoker.java
@@ -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);
+// }
diff --git a/src/main/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvoker.java b/src/main/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvoker.java
index 4b66c9d67f599dfc793bab1a4f9481eeaea00d39..3d478c64a037dd3ad9ee2da80cf190047410f7e7 100644
--- a/src/main/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvoker.java
+++ b/src/main/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvoker.java
@@ -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);
+// }
}
diff --git a/src/main/java/com/monkeyk/sos/service/dto/AccessTokenDto.java b/src/main/java/com/monkeyk/sos/service/dto/AccessTokenDto.java
index cb97b84548e17738d5b87cdec7ec2cccb2be4237..62c1412cddc8591a9bad60d122fd1e1246efefce 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/AccessTokenDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/AccessTokenDto.java
@@ -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.scope = StringUtils.join(token.getScope(), ",");
- this.tokenType = token.getTokenType();
+ this(token, null);
+ }
- final OAuth2RefreshToken oAuth2RefreshToken = token.getRefreshToken();
- if (oAuth2RefreshToken != null) {
- this.refreshToken = oAuth2RefreshToken.getValue();
- }
+ /**
+ * @since 3.0.0
+ */
+ public AccessTokenDto(OAuth2AccessToken token, OAuth2RefreshToken refreshToken) {
+ this.accessToken = token.getTokenValue();
+ this.expiresIn = token.getExpiresAt().get(ChronoField.SECOND_OF_DAY);
+
+ 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();
+// }
}
diff --git a/src/main/java/com/monkeyk/sos/service/dto/ClientSettingsDto.java b/src/main/java/com/monkeyk/sos/service/dto/ClientSettingsDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..072d72cd644880cf70fa07f46c85f1367bd5ffe2
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/service/dto/ClientSettingsDto.java
@@ -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
+ *
+ * .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 + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/service/dto/OauthClientDetailsDto.java b/src/main/java/com/monkeyk/sos/service/dto/OauthClientDetailsDto.java
index bae676282fda73a289c2140f8234e632fd77f19d..8d632be093a77854de6438b1fd88c6a367b987b6 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/OauthClientDetailsDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/OauthClientDetailsDto.java
@@ -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;
- private String clientSecret = GuidGenerator.generateClientSecret();
- private String scope;
+ /**
+ * client 名称,
+ * 一般由添加时填写
+ *
+ * @since 3.0.0
+ */
+ private String clientName;
- private String authorizedGrantTypes;
- private String webServerRedirectUri;
+ /**
+ * client 签发时间,一般指创建时间
+ *
+ * @since 3.0.0
+ */
+ private String clientIdIssuedAt;
- private String authorities;
- private Integer accessTokenValidity;
+ private String clientSecret = GuidGenerator.generateClientSecret();
- private Integer refreshTokenValidity;
- // optional
- private String additionalInformation;
+ /**
+ * secret 过期时间,
+ * null则无过期;
+ * 可用于一些临时签发使用
+ *
+ * @since 3.0.0
+ */
+ private String 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 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,133 +194,202 @@ public class OauthClientDetailsDto implements Serializable {
this.clientSecret = clientSecret;
}
- public String getScope() {
- return scope;
+
+ public static List toDtos(List clientDetailses) {
+ List dtos = new ArrayList<>(clientDetailses.size());
+ for (OauthClientDetails clientDetailse : clientDetailses) {
+ dtos.add(new OauthClientDetailsDto(clientDetailse));
+ }
+ return dtos;
}
- public String getScopeWithBlank() {
- if (scope != null && scope.contains(",")) {
- return scope.replaceAll(",", " ");
+ public boolean isContainsAuthorizationCode() {
+ if (!this.authorizationGrantTypes.contains(AUTHORIZATION_CODE.getValue())) {
+ return false;
}
- return scope;
+ if (clientSettings == null) {
+ return true;
+ }
+ return !clientSettings.isRequireProofKey();
}
- public void setScope(String scope) {
- this.scope = scope;
+ /**
+ * PKCE flow
+ *
+ * @since 3.0.0
+ */
+ public boolean isContainsAuthorizationCodeWithPKCE() {
+ if (!this.authorizationGrantTypes.contains(AUTHORIZATION_CODE.getValue())) {
+ return false;
+ }
+ return clientSettings != null && clientSettings.isRequireProofKey();
}
- public String getAuthorizedGrantTypes() {
- return authorizedGrantTypes;
+ /**
+ * OAuth2.1不支持
+ *
+ * @deprecated from OAuth2.1
+ */
+ public boolean isContainsPassword() {
+ return this.authorizationGrantTypes.contains(PASSWORD.getValue());
}
- public void setAuthorizedGrantTypes(String authorizedGrantTypes) {
- this.authorizedGrantTypes = authorizedGrantTypes;
+// public boolean isContainsImplicit() {
+// return this.authorizationGrantTypes.contains("implicit");
+// }
+
+ public boolean isContainsClientCredentials() {
+ return this.authorizationGrantTypes.contains(CLIENT_CREDENTIALS.getValue());
}
- public String getWebServerRedirectUri() {
- return webServerRedirectUri;
+ public boolean isContainsRefreshToken() {
+ return this.authorizationGrantTypes.contains(REFRESH_TOKEN.getValue());
}
- public void setWebServerRedirectUri(String webServerRedirectUri) {
- this.webServerRedirectUri = webServerRedirectUri;
+ /**
+ * @since 3.0.0
+ */
+ public boolean isContainsDeviceCode() {
+ return this.authorizationGrantTypes.contains(DEVICE_CODE.getValue());
}
- public String getAuthorities() {
- return authorities;
+ /**
+ * @since 3.0.0
+ */
+ public boolean isContainsJwtBearer() {
+ return this.authorizationGrantTypes.contains(JWT_BEARER.getValue());
}
- public void setAuthorities(String authorities) {
- this.authorities = authorities;
+
+ public OauthClientDetails createDomain() {
+ OauthClientDetails clientDetails = new OauthClientDetails()
+ .id(GuidGenerator.generateNumber())
+ .clientId(clientId)
+ .clientName(clientName)
+ // encrypted client secret
+ .clientSecret(PasswordHandler.encode(clientSecret))
+ .postLogoutRedirectUris(postLogoutRedirectUris)
+ .authorizationGrantTypes(authorizationGrantTypes)
+ .clientAuthenticationMethods(clientAuthenticationMethods)
+ .scopes(scopes);
+
+ if (StringUtils.isNotBlank(clientIdIssuedAt)) {
+ clientDetails.clientIdIssuedAt(Instant.parse(this.clientIdIssuedAt));
+ }
+
+ if (StringUtils.isNotBlank(clientSecretExpiresAt)) {
+ clientDetails.clientSecretExpiresAt(Instant.parse(this.clientSecretExpiresAt));
+ }
+
+ if (StringUtils.isNotEmpty(redirectUris)) {
+ clientDetails.redirectUris(redirectUris);
+ }
+
+ clientDetails.clientSettings(SettingsUtils.textClientSettings(this.clientSettings.toSettings()));
+ clientDetails.tokenSettings(SettingsUtils.textTokenSettings(this.tokenSettings.toSettings()));
+
+ return clientDetails;
}
- public Integer getAccessTokenValidity() {
- return accessTokenValidity;
+
+ public String getId() {
+ return id;
}
- public void setAccessTokenValidity(Integer accessTokenValidity) {
- this.accessTokenValidity = accessTokenValidity;
+ public void setId(String id) {
+ this.id = id;
}
- public Integer getRefreshTokenValidity() {
- return refreshTokenValidity;
+ public String getClientName() {
+ return clientName;
}
- public void setRefreshTokenValidity(Integer refreshTokenValidity) {
- this.refreshTokenValidity = refreshTokenValidity;
+ public void setClientName(String clientName) {
+ this.clientName = clientName;
}
- public String getAdditionalInformation() {
- return additionalInformation;
+ public String getClientIdIssuedAt() {
+ return clientIdIssuedAt;
}
- public void setAdditionalInformation(String additionalInformation) {
- this.additionalInformation = additionalInformation;
+ public void setClientIdIssuedAt(String clientIdIssuedAt) {
+ this.clientIdIssuedAt = clientIdIssuedAt;
}
- public boolean isTrusted() {
- return trusted;
+ public String getClientSecretExpiresAt() {
+ return clientSecretExpiresAt;
}
- public void setTrusted(boolean trusted) {
- this.trusted = trusted;
+ public void setClientSecretExpiresAt(String clientSecretExpiresAt) {
+ this.clientSecretExpiresAt = clientSecretExpiresAt;
}
- public static List toDtos(List clientDetailses) {
- List dtos = new ArrayList<>(clientDetailses.size());
- for (OauthClientDetails clientDetailse : clientDetailses) {
- dtos.add(new OauthClientDetailsDto(clientDetailse));
- }
- return dtos;
+ public String getClientAuthenticationMethods() {
+ return clientAuthenticationMethods;
}
+ public void setClientAuthenticationMethods(String clientAuthenticationMethods) {
+ this.clientAuthenticationMethods = clientAuthenticationMethods;
+ }
- public boolean isContainsAuthorizationCode() {
- return this.authorizedGrantTypes.contains("authorization_code");
+ public String getScopes() {
+ return scopes;
}
- public boolean isContainsPassword() {
- return this.authorizedGrantTypes.contains("password");
+ public void setScopes(String scopes) {
+ this.scopes = scopes;
}
- public boolean isContainsImplicit() {
- return this.authorizedGrantTypes.contains("implicit");
+ public String getAuthorizationGrantTypes() {
+ return authorizationGrantTypes;
}
- public boolean isContainsClientCredentials() {
- return this.authorizedGrantTypes.contains("client_credentials");
+ public void setAuthorizationGrantTypes(String authorizationGrantTypes) {
+ this.authorizationGrantTypes = authorizationGrantTypes;
}
- public boolean isContainsRefreshToken() {
- return this.authorizedGrantTypes.contains("refresh_token");
+ public String getRedirectUris() {
+ return redirectUris;
+ }
+
+ public void setRedirectUris(String redirectUris) {
+ this.redirectUris = redirectUris;
}
+ public String getPostLogoutRedirectUris() {
+ return postLogoutRedirectUris;
+ }
- public OauthClientDetails createDomain() {
- OauthClientDetails clientDetails = new OauthClientDetails()
- .clientId(clientId)
- // encrypted client secret
- .clientSecret(PasswordHandler.encode(clientSecret))
- .resourceIds(resourceIds)
- .authorizedGrantTypes(authorizedGrantTypes)
- .scope(scope);
+ public void setPostLogoutRedirectUris(String postLogoutRedirectUris) {
+ this.postLogoutRedirectUris = postLogoutRedirectUris;
+ }
- if (StringUtils.isNotEmpty(webServerRedirectUri)) {
- clientDetails.webServerRedirectUri(webServerRedirectUri);
- }
+ public ClientSettingsDto getClientSettings() {
+ return clientSettings;
+ }
- if (StringUtils.isNotEmpty(authorities)) {
- clientDetails.authorities(authorities);
- }
+ public void setClientSettings(ClientSettingsDto clientSettings) {
+ this.clientSettings = clientSettings;
+ }
+
+ public TokenSettingsDto getTokenSettings() {
+ return tokenSettings;
+ }
- clientDetails.accessTokenValidity(accessTokenValidity)
- .refreshTokenValidity(refreshTokenValidity)
- .trusted(trusted);
+ public void setTokenSettings(TokenSettingsDto tokenSettings) {
+ this.tokenSettings = tokenSettings;
+ }
- if (StringUtils.isNotEmpty(additionalInformation)) {
- clientDetails.additionalInformation(additionalInformation);
+ /**
+ * 逗号, 转化为 ' '
+ *
+ * @return scopes
+ */
+ public String getScopesWithBlank() {
+ if (scopes != null && scopes.contains(",")) {
+ return scopes.replaceAll(",", " ");
}
-
- return clientDetails;
+ return scopes;
}
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/service/dto/TokenSettingsDto.java b/src/main/java/com/monkeyk/sos/service/dto/TokenSettingsDto.java
new file mode 100644
index 0000000000000000000000000000000000000000..2305f43b911318e66dc83c65070d240bf44eb886
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/service/dto/TokenSettingsDto.java
@@ -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
+ *
+ *
+ * .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 + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/service/dto/UserDto.java b/src/main/java/com/monkeyk/sos/service/dto/UserDto.java
index b0eebd3617098ea6f0b0c74750754928f2b486be..14b2447a757777405c34e1e9624664219cea0535 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/UserDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/UserDto.java
@@ -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 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() {
diff --git a/src/main/java/com/monkeyk/sos/service/dto/UserFormDto.java b/src/main/java/com/monkeyk/sos/service/dto/UserFormDto.java
index 1468c16ab359e962fe90820694412c06bb08ab93..67c34a9730a2feabcaa5b44f452eb4bb735ba919 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/UserFormDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/UserFormDto.java
@@ -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;
}
}
diff --git a/src/main/java/com/monkeyk/sos/service/dto/UserJsonDto.java b/src/main/java/com/monkeyk/sos/service/dto/UserJsonDto.java
index 76a6b0830f385574bbc583566764de0ceebc7ef5..6d330c59eae7eca1b8b418157a21fbecc37d9720 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/UserJsonDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/UserJsonDto.java
@@ -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;
diff --git a/src/main/java/com/monkeyk/sos/service/dto/UserOverviewDto.java b/src/main/java/com/monkeyk/sos/service/dto/UserOverviewDto.java
index 18fcb38177ad79915a1f858a700c15a73809efce..26cbeda33f4d28f199e6794d35e358f10eb43a9b 100644
--- a/src/main/java/com/monkeyk/sos/service/dto/UserOverviewDto.java
+++ b/src/main/java/com/monkeyk/sos/service/dto/UserOverviewDto.java
@@ -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;
diff --git a/src/main/java/com/monkeyk/sos/service/impl/OauthServiceImpl.java b/src/main/java/com/monkeyk/sos/service/impl/OauthServiceImpl.java
index 947aa6abfbe646570d6268a91c06776cfdc056d1..c165ea1ff5c5587473c71b7c5dc4b4ac330dfd98 100644
--- a/src/main/java/com/monkeyk/sos/service/impl/OauthServiceImpl.java
+++ b/src/main/java/com/monkeyk/sos/service/impl/OauthServiceImpl.java
@@ -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 loadAllOauthClientDetailsDtos() {
List 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);
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/service/impl/UserServiceImpl.java b/src/main/java/com/monkeyk/sos/service/impl/UserServiceImpl.java
index 33ad8b356284bee2271eb9bbfcbdc58139c83e26..aba1615ce8711c74477aeda356e29ea08f624f12 100644
--- a/src/main/java/com/monkeyk/sos/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/monkeyk/sos/service/impl/UserServiceImpl.java
@@ -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 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 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 authorities = oAuth2Authentication.getAuthorities();
+// for (GrantedAuthority authority : authorities) {
+// userJsonDto.getPrivileges().add(authority.getAuthority());
+// }
+//
+// return userJsonDto;
+// }
}
\ No newline at end of file
diff --git a/src/main/java/com/monkeyk/sos/web/WebUtils.java b/src/main/java/com/monkeyk/sos/web/WebUtils.java
index cb05febce0fb5c827a25e8cee506d02302c1fb9b..9a246b51b3c25dad2d8155578037175d5df16a7d 100644
--- a/src/main/java/com/monkeyk/sos/web/WebUtils.java
+++ b/src/main/java/com/monkeyk/sos/web/WebUtils.java
@@ -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
*/
- public static final String VERSION = "2.1.0";
+ public static final String VERSION = "3.0.0";
private static ThreadLocal ipThreadLocal = new ThreadLocal<>();
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/AbstractAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/AbstractAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa49eddcf344ff2530f766fa7ccb920910c07f86
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/AbstractAuthenticationRestConverter.java
@@ -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);
+ }
+
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/AuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/AuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..778c029fabd5dd59f630811bcbff53246f8ac755
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/AuthenticationRestConverter.java
@@ -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 parameters);
+
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/DelegatingAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/DelegatingAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d92db8041fbb1bc8e63adbd94b9bd2fa98cebe11
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/DelegatingAuthenticationRestConverter.java
@@ -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 converters;
+
+ /**
+ * Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
+ *
+ * @param converters a {@code List} of {@link AuthenticationConverter}(s)
+ */
+ public DelegatingAuthenticationRestConverter(List converters) {
+ Assert.notEmpty(converters, "converters cannot be empty");
+ this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
+ }
+
+
+ @Override
+ public Authentication convert(Map 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;
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/OAuth2AuthorizationCodeAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2AuthorizationCodeAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..299afe1af63052a6906e691f577b1e02ebb27177
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2AuthorizationCodeAuthenticationRestConverter.java
@@ -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 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 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 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);
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/OAuth2ClientCredentialsAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2ClientCredentialsAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b467227b55ab6e34588e02da24e3b934251d6c5b
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2ClientCredentialsAuthenticationRestConverter.java
@@ -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 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 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 requestedScopes = null;
+ if (StringUtils.hasText(scope)) {
+ requestedScopes = new HashSet<>(
+ Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
+ }
+
+ Map 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);
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/OAuth2DeviceCodeAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2DeviceCodeAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f56eeda54ed09c0faa06cb153c35f9d4070c780
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2DeviceCodeAuthenticationRestConverter.java
@@ -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 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 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 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);
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/web/authentication/OAuth2RefreshTokenAuthenticationRestConverter.java b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2RefreshTokenAuthenticationRestConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b775755bf84455106ebd3c5bbf2442a3d91ea2ed
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/authentication/OAuth2RefreshTokenAuthenticationRestConverter.java
@@ -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 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 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 requestedScopes = null;
+ if (StringUtils.hasText(scope)) {
+ requestedScopes = new HashSet<>(
+ Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
+ }
+
+ Map 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);
+ }
+}
diff --git a/src/main/java/com/monkeyk/sos/web/context/SOSContextHolder.java b/src/main/java/com/monkeyk/sos/web/context/SOSContextHolder.java
index 038550ee1b2cf812edcbcf23702be93e655da067..ab02d6539ae20325cbacd203a27f429b0dd59b49 100644
--- a/src/main/java/com/monkeyk/sos/web/context/SOSContextHolder.java
+++ b/src/main/java/com/monkeyk/sos/web/context/SOSContextHolder.java
@@ -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);
}
}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/AuthorizationConsentController.java b/src/main/java/com/monkeyk/sos/web/controller/AuthorizationConsentController.java
new file mode 100644
index 0000000000000000000000000000000000000000..735e9234284a5c9c8b2164c36c1ee1e2ea074d92
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/controller/AuthorizationConsentController.java
@@ -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
+ *
+ * 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 scopesToApprove = new HashSet<>();
+ Set previouslyApprovedScopes = new HashSet<>();
+
+ OAuth2AuthorizationConsent currentAuthorizationConsent =
+ this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
+ Set 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 withDescription(Set scopes) {
+ Set 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 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);
+ }
+ }
+
+}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/ClientDetailsController.java b/src/main/java/com/monkeyk/sos/web/controller/ClientDetailsController.java
index 4cdccf612582e92255b6f1179cc3079813b698d7..55bc8ada3e44d6cbe742790a64c778e6f607ef30 100644
--- a/src/main/java/com/monkeyk/sos/web/controller/ClientDetailsController.java
+++ b/src/main/java/com/monkeyk/sos/web/controller/ClientDetailsController.java
@@ -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
+ *
+ * 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);
diff --git a/src/main/java/com/monkeyk/sos/web/controller/JwtBearerJwksController.java b/src/main/java/com/monkeyk/sos/web/controller/JwtBearerJwksController.java
new file mode 100644
index 0000000000000000000000000000000000000000..50244cc2c4dba3132cd73086b05995cef34471ff
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/controller/JwtBearerJwksController.java
@@ -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
+ *
+ * grant_type=jwt-bearer 中的 jwkSetUrl 实现参考
+ *
+ * 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 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);
+ }
+
+}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/OAuth2DeviceVerificationController.java b/src/main/java/com/monkeyk/sos/web/controller/OAuth2DeviceVerificationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..56048019ffa8276e4db02092b80244a32fc7b6c5
--- /dev/null
+++ b/src/main/java/com/monkeyk/sos/web/controller/OAuth2DeviceVerificationController.java
@@ -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
+ *
+ * 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";
+ }
+
+
+}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/OAuthRestController.java b/src/main/java/com/monkeyk/sos/web/controller/OAuthRestController.java
index 8a66d8af76af9e64ddee87f5f7b25fd93a18d768..f42aa77f18f9b3945d372aafef9b2e0dcbad942e 100644
--- a/src/main/java/com/monkeyk/sos/web/controller/OAuthRestController.java
+++ b/src/main/java/com/monkeyk/sos/web/controller/OAuthRestController.java
@@ -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 java.util.Collections;
+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.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 accessTokenHttpResponseConverter =
+ new OAuth2AccessTokenResponseHttpMessageConverter();
+ private final HttpMessageConverter 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;
+ @Autowired
+ private AuthorizationServerSettings authorizationServerSettings;
- private OAuth2RequestFactory oAuth2RequestFactory;
- private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
- private WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();
+ public OAuthRestController() {
+ this.authenticationConverter = new DelegatingAuthenticationRestConverter(
+ Arrays.asList(
+ new OAuth2AuthorizationCodeAuthenticationRestConverter(),
+ new OAuth2RefreshTokenAuthenticationRestConverter(),
+ new OAuth2ClientCredentialsAuthenticationRestConverter(),
+ new OAuth2DeviceCodeAuthenticationRestConverter()));
+ }
- @RequestMapping(value = "/oauth/rest_token", method = RequestMethod.POST)
+ /**
+ * 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 parameters) {
-
+ public void postAccessToken(@RequestBody Map parameters, HttpServletResponse response)
+ throws OAuth2AuthenticationException, IOException {
- String clientId = getClientId(parameters);
- ClientDetails authenticatedClient = clientDetailsService.loadClientByClientId(clientId);
+ //init OAuth2 contexts
+ initialOAuth2Contexts(parameters);
- //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");
- }
+ // oauth2 flow start...
+ String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
+ if (grantType == null) {
+ throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.GRANT_TYPE);
}
- 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");
- }
+ 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));
}
- oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
+ checkAndInitialAuthenticationManager();
- 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");
+ OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
+ (OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
+ this.sendAccessTokenResponse(response, accessTokenAuthentication);
+ }
+
+ private void initialOAuth2Contexts(Map parameters) {
+ String clientId = parameters.get(OAuth2ParameterNames.CLIENT_ID);
+ RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+ if (registeredClient == null) {
+ throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
}
- 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.emptySet());
- }
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Retrieved registered client");
}
+ if (!registeredClient.getClientAuthenticationMethods().contains(
+ ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+ throwInvalidClient("authentication_method");
+ }
- 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)));
+ String clientSecret = parameters.get(OAuth2ParameterNames.CLIENT_SECRET);
+ if (clientSecret == null) {
+ throwInvalidClient("credentials");
}
- OAuth2AccessToken token = getTokenGranter(grantType).grant(grantType, tokenRequest);
- if (token == null) {
- throw new UnsupportedGrantTypeException("Unsupported grant type: " + grantType);
+// 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");
+ }
- return token;
+ 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();
+ }
+ @Override
+ public AuthorizationServerSettings getAuthorizationServerSettings() {
+ return authorizationServerSettings;
+ }
+ };
+ AuthorizationServerContextHolder.setContext(authorizationServerContext);
}
- 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);
+ /**
+ * 异常处理
+ */
+ @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);
}
+ this.sendErrorResponse(response, ex);
}
- @ExceptionHandler(Exception.class)
- public ResponseEntity handleException(Exception e) throws Exception {
- LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
- return getExceptionTranslator().translate(e);
- }
-
- @ExceptionHandler(ClientRegistrationException.class)
- public ResponseEntity handleClientRegistrationException(Exception e) throws Exception {
- LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
- return getExceptionTranslator().translate(new BadClientCredentialsException());
+ private void checkAndInitialAuthenticationManager() {
+ if (this.authenticationManager == null) {
+ OAuth2ServerConfiguration serverConfiguration = applicationContext.getBean(OAuth2ServerConfiguration.class);
+ this.authenticationManager = serverConfiguration.authenticationManagerOAuth2();
+ Assert.notNull(this.authenticationManager, "authenticationManager cannot be null");
+ }
}
- @ExceptionHandler(OAuth2Exception.class)
- public ResponseEntity handleException(OAuth2Exception e) throws Exception {
- LOG.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
- return getExceptionTranslator().translate(e);
- }
+ private void sendErrorResponse(HttpServletResponse response,
+ AuthenticationException exception) throws IOException {
- private boolean isRefreshTokenRequest(Map parameters) {
- return "refresh_token".equals(parameters.get(OAuth2Utils.GRANT_TYPE)) && parameters.get("refresh_token") != null;
+ OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
+ ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
+ httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
+ this.errorHttpResponseConverter.write(error, null, httpResponse);
}
- private boolean isAuthCodeRequest(Map parameters) {
- return "authorization_code".equals(parameters.get(OAuth2Utils.GRANT_TYPE)) && parameters.get("code") != null;
- }
+ private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication) throws IOException {
+ OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
+ (OAuth2AccessTokenAuthenticationToken) authentication;
- protected String getClientId(Map parameters) {
- return parameters.get(OAuth2Utils.CLIENT_ID);
- }
+ OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
+ OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
+ Map additionalParameters = accessTokenAuthentication.getAdditionalParameters();
- protected String getClientSecret(Map parameters) {
- return parameters.get("client_secret");
- }
-
-
- private AuthenticationManager getAuthenticationManager() {
- return this.authenticationManager;
+ 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);
}
- @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);
+ 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);
}
- protected WebResponseExceptionTranslator getExceptionTranslator() {
- return providerExceptionHandler;
+ private static void throwInvalidClient(String parameterName) {
+ OAuth2Error error = new OAuth2Error(
+ OAuth2ErrorCodes.INVALID_CLIENT,
+ "Client authentication failed: " + parameterName,
+ CLIENT_ERROR_URI
+ );
+ throw new OAuth2AuthenticationException(error);
}
-
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
- if (this.authenticationManager == null) {
- this.authenticationManager = (AuthenticationManager) applicationContext.getBean("authenticationManagerBean");
- }
- }
}
diff --git a/src/main/java/com/monkeyk/sos/web/oauth/OauthClientDetailsDtoValidator.java b/src/main/java/com/monkeyk/sos/web/controller/OauthClientDetailsDtoValidator.java
similarity index 55%
rename from src/main/java/com/monkeyk/sos/web/oauth/OauthClientDetailsDtoValidator.java
rename to src/main/java/com/monkeyk/sos/web/controller/OauthClientDetailsDtoValidator.java
index 34a1e9cf9cf07ca4c5855e9a48fd93ca04eba3b2..255a24ba7c75e04763e493e5e33d7292f4d04433 100644
--- a/src/main/java/com/monkeyk/sos/web/oauth/OauthClientDetailsDtoValidator.java
+++ b/src/main/java/com/monkeyk/sos/web/controller/OauthClientDetailsDtoValidator.java
@@ -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;
}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/SOSController.java b/src/main/java/com/monkeyk/sos/web/controller/SOSController.java
index 97c5d4ee7fada90e7013135ded119da40680ace6..0704c6a0416a5c933987ffd8e8f0fd34dabac671 100644
--- a/src/main/java/com/monkeyk/sos/web/controller/SOSController.java
+++ b/src/main/java/com/monkeyk/sos/web/controller/SOSController.java
@@ -40,4 +40,16 @@ public class SOSController {
}
+// /**
+// * 403 无权限访问
+// *
+// * @return view
+// * @since 3.0.0
+// */
+// @GetMapping("/access_denied")
+// public String accessDenied() {
+// return "access_denied";
+// }
+
+
}
diff --git a/src/main/java/com/monkeyk/sos/web/controller/UserFormDtoValidator.java b/src/main/java/com/monkeyk/sos/web/controller/UserFormDtoValidator.java
index 37245d91174cee57e40fed3c8556073a7f8294a3..cd83b884ac11461925f073742b2c44ce9044fe97 100644
--- a/src/main/java/com/monkeyk/sos/web/controller/UserFormDtoValidator.java
+++ b/src/main/java/com/monkeyk/sos/web/controller/UserFormDtoValidator.java
@@ -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");
}
}
diff --git a/src/main/java/com/monkeyk/sos/web/filter/CharacterEncodingIPFilter.java b/src/main/java/com/monkeyk/sos/web/filter/CharacterEncodingIPFilter.java
index b1c27b3daa1a51d989bee7facf2d502aab4ddcfc..20f64c398c214f63b0073c2c1b84545d69942030 100644
--- a/src/main/java/com/monkeyk/sos/web/filter/CharacterEncodingIPFilter.java
+++ b/src/main/java/com/monkeyk/sos/web/filter/CharacterEncodingIPFilter.java
@@ -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);
+ }
}
}
diff --git a/src/main/java/com/monkeyk/sos/web/filter/SOSSiteMeshFilter.java b/src/main/java/com/monkeyk/sos/web/filter/SOSSiteMeshFilter.java
deleted file mode 100644
index f8693dedca3780c551c4dc05e3122d8113af36ee..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/web/filter/SOSSiteMeshFilter.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.monkeyk.sos.web.filter;
-
-import org.sitemesh.builder.SiteMeshFilterBuilder;
-import org.sitemesh.config.ConfigurableSiteMeshFilter;
-
-/**
- * 2018/2/3
- *
- * Replace decorator.xml
- *
- * 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/**");
-
-
- }
-}
diff --git a/src/main/java/com/monkeyk/sos/web/oauth/OauthUserApprovalHandler.java b/src/main/java/com/monkeyk/sos/web/oauth/OauthUserApprovalHandler.java
deleted file mode 100644
index 2b4e1145293b4ca31acac002f9a771779d7d4d85..0000000000000000000000000000000000000000
--- a/src/main/java/com/monkeyk/sos/web/oauth/OauthUserApprovalHandler.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a70ef43bc446192071f6c937e0ab3f8b3bd56500..a9fa91e43d70342838801f6c57779e7887bc8718 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -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
diff --git a/src/main/resources/jwks.json b/src/main/resources/jwks.json
new file mode 100644
index 0000000000000000000000000000000000000000..44af5411e65e1c66896464471f1f544645f39b64
--- /dev/null
+++ b/src/main/resources/jwks.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7e2b4ec30de65c42119a7b7cefa80ef4bbf8782e
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,47 @@
+
+
+ ${spring.application.name}
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n
+
+
+
+
+
+ true
+
+
+ logs/%d{yyyy-MM-dd}/sos-%i.log
+ 10MB
+ 15
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/logging.properties.old b/src/main/resources/logging.properties.old
deleted file mode 100644
index 1d66b3e2b32eb6debe0068af283200d3092ae229..0000000000000000000000000000000000000000
--- a/src/main/resources/logging.properties.old
+++ /dev/null
@@ -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
diff --git a/src/main/resources/spring-oauth-server.properties.old b/src/main/resources/spring-oauth-server.properties.old
deleted file mode 100644
index 539c2c3ebca5f2fadc7da18c382071c7e56c5cce..0000000000000000000000000000000000000000
--- a/src/main/resources/spring-oauth-server.properties.old
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/src/main/resources/spring/context.xml.old b/src/main/resources/spring/context.xml.old
deleted file mode 100644
index 49acc1a9586d02d4de50dd5eceb6cebe8eb7a405..0000000000000000000000000000000000000000
--- a/src/main/resources/spring/context.xml.old
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- classpath:spring-oauth-server.properties
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/resources/spring/security.xml.old b/src/main/resources/spring/security.xml.old
deleted file mode 100644
index 90c08752166dc564d99e4f252345829482aa089b..0000000000000000000000000000000000000000
--- a/src/main/resources/spring/security.xml.old
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/resources/spring/transaction.xml.old b/src/main/resources/spring/transaction.xml.old
deleted file mode 100644
index 7fe1d9e844c3baf468097c95d0a4ee46627faf5b..0000000000000000000000000000000000000000
--- a/src/main/resources/spring/transaction.xml.old
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/static/angular.min.js b/src/main/resources/static/angular.min.js
similarity index 100%
rename from src/main/webapp/static/angular.min.js
rename to src/main/resources/static/angular.min.js
diff --git a/src/main/webapp/static/api/SOS_API-0.5.html b/src/main/resources/static/api/SOS_API-0.5.html
similarity index 100%
rename from src/main/webapp/static/api/SOS_API-0.5.html
rename to src/main/resources/static/api/SOS_API-0.5.html
diff --git a/src/main/webapp/static/api/SOS_API-0.6.html b/src/main/resources/static/api/SOS_API-0.6.html
similarity index 100%
rename from src/main/webapp/static/api/SOS_API-0.6.html
rename to src/main/resources/static/api/SOS_API-0.6.html
diff --git a/src/main/webapp/static/api/SOS_API-1.0.html b/src/main/resources/static/api/SOS_API-1.0.html
similarity index 100%
rename from src/main/webapp/static/api/SOS_API-1.0.html
rename to src/main/resources/static/api/SOS_API-1.0.html
diff --git a/src/main/webapp/static/api/SOS_API-2.0.html b/src/main/resources/static/api/SOS_API-2.0.html
similarity index 100%
rename from src/main/webapp/static/api/SOS_API-2.0.html
rename to src/main/resources/static/api/SOS_API-2.0.html
diff --git a/src/main/resources/static/api/SOS_API-3.0.0.html b/src/main/resources/static/api/SOS_API-3.0.0.html
new file mode 100644
index 0000000000000000000000000000000000000000..ce44f5f8cb5e9d8224fa45f299102da0a77e6e99
--- /dev/null
+++ b/src/main/resources/static/api/SOS_API-3.0.0.html
@@ -0,0 +1,1198 @@
+
+
+
+
+
+
+
+ spring-oauth-server API
+
+
+
+
+
+
+
+
说明: 本文档用于描述spring-oauth-server对外开发的接口(API)使用, 所有标记
+
public
+ 的API都是公开的, 其他的API则需要先授权获取
+
access_token
+ 后可调用 (如何传递access_token请查看
https://andaily.com/blog/?p=500).
+
+
+
+
+
+
+
+
获取access_token (grant_type=authorization_code)
+ public
+
+
+
使用grant_type=authorization_code 方式来获取access_token, 需要先获取code
+
+
+
+
+
+
返回
+
+
获取access_token (grant_type=client_credentials)
+ public
+
+
+
使用grant_type=client_credentials 方式来获取access_token, 不需要username, password
+
+
+
+
+
+
返回
+
+
刷新access_token (grant_type=refresh_token)
+ public
+
+
+
用于在access_token要过期时换取新的access_token (grant_type需要有refresh_token)
+
+
+
+
+
+
获取access_token (Restful API)
+ public
+
+
+
Restful API 获取access_token,
+ 适用于grant_type为authorization_code,refresh_token,client_credentials
+
+
+
+
+
+
检查token (/oauth2/introspect)
+ public
+
+
+
校验, 检查token的有效性
+
+
+
+
+
+
返回
+
撤销token (/oauth2/revoke)
+ public
+
+
+
撤销已经签发的token
+
+
+
+
+
+
[device_code]流程 - 发起认证(/oauth2/device_authorization)public
+
+
发起认证, 获取user_code, device_code等信息
+
+
+
+
+
+
[device_code]流程 - 获取token(/oauth2/token)public
+
+
设备上轮循调用, 获取token
+
+
+
+
+
+
[jwt-bearer] - 获取token(/oauth2/token)public
+
+
jwt-bearer流程, 获取token
+
+
+
+
+
+
OIDC /userinfo
+
+
客户端带上access_token获取用户信息
+
+
+ -
+
+ 请求URI: /userinfo GET
+
+
+
+ 请求参数说明:
+
+
+
+ | 参数名 |
+ 参数值 |
+ 必须? |
+ 备注 |
+
+
+
+
+ | 无 |
+
+
+
+ 请求示例:
+
curl --location 'http://localhost:8080/userinfo' \
+--header 'Content-Type: application/json' \
+--header 'Authorization: Bearer eyJraWQiOiJzb3MtcnNhLWtpZDIiLCJhbGciOiJSUzI1NiJ9.eyJzdWI...'
+
+
+
+
+ 响应
+
+
+ -
+
+ 正常 [200]
+
{
+ "sub": "unity",
+ "updated_at": 0,
+ "nickname": ""
+}
+
具体有哪些属性值由scope范围来决定
+
+
+ -
+
+
+
+
+
+
+
+
+
OIDC /openid-configurationpublic
+
+
OIDC well-known API
+
+
+
+
+
+
返回
+
OAuth2.1 /oauth-authorization-serverpublic
+
+
OAuth2.1 well-known API
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/static/bootstrap.min.css b/src/main/resources/static/bootstrap.min.css
similarity index 100%
rename from src/main/webapp/static/bootstrap.min.css
rename to src/main/resources/static/bootstrap.min.css
diff --git a/src/main/webapp/static/favicon.ico b/src/main/resources/static/favicon.ico
similarity index 100%
rename from src/main/webapp/static/favicon.ico
rename to src/main/resources/static/favicon.ico
diff --git a/src/main/resources/templates/clientdetails/client_details.html b/src/main/resources/templates/clientdetails/client_details.html
new file mode 100644
index 0000000000000000000000000000000000000000..3150d3a65d56cafd375d5190ccb78cd14328f95d
--- /dev/null
+++ b/src/main/resources/templates/clientdetails/client_details.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+ client_details - Spring Security&OAuth2.1
+
+
+
+
+
+Home
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/clientdetails/register_client.html b/src/main/resources/templates/clientdetails/register_client.html
new file mode 100644
index 0000000000000000000000000000000000000000..96f1cfa5495aa9b2de97f6a013913616c41d5122
--- /dev/null
+++ b/src/main/resources/templates/clientdetails/register_client.html
@@ -0,0 +1,396 @@
+
+
+
+
+
+
+
+
+ 注册client - Spring Security&OAuth2.1
+
+
+
+
+
+Home
+
+注册client
+
+
+
+ 若对OAuth2.1的
client_details中的属性及作用不清楚,
+ 建议你先查看项目中的
db_table_description.html文件(位于others目录)中对表
oauth2_registered_client的说明,
+ 或在线访问
db_table_description.html;
+ 因为注册client实际上是向该表中按不同的条件添加数据.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/clientdetails/test_client.html b/src/main/resources/templates/clientdetails/test_client.html
new file mode 100644
index 0000000000000000000000000000000000000000..fe1d2d6e5d400e1430642008321ce10bd1b9d09c
--- /dev/null
+++ b/src/main/resources/templates/clientdetails/test_client.html
@@ -0,0 +1,666 @@
+
+
+
+
+
+
+
+
+ Test [[${clientDetailsDto.clientId}]] - Spring Security&OAuth2.1
+
+
+
+
+
+
+
Home
+
+
Test [[${clientDetailsDto.clientId}]]
+
+
+ 针对不同的grant_type提供不同的测试URL,
+ 完整的OAuth测试请访问spring-oauth-client项目.
+
+
+
+
+ 请先输入client_secret:
+
+
+
+
Test [authorization_code]
+
+
输入每一步必要的信息后点击其下面的按钮地址.
+
+ -
+
+
从 spring-oauth-server获取 'code'
+
+
+
+
+ -
+
用 'code' 换取 'access_token'
+
+ 输入第一步获取的'code'并点击按钮链接地址.
+
+
+
+
+
+
+
+
+
+
Test [authorization_code + PKCE]
+
+
输入每一步必要的信息后点击其下面的链接地址.
+
+ -
+
+
从 spring-oauth-server获取 'code'
+
+ PKCE流程在开始前需要先通过代码生成
code_verifier与
code_challenge (如何生成详见工具类
+
PKCEUtils.java
+ );
+
+ 生成后在获取'code'时要在已有的参数基础上再增加两个参数:
+
+
+ code_challenge |
+ 对 code_verifier 使用指定算法进行计算(digest)并base encode的值 |
+
+
+ code_challenge_method |
+ 固定值:S256 |
+
+
+
+
+
+
+
+
+ -
+
用 'code' 换取 'access_token'
+
+ 输入第一步获取的code并点击按钮地址.
+
+
+
+
+
+
+
+
+
Test [password] OAuth2.1不支持
+
+
输入username, password 后点击链接地址.
+ username:
+
+ password:
+
+
+
+
+
+
+
+
+
Test [device_code] OAuth2.1新增
+
+
+ -
+
设备上请求 /oauth2/device_authorization获取 user_code,
+ device_code,verification_uri等
+
+ 一般此步骤是在设备上通过代码来完成, 此处只作演示流程
+
+ -
+
在设备上展示user_code或显示一个二维码(内容为verification_uri_complete URL)
+ 用已经登录成功的浏览器(或另一个已经认证的设备)访问verification_uri_complete URL(可通过扫码等方式获取内容)
+
+ 此处方便演示, 请点击/oauth2/device_verification并输入上一步获取到的user_code
+ (若未认证将跳转到登录)
+
+ 提示:此步骤必须在有效时间内完成, user_code的有效时长在上一步中返回的数据expires_in来决定(单位:秒,
+ 默认5分钟)
+
+ -
+
+ 在第2步进行的同时,
+ 设备上后台将定时(如每隔5秒)向spring-oauth-server发起获取token的请求/oauth2/token
+ (需要使用第1步中获取到 device_code 的值),
+
+ 直到获取成功(即第2步操作完成授权设备登录)或超时(即设备轮询请求等待的时长超出第1步返回的时间expires_in)
+
+ 请输入device_code后点击按钮地址.
+
+ 提示:在第2步进行过程中调用第3步获取token API时会响应等待授权的结果(Http状态码 400,
+ error='authorization_pending')
+
+
+
+
+
+
+
+
Test [jwt-bearer] OAuth2.1新增
+
+
+ -
+
jwt-bearer是一类增强client端请求安全性的断言(assertion)实现;
+ 通过类似'双向SSL'的机制来让server端验证client端的签名实现强安全性.
+
+ -
+
当注册或添加client端时需要填写一个jwk URL地址(用来获取验签的公钥), 指定认证jwt签名算法(如RS256),
+ 设置methods为client_secret_jwt(对称算法,
+ 使用client_secret为MacKey)或private_key_jwt(非对称算法)
+ 注意: grant_type不能只有jwt-bearer, 无实用意义
+
+
+
+
+ cURL示例:
+
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"'
+ 增加两个请求参数:
+
+
+ | client_assertion_type |
+ 固定值: urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
+
+
+ | client_assertion |
+
+ 使用提供的 jwk URL中的 private_key进行签名生成的 JWT(如何生成详见: JwtBearerFlowTest.java)
+ |
+
+
+
+
+
输入client_assertion值, 点击按钮地址即可测试
+
+
+
+
+
+
+
Test [client_credentials]
+
+
+
+
+
Test [refresh_token]
+
+
输入refresh_token 后点击链接地址.
+
+
+
+
+
+
+
Test OIDC-Logout OAuth2.1新增
+
+
将'spring-oauth-server'退出并重定向会指定的uri(添加client端时的字段logout_redirect_uris), 由client端通过浏览器发起调用.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/consent.html b/src/main/resources/templates/consent.html
new file mode 100644
index 0000000000000000000000000000000000000000..af5644193cc1bc27d9134afe4135fbeb4e05ba6d
--- /dev/null
+++ b/src/main/resources/templates/consent.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+ 授权确认 - Spring Security&OAuth2.1
+
+
+
+
+
+
授权确认
+
+
+
+
+ The application
+
+ wants to access your account
+
+
+
+
+
+
+
+ You have provided the code
+ .
+ Verify that this code matches what is shown on your device.
+
+
+
+
+
+
+ The following permissions are requested by the above app.
+ Please review these and consent if you approve.
+
+
+
+
+
+
+
+
+ Your consent to provide access is required.
+ If you do not approve, click Cancel, in which case no information will be shared with the app.
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/consent_error.html b/src/main/resources/templates/consent_error.html
new file mode 100644
index 0000000000000000000000000000000000000000..ab8c914f804c0707a98124dfaf26900e22640113
--- /dev/null
+++ b/src/main/resources/templates/consent_error.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ Consent Error - Spring Security&OAuth2.1
+
+
+
+
+
+
+
Consent Error
+
Message:
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/device_verification.html b/src/main/resources/templates/device_verification.html
new file mode 100644
index 0000000000000000000000000000000000000000..bf37c38ddbab799b481f382dd975e7a0d051888d
--- /dev/null
+++ b/src/main/resources/templates/device_verification.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Device Login - Spring Security&OAuth2.1
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html
new file mode 100644
index 0000000000000000000000000000000000000000..ca12aee0acb1a108afcb82e69bfaa80be0edc1b2
--- /dev/null
+++ b/src/main/resources/templates/error/403.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ 403 - Spring Security&OAuth2.1
+
+
+
+
+
+
+
403 - Access Denied
+
Sorry, you do not have permission to access this resource.
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/fragments/main.html b/src/main/resources/templates/fragments/main.html
new file mode 100644
index 0000000000000000000000000000000000000000..fe03c8228bdd24c7a1624762a099afcd48852c28
--- /dev/null
+++ b/src/main/resources/templates/fragments/main.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Fragments
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..443b98ebc5234ee0b52f5809f3d84b03d8d4757e
--- /dev/null
+++ b/src/main/resources/templates/index.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+ Home - Spring Security&OAuth2.1
+
+
+
+
+Spring Security&OAuth2.1
+ 3.0.0
+
+
+
+ Logged:
+
+ Authorities:
+
+
+
+
+
+
操作说明
+
+ -
+
+ 菜单 User 是不需要OAuth 验证即可访问的(即公开的resource); 用于管理用户信息(添加,删除等).
+
+
+ -
+
+ 菜单 Unity 与 Mobile 需要登录认证后才能访问(即受保护的resource);
+ Unity 需要 [ROLE_UNITY] 权限, Mobile 需要 [ROLE_MOBILE] 权限.
+
+
+ -
+
+ device_login 用于在设备认证时,输入用户码(user_code)完成授权.
+
+
+ -
+
+ 在使用之前, 建议先了解OAuth2.1支持的grant_type, 请访问 https://andaily.com/blog/?p=103
+
+
+ -
+
+ 在项目的 others目录里有 oauth2.1-flow.md文件, 里面有测试的URL地址(包括浏览器与客户端的),
+ 若想访问 Unity 与 Mobile, 则先用基于浏览器的测试URL 访问,等验证通过后即可访问(注意不同的账号对应的权限).
+
+
+ -
+
+ 若需要自定义client_details数据并进行测试,
+ 可进入client_details去手动添加client_details或删除已创建的client_details.
+
+
+
+
+
+菜单
+
+ -
+
+ API - 查看提供的API文档
+
+
+ -
+
+ client_details - 管理ClientDetails
+
+
+ -
+
+ device_login - [device_code]流程中使用 OAuth2.1新增
+
+
+ -
+
+ User - 管理User
+
+
+ -
+
+ Unity - Unity 资源(resource), 需要具有 [ROLE_UNITY] 权限才能访问
+
+
+ -
+
+ Mobile - Mobile资源(resource), 需要具有 [ROLE_MOBILE] 权限才能访问
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/resources/templates/login.html
similarity index 53%
rename from src/main/webapp/WEB-INF/jsp/login.jsp
rename to src/main/resources/templates/login.html
index facbdc1443fb371d7142d4ec5ec9ca96dfb1c8dc..439f66f06a00197836a7cfc0c44bc3d6cdf37f04 100644
--- a/src/main/webapp/WEB-INF/jsp/login.jsp
+++ b/src/main/resources/templates/login.html
@@ -1,27 +1,25 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib tagdir="/WEB-INF/tags" prefix="tags" %>
-
-
+
+
- OAuth Login
+
+
+
+
+
+ Authenticate - Spring Security&OAuth2.1
+
+
-
-
+
+
-
+
+
.well-known endpoint
+
+
-
你可以使用以下几个初始的账号进行登录:
+
可以使用以下几个初始的账号进行登录:
@@ -71,18 +82,18 @@
| admin |
- admin |
+ Admin@2013 |
All privileges, allow visit [Mobile] and [Unity] resources, manage user |
| unity |
- unity |
+ Unity#2013 |
Only allow visit [Unity] resource, support grant_type:
- authorization_code,refresh_token,implicit |
+ authorization_code,refresh_token,device_code
| mobile |
- mobile |
+ Mobile*2013 |
Only allow visit [Mobile] resource, support grant_type: password,refresh_token |
@@ -90,6 +101,6 @@
-
+
\ No newline at end of file
diff --git a/src/main/resources/templates/mobile/dashboard.html b/src/main/resources/templates/mobile/dashboard.html
new file mode 100644
index 0000000000000000000000000000000000000000..ef9d710c16c13eb913f7d96cee755862aa032cf3
--- /dev/null
+++ b/src/main/resources/templates/mobile/dashboard.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ Mobile 资源 - Spring Security&OAuth2.1
+
+
+
+
+Home
+
+Hi Mobile
+ 你已成功访问 [mobile] 资源
+
+
+用户信息:
+
+
+
+
+
+ 访问API
+
+用户信息(JSON)
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/unity/dashboard.html b/src/main/resources/templates/unity/dashboard.html
new file mode 100644
index 0000000000000000000000000000000000000000..f7afd74bf8343103bd66cf3633aedb3615ff16d0
--- /dev/null
+++ b/src/main/resources/templates/unity/dashboard.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+ Unity 资源 - Spring Security&OAuth2.1
+
+
+
+
+Home
+
+Hi Unity
+ 你已成功访问 [unity] 资源
+
+
+用户信息:
+
+
+
+
+
+ 访问API
+
+用户信息(JSON)
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/user_form.jsp b/src/main/resources/templates/user_form.html
similarity index 37%
rename from src/main/webapp/WEB-INF/jsp/user_form.jsp
rename to src/main/resources/templates/user_form.html
index 203d69ad677b89c70330311bab028e261cbd6a75..979b1f01b7166d4c9e378a9440d1617ed3ad3fad 100644
--- a/src/main/webapp/WEB-INF/jsp/user_form.jsp
+++ b/src/main/resources/templates/user_form.html
@@ -1,29 +1,27 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fun" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
-
-
+
+
- Add User
+
+
+
+
+
+ Add User - Spring Security&OAuth2.1
+
+
-
-Home
+
+Home
Add User
-
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/user_overview.jsp b/src/main/resources/templates/user_overview.html
similarity index 32%
rename from src/main/webapp/WEB-INF/jsp/user_overview.jsp
rename to src/main/resources/templates/user_overview.html
index 47e2e1e30d081d35222e01271c37b8c1ee901b8e..bc3dec30966533058a83f013f95bb3f8d6da1b3c 100644
--- a/src/main/webapp/WEB-INF/jsp/user_overview.jsp
+++ b/src/main/resources/templates/user_overview.html
@@ -1,30 +1,30 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-
-
+
+
- User Overview
+
+
+
+
+
+ User Overview - Spring Security&OAuth2.1
+
+
-
-Home
+
+Home
User Overview
@@ -32,22 +32,28 @@
| Username |
Privilege |
+ Enabled |
Phone |
Email |
+ Nickname |
+ Address |
CreateTime |
-
-
- | ${user.username} |
- ${user.privileges} |
- ${user.phone} |
- ${user.email} |
- ${user.createTime} |
-
-
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/decorators.xml.old b/src/main/webapp/WEB-INF/decorators.xml.old
deleted file mode 100644
index 27d7f9cbb82d7b7c17428e972c2e53cd1b7d235b..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/decorators.xml.old
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
- /*
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/clientdetails/client_details.jsp b/src/main/webapp/WEB-INF/jsp/clientdetails/client_details.jsp
deleted file mode 100644
index 62f24cb00260e13ee142a8ef04835f9f3f0d514c..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/clientdetails/client_details.jsp
+++ /dev/null
@@ -1,85 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fun" uri="http://java.sun.com/jsp/jstl/functions" %>
-
-
-
- client_details
-
-
-
-
-Home
-
-
-
-
-
-
-
-
- -
-
-
- ${cli.clientId}
- ${cli.authorizedGrantTypes}
-
-
-
- client_id: ${cli.clientId}
- client_secret: ***
-
- authorized_grant_types: ${cli.authorizedGrantTypes}
- resource_ids: ${cli.resourceIds}
-
- scope: ${cli.scope}
- web_server_redirect_uri: ${cli.webServerRedirectUri}
-
- authorities: ${cli.authorities}
- access_token_validity: ${cli.accessTokenValidity}
- refresh_token_validity: ${cli.refreshTokenValidity}
-
- create_time: ${cli.createTime}
- archived: ${cli.archived}
- trusted: ${cli.trusted}
- additional_information: ${cli.additionalInformation}
-
-
-
-
-
-
- 每一个item对应oauth_client_details表中的一条数据; 共${fun:length(clientDetailsDtoList)}条数据.
-
- 对spring-oauth-server数据库表的详细说明请访问
- http://andaily.com/spring-oauth-server/db_table_description.html
- (或访问项目others目录的db_table_description.html文件)
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/clientdetails/register_client.jsp b/src/main/webapp/WEB-INF/jsp/clientdetails/register_client.jsp
deleted file mode 100644
index b8033a710b63a07db99138b76146609e92d89682..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/clientdetails/register_client.jsp
+++ /dev/null
@@ -1,243 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fun" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
-
-
-
- 注册client
-
-
-
-
-Home
-
-注册client
-
-
-
- 若对Oauth的client_details中的属性及作用不清楚,
- 建议你先查看项目中的db_table_description.html文件(位于others目录)中对表oauth_client_details的说明,
- 或在线访问db_table_description.html;
- 因为注册client实际上是向该表中按不同的条件添加数据.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/clientdetails/test_client.jsp b/src/main/webapp/WEB-INF/jsp/clientdetails/test_client.jsp
deleted file mode 100644
index 5b9abf425022a94fd7a5958a800603b58152abc3..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/clientdetails/test_client.jsp
+++ /dev/null
@@ -1,187 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
-
-
- Test [${clientDetailsDto.clientId}]
-
-
-
-
-
-
-
Home
-
-
Test [${clientDetailsDto.clientId}]
-
-
- 针对不同的grant_type提供不同的测试URL,
- 完整的OAuth测试请访问spring-oauth-client项目.
-
-
-
-
- 请先输入client_secret:
-
-
-
-
-
Test [authorization_code]
-
-
-
-
-
-
-
Test [password]
-
-
输入username, password 后点击链接地址.
- username:
-
- password:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Test [client_credentials]
-
-
-
-
-
-
-
Test [refresh_token]
-
-
输入refresh_token 后点击链接地址.
- refresh_token:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/decorators/main.jsp b/src/main/webapp/WEB-INF/jsp/decorators/main.jsp
deleted file mode 100644
index d60295777e1f03a407ea97056f2aae1cf700c7f9..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/decorators/main.jsp
+++ /dev/null
@@ -1,42 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" trimDirectiveWhitespaces="true" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib tagdir="/WEB-INF/tags" prefix="tags" %>
-
-
-
-
-
-
-
-
-
-
-
- - Spring Security&OAuth2
-
-
- <%----%>
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp
deleted file mode 100644
index c6446e62385096792f9c56b12a96f441ca2f8e2b..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/index.jsp
+++ /dev/null
@@ -1,120 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
-<%@ taglib tagdir="/WEB-INF/tags" prefix="tags" %>
-
-
-
- Home
-
-
-
-
-Spring Security&OAuth2
- ${mainVersion}
-
-
-
- Logged: ${SPRING_SECURITY_CONTEXT.authentication.principal.username}
-
-
-
-
- 操作说明:
-
- -
-
- 菜单 User 是不需要OAuth 验证即可访问的(即公开的resource); 用于管理用户信息(添加,删除等).
-
-
- -
-
- 菜单 Unity 与 Mobile 需要OAuth 验证后才能访问(即受保护的resource);
- Unity 需要 [ROLE_UNITY] 权限(resourceId:
- unity-resource
- ), Mobile 需要 [ROLE_MOBILE] 权限(resourceId:
- mobile-resource
- ).
-
-
- -
-
- 在使用之前, 建议先了解OAuth2支持的5类grant_type, 请访问 http://andaily.com/blog/?p=103
-
-
- -
-
- 在项目的 others目录里有 oauth_test.txt文件, 里面有测试的URL地址(包括浏览器与客户端的),
- 若想访问 Unity 与 Mobile, 则先用基于浏览器的测试URL 访问,等验证通过后即可访问(注意不同的账号对应的权限).
-
-
- -
-
- 若需要自定义client_details数据并进行测试, 可进入client_details去手动添加client_details或删除已创建的client_details.
-
-
-
-
-
-菜单
-
- -
-
- API - 查看提供的API文档
-
-
- -
-
- client_details - 管理ClientDetails
-
-
-
- -
-
- User - 管理User
-
-
-
- -
-
- Unity - Unity 资源(resource), 需要具有 [ROLE_UNITY] 权限(resourceId:
- unity-resource才能访问
-
-
- -
-
- Mobile - Mobile资源(resource), 需要具有 [ROLE_MOBILE] 权限(resourceId:
- mobile-resource才能访问
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/mobile/dashboard.jsp b/src/main/webapp/WEB-INF/jsp/mobile/dashboard.jsp
deleted file mode 100644
index 24f22897ca72e11a8196673ea4e36ca568e15ef4..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/mobile/dashboard.jsp
+++ /dev/null
@@ -1,30 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-
-
-
- Mobile 资源
-
-
-Home
-
-Hi Mobile
- 你已成功访问 [mobile] 资源
-
-
-用户信息:
-
-${SPRING_SECURITY_CONTEXT.authentication.principal}
-
-
-
- 访问API
-
-用户信息(JSON)
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/oauth_approval.jsp b/src/main/webapp/WEB-INF/jsp/oauth_approval.jsp
deleted file mode 100644
index 311d4d7125d0241a12856021e374069661dfccf8..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/oauth_approval.jsp
+++ /dev/null
@@ -1,27 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-
-
-
-
- Oauth Approval
-
-OAuth Approval
-
-Do you authorize '${authorizationRequest.clientId}' to access your protected resources?
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/oauth_error.jsp b/src/main/webapp/WEB-INF/jsp/oauth_error.jsp
deleted file mode 100644
index bb1838a72fbe5f6db6aeed70eda5ba3996ea25a0..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/oauth_error.jsp
+++ /dev/null
@@ -1,25 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-
-
-
- Oauth Error
-
-
-Home
-
-
- Illegal action.
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/unity/dashboard.jsp b/src/main/webapp/WEB-INF/jsp/unity/dashboard.jsp
deleted file mode 100644
index b38e5b77a9d3099ed2a9fcbe066d6e39545100af..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/jsp/unity/dashboard.jsp
+++ /dev/null
@@ -1,31 +0,0 @@
-<%--
- *
- * @author Shengzhao Li
---%>
-
-<%@ page contentType="text/html;charset=UTF-8" language="java" %>
-<%@ taglib tagdir="/WEB-INF/tags" prefix="tags" %>
-
-
-
- Unity 资源
-
-
-Home
-
-Hi Unity
- 你已成功访问 [unity] 资源
-
-
-用户信息:
-
-${SPRING_SECURITY_CONTEXT.authentication.principal}
-
-
-
- 访问API
-
-用户信息(JSON)
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/log4j.xml.old b/src/main/webapp/WEB-INF/log4j.xml.old
deleted file mode 100644
index 2125676ec957f4f76f0e32c6ada63f73807b7e69..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/log4j.xml.old
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/mkk-servlet.xml.old b/src/main/webapp/WEB-INF/mkk-servlet.xml.old
deleted file mode 100644
index 624729401ec26823c8b48284262f6c85acbfa5c7..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/mkk-servlet.xml.old
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tags/csrf.tag b/src/main/webapp/WEB-INF/tags/csrf.tag
deleted file mode 100644
index f8f4bb99135ca568b99eca77aff1a442cfc21965..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/tags/csrf.tag
+++ /dev/null
@@ -1,3 +0,0 @@
-<%@tag pageEncoding="UTF-8" %>
-
-
diff --git a/src/main/webapp/WEB-INF/web.xml.old b/src/main/webapp/WEB-INF/web.xml.old
deleted file mode 100644
index c70e61b0e9af5560b0d2e0a5fd0cd9722c1c1065..0000000000000000000000000000000000000000
--- a/src/main/webapp/WEB-INF/web.xml.old
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
- spring-oauth-server
-
-
-
- webAppRootKey
- spring-oauth-server
-
-
-
-
- encodingFilter
- com.monkeyk.sos.web.filter.CharacterEncodingIPFilter
-
- encoding
- UTF-8
-
-
- forceEncoding
- true
-
-
-
- encodingFilter
- /*
-
-
-
-
- springSecurityFilterChain
- org.springframework.web.filter.DelegatingFilterProxy
-
-
-
- springSecurityFilterChain
- /*
-
-
-
-
- sitemesh
- com.opensymphony.sitemesh.webapp.SiteMeshFilter
-
-
- sitemesh
- /*
-
-
-
- ico
- image/vnd.microsoft.icon
-
-
-
-
- contextConfigLocation
- classpath:spring/*.xml
-
-
- log4jConfigLocation
- /WEB-INF/log4j.xml
-
-
- org.springframework.web.util.Log4jConfigListener
-
-
-
-
- org.springframework.web.context.ContextLoaderListener
-
-
-
-
- mkk
- org.springframework.web.servlet.DispatcherServlet
- 2
-
-
- mkk
- /
-
-
-
-
-
-
-
-
- 30
-
-
-
-
- index.jsp
-
-
-
-
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/ContextTest.java b/src/test/java/com/monkeyk/sos/ContextTest.java
index df06b2eae20856251784702e6be617238d9cbb2f..bcb56e39ced3ecb2e881b08de76ba290be15cd17 100644
--- a/src/test/java/com/monkeyk/sos/ContextTest.java
+++ b/src/test/java/com/monkeyk/sos/ContextTest.java
@@ -1,16 +1,14 @@
package com.monkeyk.sos;
-import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
-import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.transaction.BeforeTransaction;
/**
* @author Shengzhao Li
*/
-@RunWith(SpringRunner.class)
+
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
public abstract class ContextTest extends AbstractTransactionalJUnit4SpringContextTests {
diff --git a/src/test/java/com/monkeyk/sos/SpringOauthServerApplicationTests.java b/src/test/java/com/monkeyk/sos/SpringOauthServerApplicationTests.java
index a14726be16eb3c029a38a9936521c1c52d6cf08f..2375cc8b6fd3974b85293bb067d2bc5343cc211d 100644
--- a/src/test/java/com/monkeyk/sos/SpringOauthServerApplicationTests.java
+++ b/src/test/java/com/monkeyk/sos/SpringOauthServerApplicationTests.java
@@ -1,18 +1,20 @@
package com.monkeyk.sos;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+
+import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
-import org.springframework.test.context.junit4.SpringRunner;
-@RunWith(SpringRunner.class)
+
+/**
+ * @since 2.0.0
+ */
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
public class SpringOauthServerApplicationTests {
- @Test
- public void contextLoads() {
- }
+ @Test
+ public void contextLoads() {
+ }
}
diff --git a/src/test/java/com/monkeyk/sos/config/JWTTokenStoreConfigurationTest.java b/src/test/java/com/monkeyk/sos/config/JWTTokenStoreConfigurationTest.java
deleted file mode 100644
index d57a8eb7f1464bfc7b943fe61c5d75c249274b81..0000000000000000000000000000000000000000
--- a/src/test/java/com/monkeyk/sos/config/JWTTokenStoreConfigurationTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.monkeyk.sos.config;
-
-import org.junit.Test;
-import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
-import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
-
-import java.util.Map;
-
-import static org.junit.Assert.*;
-
-/**
- * 2020/6/9
- *
- * @author Shengzhao Li
- * @since 2.1.0
- */
-public class JWTTokenStoreConfigurationTest {
-
-
- @Test
- public void keyTest() throws Exception {
-
- RandomValueStringGenerator randomValueStringGenerator = new RandomValueStringGenerator(32);
- String verifierKey = randomValueStringGenerator.generate();
- assertNotNull(verifierKey);
-// System.out.println(verifierKey);
-
- }
-
-
- @Test
- public void testJwtAccessTokenConverter() throws Exception {
-
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- jwtAccessTokenConverter.setSigningKey("IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa");
- jwtAccessTokenConverter.afterPropertiesSet();
-
- assertFalse(jwtAccessTokenConverter.isPublic());
- Map key = jwtAccessTokenConverter.getKey();
- assertNotNull(key);
-
- }
-
-}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/config/OAuth2ServerConfigurationTest.java b/src/test/java/com/monkeyk/sos/config/OAuth2ServerConfigurationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a8a9b2558f4612a855440b339e147d8fd2e463f
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/config/OAuth2ServerConfigurationTest.java
@@ -0,0 +1,31 @@
+package com.monkeyk.sos.config;
+
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
+import com.nimbusds.jose.proc.SecurityContext;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+import static com.monkeyk.sos.config.OAuth2ServerConfiguration.KEYSTORE_NAME;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 2023/10/12 17:58
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+class OAuth2ServerConfigurationTest {
+
+
+ @Test
+ void jwkSource() throws Exception {
+
+ Resource resource = new ClassPathResource(KEYSTORE_NAME);
+ JWKSource jwkSource = JWKSourceBuilder.create(resource.getURL()).build();
+ assertNotNull(jwkSource);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/domain/shared/GuidGeneratorTest.java b/src/test/java/com/monkeyk/sos/domain/shared/GuidGeneratorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4896329a88376a4415c820ea71e5338693b35f49
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/domain/shared/GuidGeneratorTest.java
@@ -0,0 +1,22 @@
+package com.monkeyk.sos.domain.shared;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 2023/10/13 10:28
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+class GuidGeneratorTest {
+
+ @Test
+ void generate() {
+
+ String generate = GuidGenerator.generate();
+ assertNotNull(generate);
+// System.out.println(generate);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/DateUtilsTest.java b/src/test/java/com/monkeyk/sos/infrastructure/DateUtilsTest.java
index f86237cf06b8ff04e3132d2a05c5d70c84d4ba67..929d6f81054528642fc4a8c9891e24c675667798 100644
--- a/src/test/java/com/monkeyk/sos/infrastructure/DateUtilsTest.java
+++ b/src/test/java/com/monkeyk/sos/infrastructure/DateUtilsTest.java
@@ -12,13 +12,14 @@
package com.monkeyk.sos.infrastructure;
-import org.junit.Test;
+
+import org.junit.jupiter.api.Test;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
/*
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/PKCEUtilsTest.java b/src/test/java/com/monkeyk/sos/infrastructure/PKCEUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..25a0cac755563b018dbdd5bdc5fb4d66b68ed095
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/infrastructure/PKCEUtilsTest.java
@@ -0,0 +1,60 @@
+package com.monkeyk.sos.infrastructure;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 2023/10/16 22:53
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+class PKCEUtilsTest {
+
+
+ @Test
+ void generateCodeVerifier() {
+
+ String verifier = PKCEUtils.generateCodeVerifier();
+ assertNotNull(verifier);
+ assertTrue(verifier.length() >= 32);
+ }
+
+
+ @Test
+ void generateCodeChallenge() {
+
+ String verifier = PKCEUtils.generateCodeVerifier();
+ assertNotNull(verifier);
+
+ String challenge = PKCEUtils.generateCodeChallenge(verifier);
+ assertNotNull(challenge);
+
+ }
+
+
+ /**
+ * PKCE 需要的参数生成测试
+ * code_challenge_method : S256 (alg: SHA-256) 固定值
+ * code_verifier : 随机生成且base64 encode的值 (推荐随机值至少32位)
+ * code_challenge : 对 code_verifier 使用指定算法进行计算(digest)并base encode的值
+ *
+ */
+ @Test
+ void pkceFlow() {
+
+ // 1. 随机生成code_verifier
+ String codeVerifier = PKCEUtils.generateCodeVerifier();
+// System.out.println("code_verifier -> " + codeVerifier);
+
+ //2. 按指定算法计算 挑战码 code_challenge
+ String codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier);
+
+ assertNotNull(codeChallenge);
+// System.out.println("code_challenge -> " + codeChallenge);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/PasswordHandlerTest.java b/src/test/java/com/monkeyk/sos/infrastructure/PasswordHandlerTest.java
index b3cae00d682d6a5b9a2ab9ec8202c1de78d87b5a..f282afc3c25c8aeb2cc33ed09e0202aaac52f3fe 100644
--- a/src/test/java/com/monkeyk/sos/infrastructure/PasswordHandlerTest.java
+++ b/src/test/java/com/monkeyk/sos/infrastructure/PasswordHandlerTest.java
@@ -1,21 +1,47 @@
package com.monkeyk.sos.infrastructure;
-import org.junit.Test;
+import com.monkeyk.sos.ContextTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.password.PasswordEncoder;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/*
- * @author Shengzhao Li
- */
-public class PasswordHandlerTest {
+ * @author Shengzhao Li
+ */
+public class PasswordHandlerTest extends ContextTest {
+ @Autowired
+ private PasswordEncoder passwordEncoder;
+
+
+// @Test
+// public void testMd5() throws Exception {
+//
+// final String md5 = PasswordHandler.encode("123456");
+// assertNotNull(md5);
+//// System.out.println(md5);
+// }
+
@Test
- public void testMd5() throws Exception {
+ void encode() throws Exception {
+
+ String pwd = "Admin@2013";
+ String encode = PasswordHandler.encode(pwd);
+ assertNotNull(encode);
+// System.out.println(encode);
- final String md5 = PasswordHandler.encode("123456");
- assertNotNull(md5);
- System.out.println(md5);
}
+
+ @Test
+ void matches() {
+ String pwd = "Admin@2013";
+ boolean matches = passwordEncoder.matches(pwd, "$2a$10$bIIt6KqIMweTZZC.IIHBLuN3dEIJL0LQFRPrtWTujn9O3Sl5Us5vW");
+ assertTrue(matches);
+ }
+
}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/SettingsUtilsTest.java b/src/test/java/com/monkeyk/sos/infrastructure/SettingsUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..343b87ff5e748477925406166d664ac85e7cc8ea
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/infrastructure/SettingsUtilsTest.java
@@ -0,0 +1,49 @@
+package com.monkeyk.sos.infrastructure;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 2023/10/13 14:59
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+class SettingsUtilsTest {
+
+
+ @Test
+ void textTokenSettings() {
+
+ TokenSettings settings = TokenSettings.builder()
+ .reuseRefreshTokens(false)
+ .build();
+ String s = SettingsUtils.textTokenSettings(settings);
+ assertNotNull(s);
+// System.out.println(s);
+
+ TokenSettings tokenSettings = SettingsUtils.buildTokenSettings(s);
+ assertNotNull(tokenSettings);
+
+ }
+
+ @Test
+ void textClientSettings() {
+
+ ClientSettings settings = ClientSettings.builder()
+ .requireProofKey(true)
+ .build();
+ String s = SettingsUtils.textClientSettings(settings);
+ assertNotNull(s);
+
+// System.out.println(s);
+
+ ClientSettings clientSettings = SettingsUtils.buildClientSettings(s);
+ assertNotNull(clientSettings);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbcTest.java b/src/test/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbcTest.java
index dcdd73101660a636af0dccd4760d883cc51ce1a4..c4690ecf4644ae7c52d6a0c660b2b31ad419adaa 100644
--- a/src/test/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbcTest.java
+++ b/src/test/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbcTest.java
@@ -15,18 +15,22 @@ import com.monkeyk.sos.domain.oauth.OauthClientDetails;
import com.monkeyk.sos.domain.oauth.OauthRepository;
import com.monkeyk.sos.domain.shared.GuidGenerator;
import com.monkeyk.sos.infrastructure.AbstractRepositoryTest;
-import org.junit.Test;
+
+import com.monkeyk.sos.infrastructure.SettingsUtils;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.util.List;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
/*
- * @author Shengzhao Li
- */
+ * @author Shengzhao Li
+ */
public class OauthRepositoryJdbcTest extends AbstractRepositoryTest {
@@ -47,7 +51,15 @@ public class OauthRepositoryJdbcTest extends AbstractRepositoryTest {
final String clientId = GuidGenerator.generate();
- OauthClientDetails clientDetails = new OauthClientDetails().clientId(clientId);
+ OauthClientDetails clientDetails = new OauthClientDetails()
+ .id(GuidGenerator.generate())
+ .clientName("Test-client")
+ .clientAuthenticationMethods("client_secret_post")
+ .authorizationGrantTypes("authorization_code")
+ .scopes("openid")
+ .clientSettings(SettingsUtils.textClientSettings(ClientSettings.builder().build()))
+ .tokenSettings(SettingsUtils.textTokenSettings(TokenSettings.builder().build()))
+ .clientId(clientId);
oauthRepositoryMyBatis.saveOauthClientDetails(clientDetails);
final OauthClientDetails oauthClientDetails = oauthRepositoryMyBatis.findOauthClientDetails(clientId);
diff --git a/src/test/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbcTest.java b/src/test/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbcTest.java
index 5160f932e8e751ab14d697428a2e53ea4719ba45..8cbb34b38b55b27b9a2e681d22b92f9b0fe55bb0 100644
--- a/src/test/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbcTest.java
+++ b/src/test/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbcTest.java
@@ -14,18 +14,19 @@ package com.monkeyk.sos.infrastructure.jdbc;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.domain.user.UserRepository;
import com.monkeyk.sos.infrastructure.AbstractRepositoryTest;
-import org.junit.Test;
+
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
/*
- * @author Shengzhao Li
- */
+ * @author Shengzhao Li
+ */
public class UserRepositoryJdbcTest extends AbstractRepositoryTest {
@@ -33,6 +34,28 @@ public class UserRepositoryJdbcTest extends AbstractRepositoryTest {
private UserRepository userRepository;
+ /**
+ * @since 3.0.0
+ */
+ @Test
+ void findProfileByUsername() {
+
+ String username = "userxxxx";
+ User user = userRepository.findProfileByUsername(username);
+ assertNull(user);
+
+ User user2 = new User(username, "{123}", "123", "ewo@honyee.cc");
+ user2.address("address").nickname("nick-name");
+ userRepository.saveUser(user2);
+
+ User user3 = userRepository.findProfileByUsername(username);
+ assertNotNull(user3);
+ assertNotNull(user3.phone());
+ assertNotNull(user3.email());
+
+ }
+
+
@Test
public void findByGuid() {
User user = userRepository.findByGuid("oood");
@@ -95,8 +118,8 @@ public class UserRepositoryJdbcTest extends AbstractRepositoryTest {
/*
- * Run the test must initial db firstly
- * */
+ * Run the test must initial db firstly
+ * */
// @Test()
public void testPrivilege() {
diff --git a/src/test/java/com/monkeyk/sos/service/JwksTest.java b/src/test/java/com/monkeyk/sos/service/JwksTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..97cc7bd08c726fc9a3fbdac1502110a0847375c1
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/service/JwksTest.java
@@ -0,0 +1,109 @@
+package com.monkeyk.sos.service;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.RSAKey;
+import org.junit.jupiter.api.Test;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Set;
+
+import static com.nimbusds.jose.jwk.KeyOperation.*;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * 2023/10/18 15:12
+ *
+ * JWK
+ * generate
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+public class JwksTest {
+
+
+ /**
+ * ES256 jwk generate
+ *
+ * @throws Exception e
+ */
+ @Test
+ void jwkEC() throws Exception {
+
+ Curve point = Curve.P_256;
+// Curve point = Curve.P_384;
+// Curve point = Curve.P_521;
+
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+ keyPairGenerator.initialize(point.toECParameterSpec());
+
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ PublicKey aPublic = keyPair.getPublic();
+ PrivateKey aPrivate = keyPair.getPrivate();
+
+
+ ECKey key = new ECKey.Builder(point, (ECPublicKey) aPublic)
+ .privateKey(aPrivate)
+ .keyOperations(Set.of(
+ SIGN,
+ VERIFY,
+ ENCRYPT,
+ DECRYPT,
+ DERIVE_KEY))
+ // keyId 必须唯一
+ .keyID("sos-ecc-kid1")
+ .algorithm(JWSAlgorithm.ES256)
+ .build();
+ assertNotNull(key);
+
+ String json = key.toJSONString();
+ assertNotNull(json);
+// System.out.println(json);
+
+
+ }
+
+ /**
+ * RS256 jwk generate
+ *
+ * @throws Exception e
+ */
+ @Test
+ void jwkRS() throws Exception {
+
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ PrivateKey aPrivate = keyPair.getPrivate();
+ PublicKey aPublic = keyPair.getPublic();
+
+
+ RSAKey key = new RSAKey.Builder((RSAPublicKey) aPublic)
+ .privateKey(aPrivate)
+// .keyUse(KeyUse.SIGNATURE)
+ .keyOperations(Set.of(
+ SIGN,
+ VERIFY,
+ ENCRYPT,
+ DECRYPT,
+ DERIVE_KEY))
+ .algorithm(JWSAlgorithm.RS256)
+ .keyID("sos-rsa-kid2")
+ .build();
+
+ assertNotNull(key);
+ String json = key.toJSONString();
+ assertNotNull(json);
+// System.out.println(json);
+ }
+
+}
diff --git a/src/test/java/com/monkeyk/sos/service/JwtBearerFlowTest.java b/src/test/java/com/monkeyk/sos/service/JwtBearerFlowTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7fc9adb5f6e0126ddf1ed985ac028dceee084c9e
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/service/JwtBearerFlowTest.java
@@ -0,0 +1,139 @@
+package com.monkeyk.sos.service;
+
+import com.nimbusds.jose.*;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jwt.JWTClaimsSet;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.util.Date;
+
+import static com.monkeyk.sos.web.controller.JwtBearerJwksController.ES256_KEY;
+import static com.monkeyk.sos.web.controller.JwtBearerJwksController.RS256_KEY;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * 2023/10/24 10:25
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+public class JwtBearerFlowTest {
+
+
+ /**
+ * MAC 生成 assertion
+ * HS256
+ * method: CLIENT_SECRET_JWT
+ *
+ * @throws Exception e
+ */
+ @Test
+ void macAssertion() throws Exception {
+
+ String clientId = "vLIXDF9GXg6Psfh1uzwVFUj0fucX2Zn9";
+ // client_secret 加密后的值
+ String macSecret = "$2a$10$kjjdfA8SIuhlVx0q4B1GYeU..9TNU9.Aj6Vdc2v/iQTJhhmT/0xCi";
+
+ JWSSigner jwsSigner = new MACSigner(macSecret);
+
+ JWSHeader header = new JWSHeader(JWSAlgorithm.HS256);
+
+
+ JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience("http://127.0.0.1:8080")
+ .expirationTime(Date.from(Instant.now().plusSeconds(300L)))
+ .build();
+
+ Payload payload = new Payload(claimsSet.toJSONObject());
+
+ JWSObject jwsObject = new JWSObject(header, payload);
+ //签名
+ jwsObject.sign(jwsSigner);
+
+ // 将 assertion 复制放到请求参数 client_assertion 的值
+ String assertion = jwsObject.serialize();
+ assertNotNull(assertion);
+// System.out.println(assertion);
+
+ }
+
+
+ /**
+ * RSA 生成 assertion
+ * SignatureAlgorithm: RS256
+ * method: PRIVATE_KEY_JWT
+ *
+ * @throws Exception e
+ */
+ @Test
+ void rs256Assertion() throws Exception {
+
+ JWK rsJwk = JWK.parse(RS256_KEY);
+
+ JWSSigner jwsSigner = new RSASSASigner(rsJwk.toRSAKey());
+ JWSHeader header = new JWSHeader(JWSAlgorithm.RS256);
+
+ String clientId = "dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn";
+ JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience("http://127.0.0.1:8080")
+ .expirationTime(Date.from(Instant.now().plusSeconds(300L)))
+ .build();
+
+ Payload payload = new Payload(claimsSet.toJSONObject());
+
+ JWSObject jwsObject = new JWSObject(header, payload);
+ //签名
+ jwsObject.sign(jwsSigner);
+
+ // 将 assertion 复制放到请求参数 client_assertion 的值
+ String assertion = jwsObject.serialize();
+ assertNotNull(assertion);
+// System.out.println(assertion);
+
+ }
+
+ /**
+ * ES 生成 assertion
+ * SignatureAlgorithm: ES256
+ * method: PRIVATE_KEY_JWT
+ *
+ * @throws Exception e
+ */
+ @Test
+ void es256Assertion() throws Exception {
+
+ JWK rsJwk = JWK.parse(ES256_KEY);
+
+ JWSSigner jwsSigner = new ECDSASigner(rsJwk.toECKey());
+ JWSHeader header = new JWSHeader(JWSAlgorithm.ES256);
+
+ String clientId = "pRC9j1mwGNMuchoI8nwJ6blr1lmPBLha";
+ JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience("http://127.0.0.1:8080")
+ .expirationTime(Date.from(Instant.now().plusSeconds(300L)))
+ .build();
+
+ Payload payload = new Payload(claimsSet.toJSONObject());
+
+ JWSObject jwsObject = new JWSObject(header, payload);
+ //签名
+ jwsObject.sign(jwsSigner);
+
+ // 将 assertion 复制放到请求参数 client_assertion 的值
+ String assertion = jwsObject.serialize();
+ assertNotNull(assertion);
+// System.out.println(assertion);
+
+ }
+
+}
diff --git a/src/test/java/com/monkeyk/sos/service/business/AbstractInlineAccessTokenInvokerTest.java b/src/test/java/com/monkeyk/sos/service/business/AbstractInlineAccessTokenInvokerTest.java
index 1c06632356c1fe083a424072ebe75b4232d93d88..b9404493f17c05dcdd13b62366c0609916e1a164 100644
--- a/src/test/java/com/monkeyk/sos/service/business/AbstractInlineAccessTokenInvokerTest.java
+++ b/src/test/java/com/monkeyk/sos/service/business/AbstractInlineAccessTokenInvokerTest.java
@@ -2,16 +2,15 @@ package com.monkeyk.sos.service.business;
import com.monkeyk.sos.domain.oauth.OauthClientDetails;
import com.monkeyk.sos.domain.oauth.OauthRepository;
+import com.monkeyk.sos.domain.shared.GuidGenerator;
import com.monkeyk.sos.domain.user.Privilege;
import com.monkeyk.sos.domain.user.User;
import com.monkeyk.sos.domain.user.UserRepository;
import com.monkeyk.sos.infrastructure.AbstractRepositoryTest;
import com.monkeyk.sos.infrastructure.PasswordHandler;
-import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
-import static com.monkeyk.sos.config.OAuth2ServerConfiguration.RESOURCE_ID;
-
/**
* 2019/7/6
*
@@ -52,11 +51,16 @@ public abstract class AbstractInlineAccessTokenInvokerTest extends AbstractRepos
OauthClientDetails createClientDetails() {
OauthClientDetails clientDetails = new OauthClientDetails();
clientDetails.clientId(clientId)
+ .clientName("TestClient")
+ .id(GuidGenerator.generateNumber())
.clientSecret(PasswordHandler.encode(clientSecret))
- .authorizedGrantTypes(grantTypes())
- .scope("read")
- .accessTokenValidity(200)
- .resourceIds(RESOURCE_ID);
+ .authorizationGrantTypes(grantTypes())
+ .clientAuthenticationMethods("client_secret_post")
+ .clientSettings("")
+ .tokenSettings("")
+ .scopes("openid");
+// .accessTokenValidity(200)
+// .resourceIds(RESOURCE_ID);
oauthRepository.saveOauthClientDetails(clientDetails);
@@ -64,7 +68,7 @@ public abstract class AbstractInlineAccessTokenInvokerTest extends AbstractRepos
}
String grantTypes() {
- return "authorization_code,password,implicit,client_credentials,refresh_token";
+ return "authorization_code,password,client_credentials,refresh_token";
}
}
diff --git a/src/test/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvokerTest.java b/src/test/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvokerTest.java
index 7d1e13d8a190b4379c0bed287c1b255a1efeb4ea..70697a7fe03895144ae5630e123d03bc1fbc8e6b 100644
--- a/src/test/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvokerTest.java
+++ b/src/test/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvokerTest.java
@@ -1,13 +1,16 @@
package com.monkeyk.sos.service.business;
import com.monkeyk.sos.service.dto.AccessTokenDto;
-import org.junit.Test;
-import org.springframework.security.oauth2.provider.NoSuchClientException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
import java.util.HashMap;
import java.util.Map;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
/**
* 2019/7/6
@@ -18,6 +21,7 @@ public class ClientCredentialsInlineAccessTokenInvokerTest extends AbstractInlin
@Test
+ @Disabled
public void invokeNormal() {
createClientDetails();
@@ -39,7 +43,8 @@ public class ClientCredentialsInlineAccessTokenInvokerTest extends AbstractInlin
}
- @Test(expected = NoSuchClientException.class)
+ // @Test(expected = NoSuchClientException.class)
+ @Test
public void invalidClientId() {
createClientDetails();
@@ -52,16 +57,21 @@ public class ClientCredentialsInlineAccessTokenInvokerTest extends AbstractInlin
ClientCredentialsInlineAccessTokenInvoker accessTokenInvoker = new ClientCredentialsInlineAccessTokenInvoker();
- final AccessTokenDto accessTokenDto = accessTokenInvoker.invoke(params);
+// AccessTokenDto accessTokenDto;
+ assertThrows(Exception.class, () -> {
+ accessTokenInvoker.invoke(params);
+ });
+// final AccessTokenDto accessTokenDto = accessTokenInvoker.invoke(params);
- assertNotNull(accessTokenDto);
- assertNotNull(accessTokenDto.getAccessToken());
+// assertNotNull(accessTokenDto);
+// assertNotNull(accessTokenDto.getAccessToken());
// System.out.println(accessTokenDto);
}
@Test()
+ @Disabled
public void invalidClientSecret() {
createClientDetails();
diff --git a/src/test/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvokerTest.java b/src/test/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvokerTest.java
index c49944e8ecd50453d473532802881ecff1e71bdb..fe363554fdbe9d537699a71e72d860992b2e0c47 100644
--- a/src/test/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvokerTest.java
+++ b/src/test/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvokerTest.java
@@ -1,14 +1,15 @@
package com.monkeyk.sos.service.business;
import com.monkeyk.sos.service.dto.AccessTokenDto;
-import org.junit.Test;
-import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
import java.util.HashMap;
import java.util.Map;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.*;
+
/**
* 2019/7/6
@@ -19,6 +20,7 @@ public class PasswordInlineAccessTokenInvokerTest extends AbstractInlineAccessTo
@Test
+ @Disabled
public void invokeNormal() {
createClientDetails();
@@ -46,7 +48,7 @@ public class PasswordInlineAccessTokenInvokerTest extends AbstractInlineAccessTo
}
- @Test(expected = InvalidGrantException.class)
+ @Test()
public void invalidUsername() {
createClientDetails();
@@ -61,16 +63,19 @@ public class PasswordInlineAccessTokenInvokerTest extends AbstractInlineAccessTo
params.put("password", "password");
PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker();
- final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params);
+ assertThrows(Exception.class, () -> {
+ accessTokenInvoker.invoke(params);
+ });
+// final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params);
- assertNull(tokenDto);
+// assertNull(tokenDto);
// System.out.println(accessTokenDto);
}
- @Test(expected = IllegalStateException.class)
+ @Test()
public void invalidScope() {
createClientDetails();
@@ -86,9 +91,12 @@ public class PasswordInlineAccessTokenInvokerTest extends AbstractInlineAccessTo
params.put("password", password);
PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker();
- final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params);
+ assertThrows(IllegalStateException.class, () -> {
+ accessTokenInvoker.invoke(params);
+ });
+// final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params);
- assertNull(tokenDto);
+// assertNull(tokenDto);
// System.out.println(accessTokenDto);
diff --git a/src/test/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvokerTest.java b/src/test/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvokerTest.java
index cd04c8177c754d25a442dd8050d0e1d22d75fb84..f645ea42fbe0b1df7d094a96f5581e46cf6c16d8 100644
--- a/src/test/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvokerTest.java
+++ b/src/test/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvokerTest.java
@@ -1,13 +1,15 @@
package com.monkeyk.sos.service.business;
import com.monkeyk.sos.service.dto.AccessTokenDto;
-import org.junit.Test;
-import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
import java.util.HashMap;
import java.util.Map;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
+
/**
* 2019/7/6
@@ -18,6 +20,7 @@ public class RefreshTokenInlineAccessTokenInvokerTest extends AbstractInlineAcce
@Test
+ @Disabled
public void invokeNormal() {
createClientDetails();
@@ -62,7 +65,8 @@ public class RefreshTokenInlineAccessTokenInvokerTest extends AbstractInlineAcce
}
- @Test(expected = InvalidGrantException.class)
+ @Test()
+ @Disabled
public void invalidRefreshToken() {
createClientDetails();
@@ -95,14 +99,17 @@ public class RefreshTokenInlineAccessTokenInvokerTest extends AbstractInlineAcce
RefreshTokenInlineAccessTokenInvoker refreshTokenInlineAccessTokenInvoker = new RefreshTokenInlineAccessTokenInvoker();
- final AccessTokenDto accessTokenDto = refreshTokenInlineAccessTokenInvoker.invoke(params2);
-
+ assertThrows(IllegalStateException.class, () -> {
+ refreshTokenInlineAccessTokenInvoker.invoke(params2);
+ });
+// final AccessTokenDto accessTokenDto = refreshTokenInlineAccessTokenInvoker.invoke(params2);
- assertNotNull(accessTokenDto);
- assertNotNull(accessTokenDto.getAccessToken());
- assertNotEquals(accessTokenDto.getAccessToken(), tokenDto.getAccessToken());
- assertEquals(accessTokenDto.getRefreshToken(), tokenDto.getRefreshToken());
+// assertNotNull(accessTokenDto);
+// assertNotNull(accessTokenDto.getAccessToken());
+//
+// assertNotEquals(accessTokenDto.getAccessToken(), tokenDto.getAccessToken());
+// assertEquals(accessTokenDto.getRefreshToken(), tokenDto.getRefreshToken());
}
diff --git a/src/test/java/com/monkeyk/sos/service/dto/TokenSettingsDtoTest.java b/src/test/java/com/monkeyk/sos/service/dto/TokenSettingsDtoTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d724b66f5853a7b3c098e698bf44cc63bb9d27e
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/service/dto/TokenSettingsDtoTest.java
@@ -0,0 +1,29 @@
+package com.monkeyk.sos.service.dto;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 2023/10/13 14:24
+ *
+ * @author Shengzhao Li
+ */
+class TokenSettingsDtoTest {
+
+
+ @Test
+ void toSettings() {
+
+
+ TokenSettingsDto settingsDto = new TokenSettingsDto();
+ TokenSettings tokenSettings = settingsDto.toSettings();
+ assertNotNull(tokenSettings);
+// System.out.println(tokenSettings);
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/web/controller/OAuthRestControllerTest.java b/src/test/java/com/monkeyk/sos/web/controller/OAuthRestControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1277f7042a75828f4474ff7d316d72cc0816ae34
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/web/controller/OAuthRestControllerTest.java
@@ -0,0 +1,119 @@
+package com.monkeyk.sos.web.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.monkeyk.sos.service.OauthService;
+import com.monkeyk.sos.service.UserService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+
+/**
+ * 2023/10/19 18:11
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+@WebMvcTest
+@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
+class OAuthRestControllerTest {
+
+
+ private MockMvc mockMvc;
+
+
+ @MockBean
+ private UserService userService;
+
+ @MockBean
+ private RegisteredClientRepository registeredClientRepository;
+
+ @MockBean
+ private OAuth2AuthorizationConsentService consentService;
+
+ @MockBean
+ private OauthService oauthService;
+
+ @MockBean
+ private OauthClientDetailsDtoValidator oauthClientDetailsDtoValidator;
+
+ @MockBean
+ private UserFormDtoValidator userFormDtoValidator;
+
+
+ @MockBean
+ private PasswordEncoder passwordEncoder;
+
+ @MockBean
+ private AuthorizationServerSettings authorizationServerSettings;
+
+
+ @BeforeEach
+ public void setup(WebApplicationContext applicationContext, RestDocumentationContextProvider contextProvider) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
+ .apply(documentationConfiguration(contextProvider))
+ .alwaysDo(result -> {
+ result.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
+ })
+ .build();
+ }
+
+
+ @Test
+ @Disabled
+ void postAccessToken() throws Exception {
+
+
+ Map parameters = new HashMap<>();
+ parameters.put("client_id", "clientxxxx");
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ String content = objectMapper.writeValueAsString(parameters);
+ assertNotNull(content);
+
+ MockHttpServletRequestBuilder requestBuilder = post("/oauth2/rest_token")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(content);
+
+ mockMvc.perform(requestBuilder)
+ //.andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("access_token").exists())
+// .andExpect(jsonPath("username").value(username))
+ .andExpect(jsonPath("refresh_token").exists())
+ .andExpect(jsonPath("scope").exists())
+ .andExpect(jsonPath("token_type").exists())
+ .andExpect(jsonPath("expires_in").exists())
+ //生成文档需要加上这句
+ .andDo(document("{ClassName}/{methodName}"));
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/monkeyk/sos/web/controller/resource/UnityControllerTest.java b/src/test/java/com/monkeyk/sos/web/controller/resource/UnityControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..51a253a9c3c52397deef240cf61a7cb6dbbb27c7
--- /dev/null
+++ b/src/test/java/com/monkeyk/sos/web/controller/resource/UnityControllerTest.java
@@ -0,0 +1,117 @@
+package com.monkeyk.sos.web.controller.resource;
+
+import com.monkeyk.sos.service.OauthService;
+import com.monkeyk.sos.service.UserService;
+import com.monkeyk.sos.service.dto.UserJsonDto;
+import com.monkeyk.sos.web.controller.OauthClientDetailsDtoValidator;
+import com.monkeyk.sos.web.controller.UserFormDtoValidator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+/**
+ * 2023/10/19 17:31
+ *
+ * @author Shengzhao Li
+ * @since 3.0.0
+ */
+@WebMvcTest
+@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
+class UnityControllerTest {
+
+
+ private MockMvc mockMvc;
+
+
+ @MockBean
+ private UserService userService;
+
+ @MockBean
+ private RegisteredClientRepository registeredClientRepository;
+
+ @MockBean
+ private OAuth2AuthorizationConsentService consentService;
+
+ @MockBean
+ private OauthService oauthService;
+
+ @MockBean
+ private OauthClientDetailsDtoValidator oauthClientDetailsDtoValidator;
+
+ @MockBean
+ private UserFormDtoValidator userFormDtoValidator;
+
+
+ @MockBean
+ private PasswordEncoder passwordEncoder;
+
+
+ @MockBean
+ private AuthorizationServerSettings authorizationServerSettings;
+
+
+ @BeforeEach
+ public void setup(WebApplicationContext applicationContext, RestDocumentationContextProvider contextProvider) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
+ .apply(documentationConfiguration(contextProvider))
+ .alwaysDo(result -> {
+ result.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE);
+ })
+ .build();
+ }
+
+
+ @Test
+ void userInfo() throws Exception {
+
+
+ UserJsonDto jsonDto = new UserJsonDto();
+ String username = "user111";
+ jsonDto.setUsername(username);
+ jsonDto.setGuid("owwiwi0a0assdfsfs11");
+ jsonDto.setEmail("user111@cloudjac.com");
+ jsonDto.setPhone("13300002222");
+ jsonDto.getPrivileges().add("ROLE_USER");
+
+ Mockito.when(userService.loadCurrentUserJsonDto()).thenReturn(jsonDto);
+
+
+ MockHttpServletRequestBuilder requestBuilder = get("/unity/user_info")
+ .contentType(MediaType.APPLICATION_JSON);
+
+ mockMvc.perform(requestBuilder)
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+// .andDo(print())
+ .andExpect(jsonPath("guid").exists())
+ .andExpect(jsonPath("username").value(username))
+ .andExpect(jsonPath("email").exists())
+ .andExpect(jsonPath("phone").exists())
+ //生成文档需要加上这句
+ .andDo(document("{ClassName}/{methodName}"));
+
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties
index da195f65a003b47e76165ccdce308a45e14876ed..49eefc0408dcafa0234e243f5b0d23e3809ce9fc 100644
--- a/src/test/resources/application-test.properties
+++ b/src/test/resources/application-test.properties
@@ -4,26 +4,42 @@ spring.application.name=spring-oauth-server
#
# MySQL
#####################
-spring.datasource.driver-class-name=com.mysql.jdbc.Driver
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2_boot_test?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=andaily
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
+# oauth2 custom issuer, since v3.0.0
+spring.security.oauth2.authorizationserver.issuer=http://127.0.0.1:${server.port}
#
-logging.level.root=INFO
+# Redis
+#
+#spring.redis.host=localhost
+#spring.redis.port=6379
+#spring.redis.database=0
+#spring.redis.password=
+#spring.redis.timeout=2000
+#spring.redis.ssl=false
+#
+# Condition Config
+# @since 2.1.0
+# Available TokenStore value: jdbc, jwt
+#sos.token.store=jwt
+# jwt key (length >= 16), optional
+# @since 2.1.0
+#sos.token.store.jwt.key=IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa
+# reuse refreshToken, default true, optional
+# @since 2.1.0
+#sos.reuse.refresh-token=true
+
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5487e5ec60c7c824d6fa8ef51c7065c8153ad868
--- /dev/null
+++ b/src/test/resources/logback.xml
@@ -0,0 +1,38 @@
+
+
+ ${spring.application.name}
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file