diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 9cc84ea9b4d95453115d0c26488d6a78694e0bc6..0000000000000000000000000000000000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 9dda3b659b46d7920f90046b0e3f106d2a59c68c..0000000000000000000000000000000000000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1 +0,0 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip diff --git a/README.md b/README.md index e0620a97edb5a9fb36efca135ebe5207a31f21fd..ad282f0809be2d26a52958bfe270ad44137e4be3 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,26 @@ ## spring-oauth-server
-java config版本(Spring Boot) +java config版本 -Spring与OAuth2的整合示例 +Spring与OAuth2的整合示例. OIDC1.0 + OAuth2.1 项目用Maven管理
-Base on Spring-Boot +Base on SpringBoot -使用的技术与版本号 +使用的主要技术与版本号
    -
  1. JDK (1.8.0_40)
  2. -
  3. spring-security-oauth2 (2.3.8.RELEASE)
  4. -
  5. spring-security-jwt (1.1.1.RELEASE)
  6. -
  7. Spring Boot(2.1.4.RELEASE)
  8. +
  9. JDK (openjdk 17)
  10. +
  11. Spring Boot(3.1.2)
  12. +
  13. Spring Core(6.0.11)
  14. +
  15. spring-security-oauth2-authorization-server (1.1.1)
  16. +
  17. thymeleaf (3.1.1.RELEASE)
+

授权协议

GPL-2.0 @@ -31,69 +33,58 @@ Base on Spring-Boot
  • 在线测试环境的使用 2018-10-14
  • spring-oauth-server v2.0.1更新内容简介 2019-08-05
  • + +> 注意:以上视频主要适用于v2.x版本 + http://list.youku.com/albumlist/show/id_51900110.html (持续更新...)
    -

    1000 star Gitee奖杯: sos-1000-stars.jpg [2019年]


    版本分支介绍

    -MySQL版本请访问Branch: master -
    -MongoDB版本请访问Branch: mongodb -
    -Redis版本请访问Branch: -config-redis + +- MySQL版本请访问Branch: master + +- MongoDB版本请访问Branch: mongodb + +- Redis版本请访问Branch: config-redis +
    +

    相关项目

    -

    - OAuth2客户端项目请访问 spring-oauth-client -

    -

    - 在线测试访问地址 http://andaily.com/spring-oauth-server/ -

    -

    - Shiro与OLTU整合的OAuth2项目 https://gitee.com/mkk/oauth2-shiro + +1. OAuth2客户端项目请访问 spring-oauth-client + +2. 在线测试访问地址 https://andaily.com/spring-oauth-server (v1.0) + +3. Shiro与OLTU整合的OAuth2项目 https://gitee.com/mkk/oauth2-shiro (相比spring-oauth-server, 该项目入门门槛相对较低, 代码更加透明, 理解更容易,可扩展性更强, 且模块化开发) -

    +
    -

    如何使用?

    -
      -
    1. -项目是Maven管理的, 需要本地安装maven(开发用的maven版本号为3.3.3), 还有MySql(开发用的mysql版本号为5.6) -
    2. -
    3. -下载(或clone)项目到本地 -
    4. -
    5. -创建MySQL数据库(如数据库名oauth2_boot), 并运行相应的SQL脚本(脚本文件位于others/database目录), -
      - 运行脚本的顺序: initial_db.ddl -> oauth.ddl -> initial_data.ddl -
    6. -
    7. -修改application.properties(位于src/main/resources目录)中的数据库连接信息(包括username, password等) -
    8. -
    9. -将本地项目导入到IDE(如Intellij IDEA)中, 可直接运行SpringOauthServerApplication.java进行访问;或配置Tomcat(或类似的servlet运行服务器), 并启动Tomcat(默认端口为8080); -
      - 也可通过maven package命令将项目编译为war文件(spring-oauth-server.war), - 将war放在Tomcat中并启动(注意: 这种方式需要将application.properties加入到classpath中并正确配置数据库连接信息). -
      - 若使用java -jar spring-oauth-server.war启动, 则需要使用参数spring.config.location指定配置文件, - 如:java -jar spring-oauth-server.war --spring.config.location=xxx.properties -
      - 提示:若打包为war则项目的 contextPath(根路径) 为 'spring-oauth-server'. -
    10. -
    11. -参考oauth_test.txt(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080/ 在界面上操作). -
    12. -
    -
    + +1. 项目是Maven管理的, 需要本地安装Maven(开发用的maven版本号为3.6.0), 还有MySql(开发用的mysql版本号为5.7.22) + +2. 下载(或clone)项目源码到本地 + +3. 创建MySQL数据库(默认数据库名oauth2_boot), 并运行相应的SQL脚本(脚本文件位于others/database目录), + 运行脚本的顺序: initial_db.ddl -> oauth.ddl -> initial_data.ddl + +4. 修改application.properties(位于src/main/resources目录)中的数据库连接信息(包括username, password等) + +5. 将本地项目导入到IDE(如Intellij IDEA)中, 可直接运行SpringOauthServerApplication.java进行访问;或配置Tomcat(或类似的servlet运行服务器), 并启动Tomcat(默认端口为8080); + 也可通过maven package命令将项目编译为jar文件(spring-oauth-server.jar), + 使用命令java -jar启动访问. + 若使用java -jar spring-oauth-server.jar启动, 建议使用参数spring.config.location指定配置文件, + 如:java -jar spring-oauth-server.jar --spring.config.location=xxx.properties + +6. 参考oauth2.1-flow.md(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080/ 在界面上操作). + +

    配置参数说明

    说明配置文件application.properties中的主要变量。 @@ -103,35 +94,36 @@ config-redis spring.datasource.*是-数据库连接相关配置 - spring.mvc.*是-Spring MVC相关配置 - logging.level.root是INFO默认的日志级别 - sos.token.store是jwt可选值:jwt,jdbc;配置Token存储方式,v2.1.0增加 - sos.token.store.jwt.key否IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa当sos.token.store为jwt时配置具体的jwt key(长度16位或32位) - sos.reuse.refresh-token否true可选值:true,false;true重复使用refresh_token值直到过期,false每次刷新时生成新的refresh_token值(类似session机制进行续期),v2.1.0增加 + spring.thymeleaf.*是-Spring MVC thymeleaf相关配置 + server.port是8080服务运行端口号 + spring.security.oauth2.authorizationserver.issuer是OAuth2 issuer, 生产环境配置对外访问完整地址 + spring.application.name是应用组件名称

    grant_type 介绍

    -
    -说明OAuth2支持的grant_type(授权方式)与功能 + +说明OAuth2.1支持的grant_type(授权方式)与功能
    1. authorization_code -- 授权码模式(即先登录获取code,再获取token)
    2. -
    3. password -- 密码模式(将用户名,密码传过去,直接获取token)
    4. +
    5. authorization_code + PKCE -- 授权码模式+PKCE (即先登录获取code, 请求时增加参数code_challengecode_challenge_method; 再获取token,增加参数code_verifier)
    6. +
    7. password -- 密码模式(将用户名,密码传过去,直接获取token) OAuth2.1不推荐使用
    8. refresh_token -- 刷新access_token
    9. -
    10. implicit -- 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash)
    11. +
    12. device_code -- 适用于各类无输入键盘的物联网智能设备进行认证授权, 通过类似'扫码登录'形式完成整个流程 OAuth2.1新增
    13. client_credentials -- 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向'服务端'获取资源)
    14. +
    15. jwt-bearer -- 增强client端请求安全性的断言(assertion)实现; 通过类似'双向SSL'的机制来让server端验证client端的签名实现强安全性.
    - +> 注意:相比OAuth2.0,去掉了 **implicit** 模式

    帮助与改进

    1. - 与该项目相关的博客请访问 http://blog.csdn.net/monkeyking1987/article/details/16828059 + 与该项目相关的博客请访问 https://blog.csdn.net/monkeyking1987/article/details/16828059

    2. @@ -164,23 +156,18 @@ config-redis 改变token过期的时间的配置, 请下载文件改变token过期的时间的配置.jpg
    3. - 自定义 grant_type, 默认情况支持的grant_type包括 [password,authorization_code,refresh_token,implicit], 若不需要其中的某些grant_type, + 自定义 grant_type, 默认情况支持的grant_type包括 [password,authorization_code,refresh_token,device_code], 若不需要其中的某些grant_type, 则可以修改 oauth_client_details 表中的 authorized_grant_types 字段的值; -
      - 若想把整个OAuth2服务修改来只支持某些grant_type, 请修改 security.xml文件中的 - 中的内容,将对应的 grant_type 注释或删掉即可
    4. 如何刷新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 +

        +
          +
        1. 底层安全架构升级:jdk升级17, spring6.x, springboot3.x, thymeleaf替换servlet/jsp

        2. +
        3. 全面升级支持 OAuth2.1协议与 OIDC1.0协议

        4. +
        5. 构建包由war换成jar, SQL相应调整

        6. +
        7. 用spring-security-oauth2-authorization-server升级替换spring-security-oauth2, 详见背景说明

        8. +
        9. 界面使用说明按OAuth2.1进行友好设计并更新各提示语句

        10. +
        11. 增加spring-restdocs文档支持, 自动生成API相关文档

        12. +
        +
      • +
      • +

        + Version: 2.1.1 [canceled]
        Date: 2022-05-05 / ---

        1. 尝试升级替换spring-security-oauth2, 详见背景说明

        -
      • @@ -343,7 +344,7 @@ config-redis

        1. #73 - Upgrade 'spring-security-oauth2' version to '2.0.6.RELEASE' (current: 1.0.5.RELEASE) [CANCELED]

        2. #74 - oauth mysql ddl add create_time, default is now()

        3. -
        4. #75 - Add user information API, for spring-oauth-client project use +

        5. #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
                               
          -

          +
        6. #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
        7. 2019-08-04 发布 2.0.1 版本

        8. 2020-06-04 发布 2.0.2 版本

        9. 2022-05-01 发布 2.1.0 版本

        10. +
        11. 2023-10-10 开始全新大版本 3.0.0 开发

        12. +
        13. 2023-10-31 发布 3.0.0 全新版本

      @@ -412,76 +416,84 @@ config-redis - -
      -

      - 与项目相关的技术文章请访问 http://andaily.com/blog/?cat=19 (不断更新与OAuth2相关的文章) -

      +
      + + 与项目相关的技术文章请访问 http://andaily.com/blog/?cat=19 (不断更新与OAuth2相关的文章) +
      -

      问答与讨论

      + 问答与讨论
      与项目相关的,与OAuth2相关的问题与回答,以及各类讨论请访问
      - http://andaily.com/blog/?dwqa-question_category=oauth + http://andaily.com/blog/?dwqa-question_category=oauth
      @@ -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_clientidvarchar主键,系统自动生成
      archivedtinyint + 用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档). +
      + 对该字段的具体使用请参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上archived + = 0条件 (扩展字段) +
      create_timedatetime数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
      updated_timetimestamp数据的最后更新时间,由数据库自行更新维护
      client_idvarchar + 唯一,不能为空. +
      + 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成). +
      + 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念. +
      client_id_issued_attimestampclient_id的签发时间, 默认为数据创建时间
      client_secretvarchar + 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成),加密保存. +
      + 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念. +
      client_secret_expires_atdatetimeclient_secret的过期时间,null表示永不过期
      client_namevarchar客户端(client)的名称,一般是一个有业务意义的名称
      client_authentication_methodsvarchar认证支持的方式,多个由逗号分隔; 如: client_secret_basic,client_secret_post; 一般指认证时传递client_secret支持哪些方式
      authorization_grant_typesvarchar + 指定客户端支持的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_codeurn:ietf:params:oauth:grant-type:jwt-bearer是OAuth2.1中新增. +
      redirect_urisvarchar + 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_urisvarchar OAuth2 退出时 post 的客户端重定向 uri; 可选 多个由逗号分隔, 一般在client注册时可填写
      scopesvarchar + 指定客户端申请的权限范围,可选值在OIDC协议中定义, + 包括openid,profile,email,address,phone;若有多个值用逗号(,)分隔,如: + "openid,email". +
      + openid是必须有的,其他值若有则在获取的id_token中会包含对应的值. +
      + 在实际应该中, 该值一般由服务端指定, 常用的值为openid. +
      client_settingsvarchar + 客户端的各类设置, 如是否支持PKCE,用户授权(consent)确认是否必须等; 详见代码ClientSettings.java; + 此字段存储JSON格式的数据值. +
      token_settingsvarchar + 对token的各类设置; 如 token有效期, refresh_token有效期等; 详见代码TokenSettings.java; + 此字段存储JSON格式的数据值. +
      +

      + + 在项目中,主要操作oauth2_registered_client表的类是ClientDetailsController.java, + OauthClientDetails.java更多的细节请参考该类; 也可以根据实际的需要,去扩展或修改该类的实现. +

      +
      oauth2_authorizationidvarchar主键
      registered_client_idvarchar + 外键, 关联oauth2_registered_client的id字段 +
      principal_namevarchar认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段
      authorization_grant_typevarcharOAuth2的 grant_type 类型
      authorized_scopesvarchar此次授权的范围(scope)
      attributesblob进行认证授权的各类信息,JSON格式
      statevarchar认证请求中传递的 state 值
      authorization_code_valueblobauthorization_code流程中的code
      authorization_code_issued_atdatetimeauthorization_code流程中的code签发时间
      authorization_code_expires_atdatetimeauthorization_code流程中的code过期时间
      authorization_code_metadatablobauthorization_code流程中的code的属性设置, 如值是否有效
      access_token_valueblobaccess_token 值
      access_token_issued_atdatetimeaccess_token 签发时间
      access_token_expires_atdatetimeaccess_token 过期时间
      access_token_metadatablobaccess_token 属性设置, 如各类claims中的属性与值
      access_token_typevarcharaccess_token 类型, 一般是Bearer
      access_token_scopesvarchar此次授权的scope范围值,如: openid,profile
      oidc_id_token_valueblobOIDC中id_token 值
      oidc_id_token_issued_atdatetimeid_token 签发时间
      oidc_id_token_expires_atdatetimeid_token 过期时间
      oidc_id_token_metadatablobid_token 属性设置, 如各类claims中的属性与值
      refresh_token_valueblobrefresh_token 值
      refresh_token_issued_atdatetimerefresh_token 签发时间
      refresh_token_expires_atdatetimerefresh_token 过期时间
      refresh_token_metadatablobrefresh_token 属性设置, 如是否复用(reuse)
      user_code_valueblobdevice_code流程中的user_code值
      user_code_issued_atdatetimeuser_code 签发时间
      user_code_expires_atdatetimeuser_code 过期时间
      user_code_metadatablobuser_code 属性设置, 如是否已经验证
      device_code_valueblobdevice_code流程中的device_code值
      device_code_issued_atdatetimedevice_code 签发时间
      device_code_expires_atdatetimedevice_code 过期时间
      device_code_metadatablobdevice_code 属性设置, 如是否已经验证
      updated_timetimestamp数据的最后修改时间, 由数据库自动维护更新
      +

      + 该表用于存储在OAuth2.1授权过程中各类信息数据, + 支持各类grant_type场景; + 对oauth2_authorization表的主要操作在JdbcOAuth2AuthorizationService.java类中, + 更多的细节请参考该类. +
      + 注意: 若对性能有要求, 此表的数据存储设计需要进行优化(如存redis或利用JWT特性简化一些不必要的存储字段). +

      +
      oauth2_authorization_consentregistered_client_idvarchar外键, 关联oauth2_registered_client表的id字段
      principal_namevarchar认证名称, 一般指用户名或clientId; 对应OIDC中的sub字段
      authoritiesvarchar授权确认过期中的属性, 如scope范围
      updated_timetimestamp数据的最后修改时间, 由数据库自动维护更新
      +

      + 该表主要存储在授权过程中需要用户进行确认(consent)的信息; + 在项目中,主要操作oauth2_authorization_consent表的对象是JdbcOAuth2AuthorizationConsentService.java. + 更多的细节请参考该类. +

      +
      user_idint主键, 自增长, 数据库自动生成
      guidvarchar唯一, 业务id
      create_timedatetime数据创建时间
      updated_timetimestamp数据的最后修改时间, 由数据库自动维护更新
      usernamevarchar用户名, 非空, 唯一
      passwordvarchar密码, 加密存储, 非空
      enabledtinyint是否启用, 默认1(即启用)
      phonevarchar手机号
      emailvarchar邮箱地址
      addressvarchar个人地址
      nicknamevarchar用户昵称, 别名
      updated_atint最后数据更新时间值
      default_usertinyint是否默认用户, 默认0(不是); 只用在初始化数据时使用
      last_login_timedatetime最后登录时间
      +

      + 在项目中,主要使用user_表的对象是UserServiceImpl.java; + 对应的实体是User.java; + 在Spring Security中, 此表存储的数据对应UserDetails.java类. +

      +
      user_privilegeuser_idint外键, 关联user_的id字段
      privilegevarchar权限值, 如: 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

      + +
        +
      • +

        + 请求URI: /oauth2/token POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        grant_typeauthorization_code固定值
        code{code}
        redirect_uri{redirect_uri}
        code_verifier{code_verifier}PKCE时必须
        + 请求示例: +
        +
          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"'
        +
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +  "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
          +}
          +
          +
        • +
        • +
          + 异常 [401]
          +
          {
          +    "error": "invalid_grant"
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      返回

      + +

      获取access_token (grant_type=client_credentials) + public +

      + +

      使用grant_type=client_credentials 方式来获取access_token, 不需要username, password

      + +
        +
      • +

        + 请求URI: /oauth2/token POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        grant_typeclient_credentials固定值
        scope{scope}如: openid
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "access_token": "p2i1WHiiFBCgTJFTs63OvO9-bclB9DbsgsebDo_ntMw_BAleu2RzIQzzFfaaJAR5oiL3xwN3xMyNTRZSrXM_1ANycleysPU5l3xuZ0aQX4V-Va178qg6e-PvLqLBsD_i",
          +    "scope": "openid profile",
          +    "token_type": "Bearer",
          +    "expires_in": 3599
          +}
          +
          +
        • +
        • +
          + 异常 [401]
          +
          {
          +    "error": "invalid_client"
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      返回

      + +

      刷新access_token (grant_type=refresh_token) + public +

      + +

      用于在access_token要过期时换取新的access_token (grant_type需要有refresh_token)

      + +
        +
      • +

        + 请求URI: /oauth2/token POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        grant_typerefresh_token固定值
        refresh_token{refresh_token}
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "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
          +}
          +
          +
        • +
        • +
          + 异常 [401]
          +
          {
          +    "error": "invalid_client"
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      获取access_token (Restful API) + public +

      + +

      Restful API 获取access_token, + 适用于grant_type为authorization_code,refresh_token,client_credentials

      + +
        +
      • +

        + 请求URI: /oauth2/rest_token POST REST +

        + +

        + Content-Type: + application/json +

        +
        + 请求Body参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        grant_type{grant_type}authorization_code,refresh_token,client_credentials
        scope{scope}如 openid
        client_id{client_id}
        client_secret{client_secret}
        code{code}grant_type=authorization_code时必须有
        code_verifier{code_verifier}grant_type=authorization_code + PKCE时必须有
        refresh_token{refresh_token}grant_type=refresh_token时必须有
        + 请求Body示例: +
        {
        +    "client_id": "cRG45sAr1hJ2SSKzmB5UsjPIgd7VVlM0",
        +    "client_secret": "cRG45sAr1hJ2SSKzmB5UsjPIgd7VVlM0",
        +    "grant_type": "authorization_code",
        +    "redirect_uri": "https://andaily.com/oauth2/callback",
        +    "scope": "openid profile",
        +    "code": "AL-Q-OUhhgDrG-x-nFCKILadiBA2gTgvnDYh4AaPuviiQYqPTzLl-BVDXH2VNILS4X3qnDhC7rGaiWY9Qdm9SUTAvttaSXox_g_V8Qn27PeI21xv3RFkGE21BJJWdaUB",
        +    "code_verifier": "UUdEMjhQMG1OZkF0NEFNcW12TmdOanVnaHpWeHREMGI="
        +}
        + 或 +
        {
        +    "client_id": "cRG45sAr1hJ2SSKzmB5UsjPIgd7VVlM0",
        +    "client_secret": "cRG45sAr1hJ2SSKzmB5UsjPIgd7VVlM0",
        +    "grant_type": "refresh_token",
        +    "refresh_token": "-ETmWRoPkWvH2bC4ufB2fZwyeXXaqwmUq17zS_vEGGHb9dHnXd1W8HRZnUCEP03kJEn0IQ5w8E6jyAYb_oDC3iTgbT0OwRmIbnKWbcqbp5uxjB4RpNKBtyUMxax-EU1_"
        +}
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "access_token": "eyJraWQiOiJzb3MtcnNhLWtpZDIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNSRzQ1c0FyMWhKMlNTS3ptQjVVc2pQSWdkN1ZWbE0wIiwibmJmIjoxNjk4NzUyNTQzLCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiXSwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNjk4NzU2MTQzLCJpYXQiOjE2OTg3NTI1NDMsImp0aSI6IjU3MDA1MDQ1Nzc1MzMwMTI1OTYyMjIzNzUxNjE2MjE1In0.PlrUmTd5DUq10Sj-p2MZYoQuciVLixnsUT-RMah1eHLBvVlP4YsoCOgvzlFQQLVT8TRv-XM8JIJQCo0o151ZHhGmDzaWHEsDdPGlJvWL959LbZjw8f3BR8MvMGaBgjjMV6uWgY_nZBTef10KwWsZZwrVtR25n_qHk9SZkyFDzI6Bk7S6BvlOMFNYp80Ik8khRTwYphFykBtQQE9D8wzR59YuLP6aQVfbU8gkM6aNYmV7vYELtoQqI_Co9pOCC1Fwb7Ae-qgQzknr_FsARoui9cIgSBk7r0i5v-1fKXFC8I1idtuw-CA9GGXqNPoohDpV-e1AkygT1rbhuYavuNnv-g",
          +    "refresh_token": "L_3boxXBZFbA2odSvrrMD9tku58L_MkIAcnP8ahP2OhpuZIYDHFPqGNr0rHuhuM_1zeXu8cLKCIMsoS8leL9ZvIBJ2h84ZXs5PgcNX2jZPCedpMwTN6pKZe236Sz1Y0j",
          +    "scope": "openid profile email",
          +    "id_token": "eyJraWQiOiJzb3MtZWNjLWtpZDEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MCIsInNpZCI6IjN3a3JXR3NucHg5SW9aQXd2d09sbkQyZ0RhTVNBSWZaazBkY2VLeHZlaVkiLCJhdWQiOiJjUkc0NXNBcjFoSjJTU0t6bUI1VXNqUElnZDdWVmxNMCIsInVwZGF0ZWRfYXQiOjAsImF6cCI6ImNSRzQ1c0FyMWhKMlNTS3ptQjVVc2pQSWdkN1ZWbE0wIiwiYXV0aF90aW1lIjoxNjk4NzUyMzQ3LCJuaWNrbmFtZSI6IiIsImV4cCI6MTY5ODc1NDM0MywiaWF0IjoxNjk4NzUyNTQzLCJqdGkiOiIyNDc3NTYyNjQxMDEwMTQxNTEyNzMwMjQ0MDQ4OTMzMyIsImVtYWlsIjoiYWRtaW5AYW5kYWlseS5jb20ifQ.k8zdBJE9aq-Tp_rz_ssUrTBxV4e8ZpKOn10s71j6xjGp7kZq1RiATrU8t00wx1RNRC4E1V62nTeYpJuTy4mdlA",
          +    "token_type": "Bearer",
          +    "expires_in": 3599
          +}
          +

          根据不同的grant_type, 返回的结果不同

          +
          +
        • +
        • +
          + 异常 [400]或[401]
          +
          {
          +    "error": "invalid_grant"
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      检查token (/oauth2/introspect) + public +

      + +

      校验, 检查token的有效性

      + +
        +
      • +

        + 请求URI: /oauth2/introspect POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        token{token}token可以是access_token, refresh_tokenid_token
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "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"
          +}
          +

          根据不同类型的token响应结果不相同; active=true表示token为有效的

          +
          +
        • +
        • +
          + 异常 [200]
          +
          {
          +    "active": false
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      返回

      +

      撤销token (/oauth2/revoke) + public +

      + +

      撤销已经签发的token

      + +
        +
      • +

        + 请求URI: /oauth2/revoke POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        token{token}token可以是access_token, refresh_tokenid_token
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          
          +                                

          此API不管什么token结果都响应200; 若token是有效的会成功撤销

          +
          +
        • +
        • +
          + 异常 [200] +
          
          +                            
          +
        • +
        +
      • +
      +
      + +
      +

      [device_code]流程 - 发起认证(/oauth2/device_authorization)public

      + +

      发起认证, 获取user_code, device_code等信息

      + +
        +
      • +

        + 请求URI: /oauth2/device_authorization POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        scope{scope}如: openid profile
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "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
          +}
          +
          +
        • +
        • +
          + 异常 [401]
          +
          {
          +    "error": "invalid_client"
          +}
          +
          +
        • +
        +
      • +
      +
      + +
      +

      [device_code]流程 - 获取token(/oauth2/token)public

      + +

      设备上轮循调用, 获取token

      + +
        +
      • +

        + 请求URI: /oauth2/token POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        grant_typeurn:ietf:params:oauth:grant-type:device_code固定值
        device_code{device_code}发起认证时返回的device_code值
        + 请求示例: +
        curl --location 'http://localhost:8080/oauth2/token' \
        +--header 'Content-Type: application/json' \
        +--form 'client_id="6urNLgR6osk2E56ekp"' \
        +--form 'client_secret="6urNLgR6osk2E56ekp"' \
        +--form 'grant_type="urn:ietf:params:oauth:grant-type:device_code"' \
        +--form 'device_code="iBv-_clBQtJR4w2eN8bgGBnwWgcoem6FCJlHgahhHNOq9oImcJAWLYKJ-jJOk207X19uE-glkArRLnhXgpm0C0pQcoxAZyoMmgznvWxOITQUYFYdAluBTo-fmDteKSgh"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "access_token": "QqPGuiF9c2HKYQEdxrs9E0WsRijEl_z9sINI6CFD5yMulXaZutLTktVtLP3zcr22XuYJOzWZMzOgvjWl2tqAoMo3S2MHBgxjPmx5gfr6DjeQPsW3fFPVc6pOa5Ll6u4S",
          +    "refresh_token": "7vtQtkU95tjt7nkaX8DZnDVntrgPYIoXB6_4WsV9FzMi-ppoPB_H5qmufi4EHqAuJPwdlxXYdDbVYoGudXd0iCPfmqT5B8CcW7zRsgaKQOHQlPw9Ju3wMGNSRk14YRWI",
          +    "scope": "profile",
          +    "token_type": "Bearer",
          +    "expires_in": 3599
          +}
          +
          +
        • +
        • +
          + 异常 [400]
          +
          {
          +    "error": "authorization_pending",
          +    "error_uri": "https://datatracker.ietf.org/doc/html/rfc8628#section-3.5"
          +}
          +

          说明: error=authorization_pending 表示授权正在进行中, 设备上需要轮循继续等待授权结果

          +
          +
        • +
        +
      • +
      +
      + +
      +

      [jwt-bearer] - 获取token(/oauth2/token)public

      + +

      jwt-bearer流程, 获取token

      + +
        +
      • +

        + 请求URI: /oauth2/token POST +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        client_id{client_id}
        client_secret{client_secret}
        grant_type{grant_type}可选值: authorization_code, client_credentials, refresh_token
        scope{scope}可选值: openid, email, profile 等
        client_assertion_typeurn:ietf:params:oauth:client-assertion-type:jwt-bearer固定值
        client_assertion{client_assertion}一个根据算法生成的JWT值, 详见JwtBearerFlowTest.java
        + 请求示例: +
        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"'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "access_token": "QqPGuiF9c2HKYQEdxrs9E0WsRijEl_z9sINI6CFD5yMulXaZutLTktVtLP3zcr22XuYJOzWZMzOgvjWl2tqAoMo3S2MHBgxjPmx5gfr6DjeQPsW3fFPVc6pOa5Ll6u4S",
          +    "refresh_token": "7vtQtkU95tjt7nkaX8DZnDVntrgPYIoXB6_4WsV9FzMi-ppoPB_H5qmufi4EHqAuJPwdlxXYdDbVYoGudXd0iCPfmqT5B8CcW7zRsgaKQOHQlPw9Ju3wMGNSRk14YRWI",
          +    "scope": "profile",
          +    "token_type": "Bearer",
          +    "expires_in": 3599
          +}
          +

          根据不同的grant_type, 响应结果有所不同.

          +
          +
        • +
        • +
          + 异常 [400]
          +
          {
          +    "error": "invalid_grant"
          +}
          +

          说明: 根据不同的grant_type,响应异常结果有差别.

          +
          +
        • +
        +
      • +
      +
      + +
      +

      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范围来决定

          +
          +
        • +
        • +
          + 异常 [401]
          +
          
          +                            
          +
        • +
        +
      • +
      +
      + +
      +

      OIDC /openid-configurationpublic

      + +

      OIDC well-known API

      + +
        +
      • +

        + 请求URI: /.well-known/openid-configuration GET +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        + 请求示例: +
        curl --location 'http://localhost:8080/.well-known/openid-configuration' \
        +--header 'Content-Type: application/json'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "issuer": "http://127.0.0.1:8080",
          +    "authorization_endpoint": "http://127.0.0.1:8080/oauth2/authorize",
          +    "device_authorization_endpoint": "http://127.0.0.1:8080/oauth2/device_authorization",
          +    "token_endpoint": "http://127.0.0.1:8080/oauth2/token",
          +    "token_endpoint_auth_methods_supported": [
          +        "client_secret_basic",
          +        "client_secret_post",
          +        "client_secret_jwt",
          +        "private_key_jwt"
          +    ],
          +    "jwks_uri": "http://127.0.0.1:8080/oauth2/jwks",
          +    "userinfo_endpoint": "http://127.0.0.1:8080/userinfo",
          +    "end_session_endpoint": "http://127.0.0.1:8080/connect/logout",
          +    "response_types_supported": [
          +        "code"
          +    ],
          +    "grant_types_supported": [
          +        "authorization_code",
          +        "client_credentials",
          +        "refresh_token",
          +        "urn:ietf:params:oauth:grant-type:device_code",
          +        "password",
          +        "urn:ietf:params:oauth:grant-type:jwt-bearer"
          +    ],
          +    "revocation_endpoint": "http://127.0.0.1:8080/oauth2/revoke",
          +    "revocation_endpoint_auth_methods_supported": [
          +        "client_secret_basic",
          +        "client_secret_post",
          +        "client_secret_jwt",
          +        "private_key_jwt"
          +    ],
          +    "introspection_endpoint": "http://127.0.0.1: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",
          +        "ES256"
          +    ],
          +    "scopes_supported": [
          +        "openid",
          +        "profile",
          +        "email",
          +        "address",
          +        "phone"
          +    ]
          +}
          +
          +
        • +
        • +
          + 异常 [400]
          +
          
          +                            
          +
        • +
        +
      • +
      +
      + +
      +

      返回

      +

      OAuth2.1 /oauth-authorization-serverpublic

      + +

      OAuth2.1 well-known API

      + +
        +
      • +

        + 请求URI: /.well-known/oauth-authorization-server GET +

        + +
        + 请求参数说明: + + + + + + + + + + + + + + +
        参数名参数值必须?备注
        + 请求示例: +
        curl --location 'http://localhost:8080/.well-known/oauth-authorization-server' \
        +--header 'Content-Type: application/json'
        + +
        +
        + + 响应 + +
          +
        • +
          + 正常 [200]
          +
          {
          +    "issuer": "http://127.0.0.1:8080",
          +    "authorization_endpoint": "http://127.0.0.1:8080/oauth2/authorize",
          +    "device_authorization_endpoint": "http://127.0.0.1:8080/oauth2/device_authorization",
          +    "token_endpoint": "http://127.0.0.1:8080/oauth2/token",
          +    "token_endpoint_auth_methods_supported": [
          +        "client_secret_basic",
          +        "client_secret_post",
          +        "client_secret_jwt",
          +        "private_key_jwt"
          +    ],
          +    "jwks_uri": "http://127.0.0.1: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://127.0.0.1:8080/oauth2/revoke",
          +    "revocation_endpoint_auth_methods_supported": [
          +        "client_secret_basic",
          +        "client_secret_post",
          +        "client_secret_jwt",
          +        "private_key_jwt"
          +    ],
          +    "introspection_endpoint": "http://127.0.0.1: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"
          +    ]
          +}
          +
          +
        • +
        • +
          + 异常 [400]
          +
          
          +                            
          +
        • +
        +
      • +
      +
      + +
      +
      + + +
      +
      +
      +

      + © 2013 - 2023 spring-oauth-server +

      +
      +
      + + \ 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 + +
      +
      +

      client_details

      +
      +
      + +
      +
      + +
      + +
      +
        +
      • +
        +
        + test + archive +
        + Archived +
        +

        + [[${cli.clientId}]] - + +

        + +
        + client_id:   + client_secret: ***  +
        + grant_types:   + authentication_methods:   +
        + scopes:   + redirect_uri:   +
        + client_id_issued:   + client_secret_expires: +
        + client_settings: +
        + token_settings: +
        + create_time:   + archived:   +
        +
      • + +
      +
      + 每一个item对应oauth2_registered_client表中的一条数据; 共条数据. +
      + 对数据库表的详细说明请访问 + https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html + (或访问项目others目录的db_table_description_3.0.0.html文件) +
      +
      + +
      + + \ 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实际上是向该表中按不同的条件添加数据. +
      + +
      +
      +
      + + +
      + + +

      client_id必须输入,且必须唯一,长度至少10位; 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.

      +
      +
      +
      + + +
      + + OAuth2.1新增 +

      Client有意义的名称.

      +
      +
      +
      + + +
      + + +

      client_secret必须输入,且长度至少10位; 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念. +
      + 注意: 由于client_secret 会加密存储, 请先复制并保留client_secret值 +

      +
      +
      +
      + + +
      + + + + +
      + OAuth2.1新增 +

      + 选择在认证时支持传递client_secret参数的方式;在正式环境中,此值一般不需要选择而是由后台创建时根据业务设置即可; +
      + client_secret_jwtprivate_key_jwt只在grant_type=jwt-bearer中会使用到 +

      +
      +
      + +
      + + +
      + + + + + + +

      scopes值由OIDC 1.0协议中定义(详见#ScopeClaims),openid必须选择;在正式环境中,此值一般不需要选择而是由后台创建时根据业务设置即可 +

      +
      +
      + +
      + + +
      + + + + + + + +

      + 至少勾选一项grant_type(s), 且不能只单独勾选refresh_token; 当勾选jwt-bearer时需要将'authentication_methods'中的 + client_secret_jwtprivate_key_jwt至少勾选一个 +

      +
      +
      + +
      + + +
      + + +

      grant_type包括authorization_code, 则必须输入至少一个 redirect_uri + (多个uri用半角逗号分隔)

      +
      +
      + +
      + + +
      + + +

      OAuth2 退出时post的客户端重定向 uri (多个uri用半角逗号分隔),可选

      + OAuth2.1新增 +
      +
      + +
      +
      + +
      + +
      +
      + + +
      + + + +

      是否在authorization_code流程中支持PKCE(Proof Key for Code Exchange)

      +
      +
      +
      + + +
      + + + +

      是否在authorization_code流程中授权时需要用户进行确认

      +
      +
      +
      + + +
      + + +

      选择在调用/oauth2/token + API时使用的签名算法(当grant_type为jwt-bearer时会用到);HS256是对称算法(secret是client_secret加密后的值), + RS256ES256是非对称算法(public-key由提供的jwk_set_url获取) +

      +
      +
      +
      + + +
      + + +

      + 设置client提供的获取jwk的URL地址(当grant_type为jwt-bearer且'认证jwt签名算法'选择RS256ES256时会用到); + 此URL返回的jwk格式要与spring-oauth-server提供的jwks一致 + (参考实现类JwtBearerJwksController.java) +

      +
      +
      + +
      + +
      + + +
      + + +

      设定authorization_code流程中code的有效时长(单位:秒);默认300(5分钟)

      +
      +
      +
      + + +
      + + +

      设定device_code流程中code的有效时长(单位:秒);默认300(5分钟)

      + OAuth2.1新增 +
      +
      +
      + + +
      + + +

      设定客户端access_token的有效时长(单位:秒),可选, 若不设定值则使用默认的有效时间值(3600秒); + 若设定则必须是大于0的整数值(推荐不小于60)

      +
      +
      + +
      + + +
      + + +

      设定客户端refresh_token的有效时长(单位:秒),可选, 若不设定值则使用默认的有效时间值(43200秒); + 若设定则必须是大于0的整数值且不能小于access_token有效时长

      +
      +
      +
      + + +
      + + + +

      当调用refresh token API后是否继续使用之前的refresh_token值,Yes则继续使用,No则每次调用refresh + token API后会返回一新的refresh_token值(即refresh_token只能使用一次,安全性更高)

      + OAuth2.1新增 +
      +
      +
      + + +
      + + + +

      设置access_token值的格式,self-contained使用JWT格式(默认),reference使用类UUID格式 +

      + OAuth2.1新增 +
      +
      +
      + + +
      + + +

      选择生成id_token时使用的算法;注意:支持的算法要有对应用jwk (jwks.json + 文件),判断支持哪些key可访问 + /.well-known/openid-configuration进行查看 +

      + OAuth2.1新增 +
      +
      +
      + + +
      +
      +
      + +
      +
      + + +
      +
      +
      + + 取消 +
      +
      +
      +
      +
      + + + +
      + + \ 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]
      +
      +

      输入每一步必要的信息后点击其下面的按钮地址.

      +
        +
      1. +
        + 从 spring-oauth-server获取 'code' +
        +
        + + + + + + + + + + + + + + + + + + + + + +
        client_id + +
        redirect_uri + +

        若配置有多个redirect_uri可自行修改(默认使用第一个)

        +
        scope + +
        state + +

        每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)

        +
        response_type + +

        固定值

        +
        + + GET +
        +
        +
      2. +
      3. + 用 'code' 换取 'access_token' +
        + 输入第一步获取的'code'并点击按钮链接地址. +
        + +
        + + + + + + + + + + + + + + + + + + + + + +
        client_id + +
        client_secret + +
        redirect_uri + +

        若配置有多个redirect_uri可自行修改(默认使用第一个)

        +
        grant_type + +

        固定值

        +
        code + +

        请输入code值

        +
        + + POST +
        +
      4. +
      +
      +
      + +
      +
      Test [authorization_code + PKCE]
      +
      +

      输入每一步必要的信息后点击其下面的链接地址.

      +
        +
      1. +
        + 从 spring-oauth-server获取 'code' +
        + PKCE流程在开始前需要先通过代码生成code_verifiercode_challenge (如何生成详见工具类 + PKCEUtils.java + ); +
        + 生成后在获取'code'时要在已有的参数基础上再增加两个参数: + + + + + + + + + +
        code_challenge对 code_verifier 使用指定算法进行计算(digest)并base encode的值
        code_challenge_method固定值:S256
        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        client_id + +
        redirect_uri + +

        若配置有多个redirect_uri可自行修改(默认使用第一个)

        +
        scope + +
        state + +

        每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)

        +
        response_type + +

        固定值

        +
        code_challenge_method + +

        固定值

        +
        code_challenge + +

        (后台代码生成,不可修改)

        +
        + + GET +
        + +
        +
      2. +
      3. + 用 'code' 换取 'access_token' +
        + 输入第一步获取的code并点击按钮地址. + +
        + + + + + + + + + + + + + + + + + + + + + + + + + +
        client_id + +
        client_secret + +
        redirect_uri + +

        若配置有多个redirect_uri可自行修改(默认使用第一个)

        +
        grant_type + +

        固定值

        +
        code + +

        请输入code值

        +
        code_verifier + +

        (后台代码生成,不可修改)

        +
        + + POST +
        +
      4. +
      +
      +
      + +
      +
      Test [password] OAuth2.1不支持
      +
      +

      输入username, password 后点击链接地址.

      + username: +
      + password: + +
      + +
      + + + + + + + + POST +
      +
      +
      + +
      +
      Test [device_code] OAuth2.1新增
      +
      +
        +
      1. +

        设备上请求 /oauth2/device_authorization获取 user_code, + device_code,verification_uri

        +
        + + + + + + + + + + + + + +
        client_id + +
        client_secret + +
        scope + +
        + + POST +
        +

        一般此步骤是在设备上通过代码来完成, 此处只作演示流程

        +
      2. +
      3. +

        在设备上展示user_code或显示一个二维码(内容为verification_uri_complete URL)

        +

        用已经登录成功的浏览器(或另一个已经认证的设备)访问verification_uri_complete URL(可通过扫码等方式获取内容)

        +

        + 此处方便演示, 请点击/oauth2/device_verification并输入上一步获取到的user_code + (若未认证将跳转到登录) +

        +

        提示:此步骤必须在有效时间内完成, user_code的有效时长在上一步中返回的数据expires_in来决定(单位:秒, + 默认5分钟)

        +
      4. +
      5. +

        + 在第2步进行的同时, + 设备上后台将定时(如每隔5秒)向spring-oauth-server发起获取token的请求/oauth2/token + (需要使用第1步中获取到 device_code 的值), +
        + 直到获取成功(即第2步操作完成授权设备登录)或超时(即设备轮询请求等待的时长超出第1步返回的时间expires_in) +

        +

        请输入device_code后点击按钮地址.

        +
        + + + + + + + + + + + + + + + + + +
        client_id + +
        client_secret + +
        grant_type + +

        固定值

        +
        device_code + +

        请输入device_code

        +
        + + POST +
        +

        提示:在第2步进行过程中调用第3步获取token API时会响应等待授权的结果(Http状态码 400, + error='authorization_pending')

        +
      6. +
      + +
      +
      + +
      +
      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值, 点击按钮地址即可测试

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      client_id + +
      client_secret + +
      scope + +
      grant_type + +

      grant_type根据需要值可以是authorization_code refresh_token等 +

      +
      client_assertion_type + +

      固定值

      +
      client_assertion + +

      如何生成client_assertion, 详见示例类: JwtBearerFlowTest.java +

      +
      + + POST +
      + +
      +
      + +
      +
      Test [client_credentials]
      +
      +

      点击按钮地址即可测试

      + + +
      + + + + + + + + + + + + + + + + + +
      client_id + +
      client_secret + +
      scope + +
      grant_type + +

      固定值

      +
      + + POST +
      +
      +
      + +
      +
      Test [refresh_token]
      +
      +

      输入refresh_token 后点击链接地址.

      + +
      + + + + + + + + + + + + + + + + + +
      client_id + +
      client_secret + +
      grant_type + +

      固定值

      +
      refresh_token + +

      请输入 refresh_token 值

      +
      + + POST +
      + 复用refresh_token: +
      +
      +
      + +
      +
      Test OIDC-Logout OAuth2.1新增
      +
      +

      将'spring-oauth-server'退出并重定向会指定的uri(添加client端时的字段logout_redirect_uris), 由client端通过浏览器发起调用.

      + +
      + + + + + + + + + + + + + + + + + +
      client_id + +
      id_token_hint + +

      填写一个已经签发且有效的id_token

      +
      post_logout_redirect_uri + +

      退出后通过post重定向的uri

      +
      state + +

      每次随机生成, spring-oauth-server原封不动返回(防止会话劫持攻击)

      +
      + + POST + +
      +
      +
      + +
      + Back +
      +
      +
      + + + +
      + + \ 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. +

      +
      +
      +
      +
      +
      + + + + +
      + + +

      +
      + +

      + You have already granted the following permissions to the above app: +

      +
      + + +

      +
      + +
      + +
      + +
      +
      +
      +
      +
      +

      + + 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 + + + +
      +
      +

      Device Login

      +
      +
      +
      +
      +
      + + +

      Please type device user code

      +
      + +
      + +
      +
      + Cancel +
      +
      +
      +
      +
      +
      + + 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: + +
      + +
      +
      +
      +
      + 操作说明 +
        +
      1. +

        + 菜单 User 是不需要OAuth 验证即可访问的(即公开的resource); 用于管理用户信息(添加,删除等). +

        +
      2. +
      3. +

        + 菜单 Unity 与 Mobile 需要登录认证后才能访问(即受保护的resource);
        + Unity 需要 [ROLE_UNITY] 权限, Mobile 需要 [ROLE_MOBILE] 权限. +

        +
      4. +
      5. +

        + device_login 用于在设备认证时,输入用户码(user_code)完成授权. +

        +
      6. +
      7. +

        + 在使用之前, 建议先了解OAuth2.1支持的grant_type, 请访问 https://andaily.com/blog/?p=103 +

        +
      8. +
      9. +

        + 在项目的 others目录里有 oauth2.1-flow.md文件, 里面有测试的URL地址(包括浏览器与客户端的),
        + 若想访问 Unity 与 Mobile, 则先用基于浏览器的测试URL 访问,等验证通过后即可访问(注意不同的账号对应的权限). +

        +
      10. +
      11. +

        + 若需要自定义client_details数据并进行测试, + 可进入client_details去手动添加client_details或删除已创建的client_details. +

        +
      12. +
      +
      +
      +菜单 +
        +
      • +

        + 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 + + - - + +
      -
      +
      [U+P] Login
      -
      - +
      @@ -45,11 +43,11 @@
      - <%--Login error--%> - Access denied !!! - Authentication Failure + + Access denied !!! + Authentication Failure
      @@ -57,9 +55,22 @@
      +
      +
      .well-known endpoint
      + +
      -

      你可以使用以下几个初始的账号进行登录:

      +

      可以使用以下几个初始的账号进行登录:

      @@ -71,18 +82,18 @@ - + - + + authorization_code,refresh_token,device_code - + @@ -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

      - +
      - +

      Username, unique.

      @@ -32,10 +30,10 @@
      - + -

      Password, unique.

      +

      Password, length >= 10 .

      @@ -43,21 +41,36 @@

      Select Privilege(s).

      +
      + + +
      + + + +

      Enable/Disable the user

      +
      +
      +
      - +

      User phone, optional.

      @@ -66,16 +79,34 @@
      - +

      User email, optional.

      +
      + + +
      + + +

      User nickname, optional.

      +
      +
      +
      + + +
      + + +

      User address, optional.

      +
      +
      - +
      @@ -87,7 +118,9 @@ Cancel
      - + + +
      \ 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

      + th:value="${overviewDto.username}"/>
      -  Total: ${overviewDto.size} +  Total: [[${overviewDto.size}]]
      adminadminAdmin@2013 All privileges, allow visit [Mobile] and [Unity] resources, manage user
      unityunityUnity#2013 Only allow visit [Unity] resource, support grant_type: - authorization_code,refresh_token,implicit
      mobilemobileMobile*2013 Only allow visit [Mobile] resource, support grant_type: password,refresh_token
      @@ -32,22 +32,28 @@ + + + - - - - - - - - - + + + + + + + + + +
      Username PrivilegeEnabled Phone EmailNicknameAddress 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 - -
      -
      -

      client_details

      -
      -
      - -
      -
      - -
      - -
      -
        - -
      • -
        - - test - archive - - Archived -
        -

        - ${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实际上是向该表中按不同的条件添加数据. -

      - -
      - -
      - - -
      - - -

      client_id必须输入,且必须唯一,长度至少5位; 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.

      -
      -
      -
      - - -
      - - -

      client_secret必须输入,且长度至少8位; 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念. -
      - 注意: 由于client_secret 会加密存储, 请先复制并保留client_secret值 -

      -
      -
      -
      - - -
      - - sos-resource - - -

      resourceIds必须选择; 可选值必须来源于与OAuth2ServerConfiguration.java中固定值 -

      -
      -
      - -
      - - -
      - - read - write - read,write - - -

      scope必须选择

      -
      -
      - -
      - - -
      - - - - - - -

      至少勾选一项grant_type(s), 且不能只单独勾选refresh_token

      -
      -
      - -
      - - -
      - - -

      grant_type包括authorization_codeimplicit, - 则必须输入redirect_uri

      -
      -
      - -
      - - -
      - - - ROLE_UNITY - ROLE_MOBILE - ROLE_UNITY,ROLE_MOBILE - - -

      指定客户端所拥有的Spring Security的权限值,可选; - 若grant_typeimplicitclient_credentials则必须选择authorities, - 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API

      -
      -
      - -
      -
      - -
      - -
      -
      - - -
      - - -

      设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时); - 若设定则必须是大于0的整数值

      -
      -
      - -
      - - -
      - - -

      设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, - 30天); - 若设定则必须是大于0的整数值

      -
      -
      - -
      - - -
      - - -

      - 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如: - {"country":"CN","country_code":"086"} -

      -
      -
      - - -
      - - -
      - - - -

      该属性是扩展的, - 只适用于grant_type(s)包括authorization_code的情况,当用户登录成功后,若选No,则会跳转到让用户Approve的页面让用户同意授权, - 若选Yes,则在登录后不需要再让用户Approve同意授权(因为是受信任的)

      -
      -
      -
      - - -
      -
      -
      - -
      -
      - - -
      -
      -
      - - 取消 -
      -
      -
      -
      -
      - - - - \ 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]
      -
      -

      输入每一步必要的信息后点击其下面的链接地址.

      -
        -
      1. -

        - 从 spring-oauth-server获取 'code' -
        - redirect_uri: -
        - - /oauth/authorize?client_id={{clientId}}&redirect_uri={{redirectUri}}&response_type=code&scope={{scope}} - GET -

        -
      2. -
      3. - 用 'code' 换取 'access_token' -
        - 输入第一步获取的code: -
        - -
        - <%-- - <%--target="_blank">/oauth/token?client_id={{clientId}}&client_secret={{clientSecret}}&grant_type=authorization_code&code={{code}}&redirect_uri={{redirectUri}}--%> - - POST -
        -
      4. -
      -
      -
      -
      - - -
      -
      Test [password]
      -
      -

      输入username, password 后点击链接地址.

      - username: -
      - password: - -
      - -
      - <%-- - <%--target="_blank">/oauth/token?client_id={{clientId}}&client_secret={{clientSecret}}&grant_type=password&scope={{scope}}&username={{username}}&password={{password}}--%> - - POST -
      -
      -
      -
      - - -
      -
      Test [implicit]
      -
      -

      输入redirect_uri 后点击链接地址. 获取access_token后注意查看redirect_uri的hash部分(#号后边部分)

      - redirect_uri: - -

      - /oauth/authorize?client_id={{clientId}}&client_secret={{clientSecret}}&response_type=token&scope={{scope}}&redirect_uri={{implicitRedirectUri}} - GET -

      -
      -
      -
      - - -
      -
      Test [client_credentials]
      -
      -

      点击链接地址即可测试

      - - -
      - <%-- - <%--target="_blank">/oauth/token?client_id={{clientId}}&client_secret={{clientSecret}}&grant_type=client_credentials&scope={{scope}}--%> - - POST -
      -
      -
      -
      - - -
      -
      Test [refresh_token]
      -
      -

      输入refresh_token 后点击链接地址.

      - refresh_token: - -
      - -
      - <%-- - <%--target="_blank">/oauth/token?client_id={{clientId}}&client_secret={{clientSecret}}&grant_type=refresh_token&refresh_token={{refreshToken}}--%> - - POST -
      -
      -
      -
      - -
      - Back -
      -
      -
      - - - - \ 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" %> - - - - - - - - - - - - <sitemesh:write property='title'/> - Spring Security&OAuth2 - - - <%----%> - - - - -
      -
      - <%----%> - -
      -
      -
      -

      - © 2013 - 2022 spring-oauth-server. - v${mainVersion} -

      -
      -
      - - \ 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} -
      - - - -
      -
      -
      - 操作说明: -
        -
      1. -

        - 菜单 User 是不需要OAuth 验证即可访问的(即公开的resource); 用于管理用户信息(添加,删除等). -

        -
      2. -
      3. -

        - 菜单 Unity 与 Mobile 需要OAuth 验证后才能访问(即受保护的resource);
        - Unity 需要 [ROLE_UNITY] 权限(resourceId: - unity-resource - ), Mobile 需要 [ROLE_MOBILE] 权限(resourceId: - mobile-resource - ). -

        -
      4. -
      5. -

        - 在使用之前, 建议先了解OAuth2支持的5类grant_type, 请访问 http://andaily.com/blog/?p=103 -

        -
      6. -
      7. -

        - 在项目的 others目录里有 oauth_test.txt文件, 里面有测试的URL地址(包括浏览器与客户端的),
        - 若想访问 Unity 与 Mobile, 则先用基于浏览器的测试URL 访问,等验证通过后即可访问(注意不同的账号对应的权限). -

        -
      8. -
      9. -

        - 若需要自定义client_details数据并进行测试, 可进入client_details去手动添加client_details或删除已创建的client_details. -

        -
      10. -
      -
      -
      -菜单 -
        -
      • -

        - API - 查看提供的API文档 -

        -
      • -
      • -

        - client_details - 管理ClientDetails -

        -
      • - -
      • -

        - User - 管理User -

        -
      • -
        -
      • -

        - Unity - Unity 资源(resource), 需要具有 [ROLE_UNITY] 权限(resourceId: - unity-resource才能访问 -

        -
      • -
      • -

        - Mobile - Mobile资源(resource), 需要具有 [ROLE_MOBILE] 权限(resourceId: - mobile-resource才能访问 -

        -
      • -
      -
      - -
      -

      - 说明: Unity与Mobile菜单需要先获取到access_token后才能正常访问; 可以尝试在URL后面任意添加access_token参数值试试效果, -
      - 如: ${contextPath}/m/dashboard?access_token=i_am_testing_access_token -

      - -

      - 请求受保护的资源时传递 - Access Token - 有两种方式, 方式一在URL参数中添加access_token, 方式二在请求的Header中添加 Authorization, 其值为 Bearer - your_access_token -

      -
      - - \ 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