Browse Source

inital commit

fengxici 10 months ago
commit
56bdcddd6c
100 changed files with 4050 additions and 0 deletions
  1. 10 0
      .idea/.gitignore
  2. 33 0
      .idea/compiler.xml
  3. 17 0
      .idea/encodings.xml
  4. 20 0
      .idea/jarRepositories.xml
  5. 14 0
      .idea/misc.xml
  6. 124 0
      .idea/uiDesigner.xml
  7. 81 0
      pom.xml
  8. 72 0
      security-auth/pom.xml
  9. 1 0
      security-auth/src/main/java/timing/ukulele/auth/AuthorizationApplication.java
  10. 424 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/AuthorizationConfig.java
  11. 4 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/AuthorizationUtil.java
  12. 47 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/JwtAuthorizationToken.java
  13. 60 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationConverter.java
  14. 100 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationProvider.java
  15. 31 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationToken.java
  16. 65 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/filter/JWTAuthorizationFilter.java
  17. 66 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/handler/ConsentAuthenticationFailureHandler.java
  18. 92 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/handler/ConsentAuthorizationResponseHandler.java
  19. 35 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/handler/DeviceAuthorizationResponseHandler.java
  20. 85 0
      security-auth/src/main/java/timing/ukulele/auth/authorization/handler/LoginTargetAuthenticationEntryPoint.java
  21. 117 0
      security-auth/src/main/java/timing/ukulele/auth/config/BeanConfig.java
  22. 79 0
      security-auth/src/main/java/timing/ukulele/auth/config/RedisConfig.java
  23. 87 0
      security-auth/src/main/java/timing/ukulele/auth/config/SecurityConfig.java
  24. 58 0
      security-auth/src/main/java/timing/ukulele/auth/config/property/TimingSecurityProperties.java
  25. 33 0
      security-auth/src/main/java/timing/ukulele/auth/constant/RedisConstants.java
  26. 104 0
      security-auth/src/main/java/timing/ukulele/auth/constant/SecurityConstants.java
  27. 198 0
      security-auth/src/main/java/timing/ukulele/auth/controller/AuthorizationController.java
  28. 44 0
      security-auth/src/main/java/timing/ukulele/auth/controller/CommonController.java
  29. 36 0
      security-auth/src/main/java/timing/ukulele/auth/controller/TestController.java
  30. 31 0
      security-auth/src/main/java/timing/ukulele/auth/http/ExchangeBeanConfig.java
  31. 21 0
      security-auth/src/main/java/timing/ukulele/auth/http/wechat/WeChatExchange.java
  32. 30 0
      security-auth/src/main/java/timing/ukulele/auth/model/CaptchaResult.java
  33. 110 0
      security-auth/src/main/java/timing/ukulele/auth/model/Result.java
  34. 44 0
      security-auth/src/main/java/timing/ukulele/auth/security/LoginTypeEnum.java
  35. 41 0
      security-auth/src/main/java/timing/ukulele/auth/security/SecurityUtils.java
  36. 42 0
      security-auth/src/main/java/timing/ukulele/auth/security/ThirdTypeEnum.java
  37. 63 0
      security-auth/src/main/java/timing/ukulele/auth/security/filter/JWTSecurityFilter.java
  38. 33 0
      security-auth/src/main/java/timing/ukulele/auth/security/handler/LoginFailureHandler.java
  39. 61 0
      security-auth/src/main/java/timing/ukulele/auth/security/handler/LoginSuccessHandler.java
  40. 24 0
      security-auth/src/main/java/timing/ukulele/auth/security/model/CustomGrantedAuthority.java
  41. 60 0
      security-auth/src/main/java/timing/ukulele/auth/security/model/SupplierDeferredSecurityContext.java
  42. 35 0
      security-auth/src/main/java/timing/ukulele/auth/security/service/TimingUserDetailService.java
  43. 75 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationFilter.java
  44. 48 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationProvider.java
  45. 50 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationSecurityConfig.java
  46. 47 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationToken.java
  47. 56 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationFilter.java
  48. 38 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationProvider.java
  49. 47 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationSecurityConfig.java
  50. 45 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationToken.java
  51. 59 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationFilter.java
  52. 40 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationProvider.java
  53. 47 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationSecurityConfig.java
  54. 60 0
      security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationToken.java
  55. 323 0
      security-auth/src/main/java/timing/ukulele/auth/support/RedisOperator.java
  56. 104 0
      security-auth/src/main/java/timing/ukulele/auth/support/RedisSessionSecurityContextRepository.java
  57. 167 0
      security-auth/src/main/java/timing/ukulele/auth/util/JsonUtils.java
  58. 32 0
      security-auth/src/main/resources/application.yml
  59. 48 0
      security-auth/target/classes/META-INF/spring-configuration-metadata.json
  60. 32 0
      security-auth/target/classes/application.yml
  61. BIN
      security-auth/target/classes/timing/ukulele/auth/AuthorizationApplication.class
  62. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/AuthorizationConfig.class
  63. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/AuthorizationUtil.class
  64. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/JwtAuthorizationToken.class
  65. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationConverter.class
  66. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationProvider.class
  67. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationToken.class
  68. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/filter/JWTAuthorizationFilter.class
  69. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/handler/ConsentAuthenticationFailureHandler.class
  70. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/handler/ConsentAuthorizationResponseHandler.class
  71. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/handler/DeviceAuthorizationResponseHandler.class
  72. BIN
      security-auth/target/classes/timing/ukulele/auth/authorization/handler/LoginTargetAuthenticationEntryPoint.class
  73. BIN
      security-auth/target/classes/timing/ukulele/auth/config/BeanConfig.class
  74. BIN
      security-auth/target/classes/timing/ukulele/auth/config/RedisConfig.class
  75. BIN
      security-auth/target/classes/timing/ukulele/auth/config/SecurityConfig.class
  76. BIN
      security-auth/target/classes/timing/ukulele/auth/config/property/TimingSecurityProperties.class
  77. BIN
      security-auth/target/classes/timing/ukulele/auth/constant/RedisConstants.class
  78. BIN
      security-auth/target/classes/timing/ukulele/auth/constant/SecurityConstants.class
  79. BIN
      security-auth/target/classes/timing/ukulele/auth/controller/AuthorizationController$ScopeWithDescription.class
  80. BIN
      security-auth/target/classes/timing/ukulele/auth/controller/AuthorizationController.class
  81. BIN
      security-auth/target/classes/timing/ukulele/auth/controller/CommonController.class
  82. BIN
      security-auth/target/classes/timing/ukulele/auth/controller/TestController.class
  83. BIN
      security-auth/target/classes/timing/ukulele/auth/http/ExchangeBeanConfig.class
  84. BIN
      security-auth/target/classes/timing/ukulele/auth/http/wechat/WeChatExchange.class
  85. BIN
      security-auth/target/classes/timing/ukulele/auth/model/CaptchaResult.class
  86. BIN
      security-auth/target/classes/timing/ukulele/auth/model/Result.class
  87. BIN
      security-auth/target/classes/timing/ukulele/auth/security/LoginTypeEnum.class
  88. BIN
      security-auth/target/classes/timing/ukulele/auth/security/SecurityUtils.class
  89. BIN
      security-auth/target/classes/timing/ukulele/auth/security/ThirdTypeEnum.class
  90. BIN
      security-auth/target/classes/timing/ukulele/auth/security/filter/JWTSecurityFilter.class
  91. BIN
      security-auth/target/classes/timing/ukulele/auth/security/handler/LoginFailureHandler.class
  92. BIN
      security-auth/target/classes/timing/ukulele/auth/security/handler/LoginSuccessHandler.class
  93. BIN
      security-auth/target/classes/timing/ukulele/auth/security/model/CustomGrantedAuthority.class
  94. BIN
      security-auth/target/classes/timing/ukulele/auth/security/model/SupplierDeferredSecurityContext.class
  95. BIN
      security-auth/target/classes/timing/ukulele/auth/security/service/TimingUserDetailService.class
  96. BIN
      security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationFilter.class
  97. BIN
      security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationProvider.class
  98. BIN
      security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationSecurityConfig.class
  99. BIN
      security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationToken.class
  100. 0 0
      security-auth/target/classes/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationFilter.class

+ 10 - 0
.idea/.gitignore

@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Zeppelin ignored files
+/ZeppelinRemoteNotebooks/

+ 33 - 0
.idea/compiler.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <annotationProcessing>
+      <profile default="true" name="Default" enabled="true" />
+      <profile name="Maven default annotation processors profile" enabled="true">
+        <sourceOutputDir name="target/generated-sources/annotations" />
+        <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
+        <outputRelativeToContentRoot value="true" />
+        <module name="security-gateway" />
+        <module name="security-auth" />
+        <module name="security-resource-webmvc" />
+        <module name="security-resource-webflux" />
+      </profile>
+    </annotationProcessing>
+    <bytecodeTargetLevel>
+      <module name="auth" target="17" />
+      <module name="auth2" target="17" />
+      <module name="ukulele-cloud-boot3-nacos" target="17" />
+    </bytecodeTargetLevel>
+  </component>
+  <component name="JavacSettings">
+    <option name="ADDITIONAL_OPTIONS_OVERRIDE">
+      <module name="auth" options="-parameters" />
+      <module name="auth2" options="-parameters" />
+      <module name="security-auth" options="-parameters" />
+      <module name="security-gateway" options="-parameters" />
+      <module name="security-resource-webflux" options="-parameters" />
+      <module name="security-resource-webmvc" options="-parameters" />
+      <module name="spring-security-oauth2" options="" />
+    </option>
+  </component>
+</project>

+ 17 - 0
.idea/encodings.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/auth/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/auth/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/auth2/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-auth/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-gateway/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-gateway/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-resource-webflux/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-resource-webflux/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-resource-webmvc/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/security-resource-webmvc/src/main/resources" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>

+ 20 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Central Repository" />
+      <option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+  </component>
+</project>

+ 14 - 0
.idea/misc.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>

+ 124 - 0
.idea/uiDesigner.xml

@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Palette2">
+    <group name="Swing">
+      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
+      </item>
+      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
+      </item>
+      <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
+        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
+        <initial-values>
+          <property name="text" value="Button" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="RadioButton" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="CheckBox" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
+        <initial-values>
+          <property name="text" value="Label" />
+        </initial-values>
+      </item>
+      <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
+          <preferred-size width="150" height="-1" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
+          <preferred-size width="150" height="50" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+          <preferred-size width="200" height="200" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
+          <preferred-size width="200" height="200" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
+      </item>
+      <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
+      </item>
+      <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
+      </item>
+      <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
+          <preferred-size width="-1" height="20" />
+        </default-constraints>
+      </item>
+      <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
+        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
+      </item>
+      <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
+        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
+      </item>
+    </group>
+  </component>
+</project>

+ 81 - 0
pom.xml

@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>timing.ukulele</groupId>
+        <artifactId>ukulele-boot-starter</artifactId>
+        <version>3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>spring-security-oauth2</artifactId>
+    <version>3.0-SNAPSHOT</version>
+
+    <name>spring-security-oauth2</name>
+    <modules>
+        <module>security-auth</module>
+        <module>security-gateway</module>
+        <module>security-resource-webmvc</module>
+        <module>security-resource-webflux</module>
+    </modules>
+    <packaging>pom</packaging>
+    <url>http://timingtech.top</url>
+
+    <properties>
+        <java.version>17</java.version>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <!-- Spring Cloud版本号 -->
+        <spring-cloud.version>2023.0.0</spring-cloud.version>
+
+        <ukulele-version>3.0-SNAPSHOT</ukulele-version>
+        <facade-version>3.0-SNAPSHOT</facade-version>
+        <data-version>3.0-SNAPSHOT</data-version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>timing.ukulele</groupId>
+                <artifactId>ukulele-common</artifactId>
+                <version>${ukulele-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>timing.ukulele</groupId>
+                <artifactId>ukulele-redisson</artifactId>
+                <version>${ukulele-version}</version>
+            </dependency>
+            <dependency>
+                <groupId>timing.ukulele</groupId>
+                <artifactId>ukulele-persistence</artifactId>
+                <version>${ukulele-version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-jar-plugin</artifactId>
+                    <version>3.3.0</version>
+                </plugin>
+                <plugin>
+                    <!--主要使用的是maven提供的assembly插件完成-->
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-assembly-plugin</artifactId>
+                    <version>3.6.0</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+</project>

+ 72 - 0
security-auth/pom.xml

@@ -0,0 +1,72 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>timing.ukulele</groupId>
+        <artifactId>spring-security-oauth2</artifactId>
+        <version>3.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>security-auth</artifactId>
+    <packaging>jar</packaging>
+
+    <name>security-auth</name>
+    <url>http://timingtech.top</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.nimbusds</groupId>
+                    <artifactId>nimbus-jose-jwt</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!--  统一版本   -->
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
+            <version>9.37.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-captcha</artifactId>
+            <version>5.8.18</version>
+        </dependency>
+    </dependencies>
+</project>

+ 1 - 0
security-auth/src/main/java/timing/ukulele/auth/AuthorizationApplication.java

@@ -0,0 +1 @@
+package timing.ukulele.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 认证服务启动类
 */
@SpringBootApplication
public class AuthorizationApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthorizationApplication.class, args);
    }

}

+ 424 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/AuthorizationConfig.java

@@ -0,0 +1,424 @@
+package timing.ukulele.auth.authorization;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import lombok.SneakyThrows;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
+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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.header.HeaderWriterFilter;
+import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
+import org.springframework.util.ObjectUtils;
+import timing.ukulele.auth.authorization.device.DeviceClientAuthenticationConverter;
+import timing.ukulele.auth.authorization.device.DeviceClientAuthenticationProvider;
+import timing.ukulele.auth.authorization.filter.JWTAuthorizationFilter;
+import timing.ukulele.auth.authorization.handler.ConsentAuthenticationFailureHandler;
+import timing.ukulele.auth.authorization.handler.ConsentAuthorizationResponseHandler;
+import timing.ukulele.auth.authorization.handler.DeviceAuthorizationResponseHandler;
+import timing.ukulele.auth.authorization.handler.LoginTargetAuthenticationEntryPoint;
+import timing.ukulele.auth.config.property.TimingSecurityProperties;
+import timing.ukulele.auth.constant.RedisConstants;
+import timing.ukulele.auth.constant.SecurityConstants;
+import timing.ukulele.auth.support.RedisOperator;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Duration;
+import java.util.UUID;
+
+@Configuration
+public class AuthorizationConfig {
+
+    private final RedisOperator<String> redisOperator;
+    private final TimingSecurityProperties securityProperties;
+
+    public AuthorizationConfig(
+            RedisOperator<String> redisOperator,
+            TimingSecurityProperties securityProperties) {
+        this.redisOperator = redisOperator;
+        this.securityProperties = securityProperties;
+    }
+
+    /**
+     * 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key
+     *
+     * @return jwt解析器 JwtAuthenticationConverter
+     */
+    @Bean
+    public JwtAuthenticationConverter jwtAuthenticationConverter() {
+        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
+        // 设置解析权限信息的前缀,设置为空是去掉前缀
+        grantedAuthoritiesConverter.setAuthorityPrefix("");
+        // 设置权限信息在jwt claims中的key
+        grantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_KEY);
+
+        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
+
+        return jwtAuthenticationConverter;
+    }
+
+
+    /**
+     * 将AuthenticationManager注入ioc中,其它需要使用地方可以直接从ioc中获取
+     *
+     * @param authenticationConfiguration 导出认证配置
+     * @return AuthenticationManager 认证管理器
+     */
+    @Bean
+    @SneakyThrows
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) {
+        return authenticationConfiguration.getAuthenticationManager();
+    }
+
+    /**
+     * 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
+     *
+     * @return JWKSource
+     */
+    @Bean
+    @SneakyThrows
+    public JWKSource<SecurityContext> jwkSource() {
+        // 先从redis获取
+        String jwkSetCache = redisOperator.get(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY);
+        if (ObjectUtils.isEmpty(jwkSetCache)) {
+            KeyPair keyPair = generateRsaKey();
+            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+            RSAKey rsaKey = new RSAKey.Builder(publicKey)
+                    .privateKey(privateKey)
+                    .keyID(UUID.randomUUID().toString())
+                    .build();
+            // 生成jws
+            JWKSet jwkSet = new JWKSet(rsaKey);
+            // 转为json字符串
+            String jwkSetString = jwkSet.toString(Boolean.FALSE);
+            // 存入redis
+            redisOperator.set(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY, jwkSetString);
+            return new ImmutableJWKSet<>(jwkSet);
+        }
+        // 解析存储的jws
+        JWKSet jwkSet = JWKSet.parse(jwkSetCache);
+        return new ImmutableJWKSet<>(jwkSet);
+    }
+
+    /**
+     * 生成rsa密钥对,提供给jwk
+     *
+     * @return 密钥对
+     */
+    private static KeyPair generateRsaKey() {
+        KeyPair keyPair;
+        try {
+            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+            keyPairGenerator.initialize(2048);
+            keyPair = keyPairGenerator.generateKeyPair();
+        } catch (Exception ex) {
+            throw new IllegalStateException(ex);
+        }
+        return keyPair;
+    }
+
+    /**
+     * 配置jwt解析器
+     *
+     * @param jwkSource jwk源
+     * @return JwtDecoder
+     */
+    @Bean
+    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
+        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
+    }
+
+    @Bean
+    JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
+        return new NimbusJwtEncoder(jwkSource);
+    }
+
+    /**
+     * 添加认证服务器配置,设置jwt签发者、默认端点请求地址等
+     *
+     * @return AuthorizationServerSettings
+     */
+    @Bean
+    public AuthorizationServerSettings authorizationServerSettings() {
+        // 注意!!!!此处的Issuer很重要,资源服务里的配置要与此一致
+        return AuthorizationServerSettings.builder().issuer(securityProperties.getIssuerUrl()).build();
+    }
+
+    /**
+     * 配置端点的过滤器链
+     *
+     * @param http spring security核心配置类
+     * @return 过滤器链
+     * @throws Exception 抛出
+     */
+    @Bean
+    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
+                                                                      JwtDecoder jwtDecoder) throws Exception {
+        // 配置默认的设置,忽略认证端点的csrf校验
+        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
+                // 开启OpenID Connect 1.0协议相关端点
+                .oidc(Customizer.withDefaults())
+                // 设置自定义用户确认授权页
+                .authorizationEndpoint(authorizationEndpoint -> {
+//                            // 校验授权确认页面是否为完整路径;是否是前后端分离的页面
+//                            boolean absoluteUrl = UrlUtils.isAbsoluteUrl(securityProperties.getConsentPageUri());
+//                            // 如果是分离页面则重定向,否则转发请求
+                            authorizationEndpoint.consentPage(securityProperties.getConsentPageUri());
+//                            if (absoluteUrl) {
+                            // 适配前后端分离的授权确认页面,成功/失败响应json
+                            authorizationEndpoint.errorResponseHandler(new ConsentAuthenticationFailureHandler(securityProperties.getConsentPageUri()));
+                            authorizationEndpoint.authorizationResponseHandler(new ConsentAuthorizationResponseHandler(securityProperties.getConsentPageUri()));
+//                            }
+                        }
+                );
+
+        // 设备码配置
+//        applyDeviceSecurity(http, registeredClientRepository, authorizationServerSettings);
+
+        http
+                // 当未登录时访问认证端点时重定向至login页面
+                .exceptionHandling((exceptions) -> exceptions
+                                .defaultAuthenticationEntryPointFor(
+                                        new LoginTargetAuthenticationEntryPoint(securityProperties.getLoginUrl(), securityProperties.getDeviceActivateUri()),
+                                        new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
+                                )
+//                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
+//                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
+                );
+        // 处理使用access token访问用户信息端点和客户端注册端点
+//                .oauth2ResourceServer((resourceServer) -> resourceServer
+//                        .jwt(Customizer.withDefaults()));
+        JWTAuthorizationFilter jwtFilter = new JWTAuthorizationFilter(jwtDecoder);
+//        http.addFilterBefore(jwtFilter, OAuth2AuthorizationEndpointFilter.class);
+//        http.addFilterAfter(jwtFilter, SecurityContextHolderFilter.class);
+        http.addFilterAfter(jwtFilter, HeaderWriterFilter.class);
+
+//        http.sessionManagement(AbstractHttpConfigurer::disable);
+        http.sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+        return http.build();
+    }
+
+    private void applyDeviceSecurity(HttpSecurity http,
+                                     RegisteredClientRepository registeredClientRepository,
+                                     AuthorizationServerSettings authorizationServerSettings) {
+        // 新建设备码converter和provider
+        DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
+                new DeviceClientAuthenticationConverter(
+                        authorizationServerSettings.getDeviceAuthorizationEndpoint());
+        DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
+                new DeviceClientAuthenticationProvider(registeredClientRepository);
+
+        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
+                // 设置设备码用户验证url(自定义用户验证页)
+                .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> {
+//                            deviceAuthorizationEndpoint.deviceAuthorizationRequestConverter(deviceClientAuthenticationConverter);
+                            deviceAuthorizationEndpoint.verificationUri(securityProperties.getDeviceActivateUri());
+                        }
+                )
+                // 设置验证设备码用户确认页面
+                .deviceVerificationEndpoint(deviceVerificationEndpoint -> {
+                            // 如果是分离页面则重定向,否则转发请求
+                            deviceVerificationEndpoint.consentPage(securityProperties.getConsentPageUri());
+                            deviceVerificationEndpoint.errorResponseHandler(new ConsentAuthenticationFailureHandler(securityProperties.getConsentPageUri()));
+                            // 添加响应json处理
+                            deviceVerificationEndpoint.deviceVerificationResponseHandler(new DeviceAuthorizationResponseHandler(securityProperties.getDeviceActivatedUri()));
+                            deviceVerificationEndpoint.authenticationProvider(deviceClientAuthenticationProvider);
+                            deviceVerificationEndpoint.deviceVerificationRequestConverter(deviceClientAuthenticationConverter);
+                        }
+                );
+//                .clientAuthentication(clientAuthentication ->
+//                        // 客户端认证添加设备码的converter和provider
+//                        clientAuthentication
+//                                .authenticationConverter(deviceClientAuthenticationConverter)
+//                                .authenticationProvider(deviceClientAuthenticationProvider)
+//                );
+    }
+
+    /**
+     * 配置客户端Repository
+     *
+     * @param passwordEncoder 密码解析器
+     * @return 基于数据库的repository
+     */
+    @Bean
+    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
+        // 默认需要授权确认
+        ClientSettings.Builder builder = ClientSettings.builder()
+                .requireAuthorizationConsent(Boolean.TRUE);
+        TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder()
+                // 自包含token(jwt)
+                .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
+                // Access Token 存活时间:2小时
+                .accessTokenTimeToLive(Duration.ofHours(2L))
+                // 授权码存活时间:5分钟
+                .authorizationCodeTimeToLive(Duration.ofMinutes(5L))
+                // 设备码存活时间:5分钟
+                .deviceCodeTimeToLive(Duration.ofMinutes(5L))
+                // Refresh Token 存活时间:7天
+                .refreshTokenTimeToLive(Duration.ofDays(7L))
+                // 刷新 Access Token 后是否重用 Refresh Token
+                .reuseRefreshTokens(Boolean.TRUE)
+                // 设置 Id Token 加密方式
+                .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);
+        RegisteredClient registeredClient = RegisteredClient.withId("messaging-client")
+                // 客户端id
+                .clientId("messaging-client")
+                // 客户端名称
+                .clientName("授权码")
+                // 客户端秘钥,使用密码解析器加密
+                .clientSecret(passwordEncoder.encode("123456"))
+                // 客户端认证方式,基于请求头的认证
+                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+                // 配置资源服务器使用该客户端获取授权时支持的方式
+                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
+                .redirectUri("http://www.test.com:5173/OAuth2Redirect")
+                .redirectUri("http://www.test.com:8000/login/oauth2/code/messaging-client-oidc")
+                // 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
+                .scope(OidcScopes.OPENID)
+                .scope(OidcScopes.PROFILE)
+                // 指定scope
+                .scope("message.read")
+                .scope("message.write")
+                // 客户端设置,设置用户需要确认授权
+                .clientSettings(builder.build())
+                // token相关配置
+                .tokenSettings(tokenSettingsBuilder.build())
+                .build();
+
+        // 正常授权码客户端
+        RegisteredClient opaqueClient = RegisteredClient.withId("opaque-client")
+                // 客户端id
+                .clientId("opaque-client")
+                // 客户端名称
+                .clientName("匿名token")
+                // 客户端秘钥,使用密码解析器加密
+                .clientSecret(passwordEncoder.encode("123456"))
+                // 客户端认证方式,基于请求头的认证
+                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+                // 配置资源服务器使用该客户端获取授权时支持的方式
+                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
+                .redirectUri("http://www.test.com:5173/OAuth2Redirect")
+                // 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
+                .scope(OidcScopes.OPENID)
+                .scope(OidcScopes.PROFILE)
+                // 指定scope
+                .scope("message.read")
+                .scope("message.write")
+                // 客户端设置,设置用户需要确认授权
+                .clientSettings(builder.build())
+                // token相关配置, 设置token为匿名token(opaque token)
+                .tokenSettings(tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
+                .build();
+
+        // 设备码授权客户端
+        RegisteredClient deviceClient = RegisteredClient.withId("device-message-client")
+                .clientId("device-message-client")
+                .clientName("普通公共客户端")
+                // 公共客户端
+                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
+                // 设备码授权
+                .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
+                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+                // 指定scope
+                .scope("message.read")
+                .scope("message.write")
+                // token相关配置
+                .tokenSettings(tokenSettingsBuilder.build())
+                .build();
+
+        // PKCE客户端
+        RegisteredClient pkceClient = RegisteredClient.withId("pkce-message-client")
+                .clientId("pkce-message-client")
+                .clientName("PKCE流程")
+                // 公共客户端
+                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
+                // 设备码授权
+                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+                // 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
+                .redirectUri("http://k7fsqkhtbx.cdhttp.cn/PkceRedirect")
+                .redirectUri("http://www.test.com:5173/PkceRedirect")
+                // 开启 PKCE 流程
+                .clientSettings(builder.requireProofKey(Boolean.TRUE).build())
+                // 指定scope
+                .scope("message.read")
+                .scope("message.write")
+                // token相关配置
+                .tokenSettings(tokenSettingsBuilder.build())
+                .build();
+
+        // 基于db存储客户端,还有一个基于内存的实现 JdbcRegisteredClientRepository
+        InMemoryRegisteredClientRepository registeredClientRepository = new InMemoryRegisteredClientRepository(registeredClient, deviceClient, opaqueClient, pkceClient);
+
+        return registeredClientRepository;
+    }
+
+    /**
+     * oauth2的授权管理服务
+     *
+     * @param registeredClientRepository 上边注入的客户端repository
+     * @return JdbcOAuth2AuthorizationService
+     */
+    @Bean
+    public OAuth2AuthorizationService authorizationService(RegisteredClientRepository registeredClientRepository) {
+        // 基于db的oauth2认证服务,JdbcOAuth2AuthorizationService InMemoryOAuth2AuthorizationService
+        return new InMemoryOAuth2AuthorizationService();
+    }
+
+    /**
+     * 配置基于db的授权确认管理服务
+     *
+     * @param registeredClientRepository 客户端repository
+     * @return JdbcOAuth2AuthorizationConsentService
+     */
+    @Bean
+    public OAuth2AuthorizationConsentService authorizationConsentService(RegisteredClientRepository registeredClientRepository) {
+        // 基于db的授权确认管理服务,还有一个基于内存的服务实现:InMemoryOAuth2AuthorizationConsentService JdbcOAuth2AuthorizationConsentService
+        return new InMemoryOAuth2AuthorizationConsentService();
+    }
+
+}

+ 4 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/AuthorizationUtil.java

@@ -0,0 +1,4 @@
+package timing.ukulele.auth.authorization;
+
+public class AuthorizationUtil {
+}

+ 47 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/JwtAuthorizationToken.java

@@ -0,0 +1,47 @@
+package timing.ukulele.auth.authorization;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serial;
+import java.util.Collection;
+
+public class JwtAuthorizationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 500L;
+    private final Object principal;
+    private Object credentials;
+
+    public JwtAuthorizationToken(Object principal, Object credentials) {
+        super(null);
+        this.principal = principal;
+        this.credentials = credentials;
+        this.setAuthenticated(false);
+    }
+
+    public JwtAuthorizationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        super.setAuthenticated(true);
+    }
+
+    public Object getCredentials() {
+        return credentials;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+        if (isAuthenticated) {
+            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+        } else {
+            super.setAuthenticated(false);
+        }
+    }
+
+    public void eraseCredentials() {
+        super.eraseCredentials();
+    }
+}

+ 60 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationConverter.java

@@ -0,0 +1,60 @@
+package timing.ukulele.auth.authorization.device;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.StringUtils;
+
+/**
+ * 获取请求中参数转化为DeviceClientAuthenticationToken
+ *
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ * @since 1.1
+ */
+public final class DeviceClientAuthenticationConverter implements AuthenticationConverter {
+    private final RequestMatcher deviceAuthorizationRequestMatcher;
+    private final RequestMatcher deviceAccessTokenRequestMatcher;
+
+    public DeviceClientAuthenticationConverter(String deviceAuthorizationEndpointUri) {
+        RequestMatcher clientIdParameterMatcher = request ->
+                request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null;
+        this.deviceAuthorizationRequestMatcher = new AndRequestMatcher(
+                new AntPathRequestMatcher(
+                        deviceAuthorizationEndpointUri, HttpMethod.POST.name()),
+                clientIdParameterMatcher);
+        this.deviceAccessTokenRequestMatcher = request ->
+                AuthorizationGrantType.DEVICE_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
+                        request.getParameter(OAuth2ParameterNames.DEVICE_CODE) != null &&
+                        request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null;
+    }
+
+    @Nullable
+    @Override
+    public Authentication convert(HttpServletRequest request) {
+        if (!this.deviceAuthorizationRequestMatcher.matches(request) &&
+                !this.deviceAccessTokenRequestMatcher.matches(request)) {
+            return null;
+        }
+
+        // client_id (REQUIRED)
+        String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);
+        if (!StringUtils.hasText(clientId) ||
+                request.getParameterValues(OAuth2ParameterNames.CLIENT_ID).length != 1) {
+            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+        }
+
+        return new DeviceClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null, null);
+    }
+
+}

+ 100 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationProvider.java

@@ -0,0 +1,100 @@
+package timing.ukulele.auth.authorization.device;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceVerificationAuthenticationToken;
+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.web.OAuth2ClientAuthenticationFilter;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * 设备码认证提供者
+ *
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ * @author vains
+ * @see DeviceClientAuthenticationToken
+ * @see DeviceClientAuthenticationConverter
+ * @see OAuth2ClientAuthenticationFilter
+ * @since 1.1
+ */
+@Slf4j
+@RequiredArgsConstructor
+public final class DeviceClientAuthenticationProvider implements AuthenticationProvider {
+
+    private final RegisteredClientRepository registeredClientRepository;
+
+    /**
+     * 异常说明地址
+     */
+    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1";
+    public static final String DEFAULT_DEVICE_REDIRECT_URI = "/activate";
+
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        // 执行时肯定是设备码流程
+        DeviceClientAuthenticationToken deviceClientAuthentication =
+                (DeviceClientAuthenticationToken) authentication;
+
+        // 只支持公共客户端
+        if (!ClientAuthenticationMethod.NONE.equals(deviceClientAuthentication.getClientAuthenticationMethod())) {
+            return null;
+        }
+
+        // 获取客户端id并查询
+        String clientId = deviceClientAuthentication.getPrincipal().toString();
+        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+        if (registeredClient == null) {
+            throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("Retrieved registered client");
+        }
+
+        // 校验客户端
+        if (registeredClient == null
+                || ObjectUtils.isEmpty(registeredClient.getClientAuthenticationMethods())
+                || !registeredClient.getClientAuthenticationMethods().contains(
+                deviceClientAuthentication.getClientAuthenticationMethod())) {
+            throwInvalidClient("authentication_method");
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("Validated device client authentication parameters");
+        }
+
+        if (log.isTraceEnabled()) {
+            log.trace("Authenticated device client");
+        }
+
+        return new DeviceClientAuthenticationToken(registeredClient,
+                deviceClientAuthentication.getClientAuthenticationMethod(), null);
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        // 只处理设备码请求
+        return DeviceClientAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+
+    private static void throwInvalidClient(String parameterName) {
+        OAuth2Error error = new OAuth2Error(
+                OAuth2ErrorCodes.INVALID_CLIENT,
+                "Device client authentication failed: " + parameterName,
+                ERROR_URI
+        );
+        throw new OAuth2AuthenticationException(error);
+    }
+
+}

+ 31 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationToken.java

@@ -0,0 +1,31 @@
+package timing.ukulele.auth.authorization.device;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Transient;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+
+import java.util.Map;
+
+/**
+ * 设备码模式token
+ *
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ * @since 1.1
+ */
+@Transient
+public class DeviceClientAuthenticationToken extends OAuth2ClientAuthenticationToken {
+
+    public DeviceClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
+                                           @Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
+        super(clientId, clientAuthenticationMethod, credentials, additionalParameters);
+    }
+
+    public DeviceClientAuthenticationToken(RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod,
+                                           @Nullable Object credentials) {
+        super(registeredClient, clientAuthenticationMethod, credentials);
+    }
+
+}

+ 65 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/filter/JWTAuthorizationFilter.java

@@ -0,0 +1,65 @@
+package timing.ukulele.auth.authorization.filter;
+
+import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+import timing.ukulele.auth.authorization.JwtAuthorizationToken;
+import timing.ukulele.auth.security.model.CustomGrantedAuthority;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class JWTAuthorizationFilter extends OncePerRequestFilter {
+
+    private final JwtDecoder jwtDecoder;
+
+    public JWTAuthorizationFilter(JwtDecoder jwtDecoder) {
+        this.jwtDecoder = jwtDecoder;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        String token = request.getParameter("token");
+        if (!StringUtils.hasText(token)) {
+//          从请求头中获取认证信息
+            String authHeader = request.getHeader("Authorization");
+            if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+                filterChain.doFilter(request, response);
+                return;
+            }
+            token = authHeader.substring(7);
+        }
+        if (!StringUtils.hasLength(token)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        Jwt decode = jwtDecoder.decode(token);
+        if (decode == null || decode.getExpiresAt() == null || decode.getExpiresAt().compareTo(Instant.now()) <= 0) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        // 将UserDetails存储到SecurityContextHolder中
+        Set<CustomGrantedAuthority> authorityList = new HashSet<>();
+        List<String> scopeList = decode.getClaimAsStringList("scope");
+        for (String scope : scopeList) {
+            CustomGrantedAuthority auth = new CustomGrantedAuthority(scope);
+            authorityList.add(auth);
+        }
+        LinkedTreeMap<String, Object> userInfo = decode.getClaim("userInfo");
+        User user = new User(decode.getClaimAsString("sub"), "", true, true, true, true, authorityList);
+        JwtAuthorizationToken authenticationToken = new JwtAuthorizationToken(user, authorityList);
+        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        filterChain.doFilter(request, response);
+    }
+}

+ 66 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/handler/ConsentAuthenticationFailureHandler.java

@@ -0,0 +1,66 @@
+package timing.ukulele.auth.authorization.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.util.UrlUtils;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 授权确认失败处理
+ *
+ * @author vains
+ */
+@RequiredArgsConstructor
+public class ConsentAuthenticationFailureHandler implements AuthenticationFailureHandler {
+
+    /**
+     * 授权确认页面路径
+     */
+    private final String consentPageUri;
+
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
+        // 获取当前认证信息
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        // 获取具体的异常
+        OAuth2AuthenticationException authenticationException = (OAuth2AuthenticationException) exception;
+        OAuth2Error error = authenticationException.getError();
+        // 异常信息
+        String message;
+        if (authentication == null) {
+            message = "登录已失效";
+        } else {
+            // 第二次点击“拒绝”会因为之前取消时删除授权申请记录而找不到对应的数据,导致抛出 [invalid_request] OAuth 2.0 Parameter: state
+            message = error.toString();
+        }
+
+        // 授权确认页面提交的请求
+        if (request.getMethod().equals(HttpMethod.POST.name()) && UrlUtils.isAbsoluteUrl(consentPageUri)) {
+            // 写回json异常
+            Result<Object> result = Result.error(HttpStatus.BAD_REQUEST.value(), message);
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.getWriter().write(JsonUtils.objectCovertToJson(result));
+            response.getWriter().flush();
+        } else {
+            // 在地址栏输入授权申请地址或设备码流程的验证地址错误(user_code错误)
+            response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
+        }
+
+    }
+
+}

+ 92 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/handler/ConsentAuthorizationResponseHandler.java

@@ -0,0 +1,92 @@
+package timing.ukulele.auth.authorization.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UriUtils;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.springframework.security.oauth2.core.OAuth2ErrorCodes.INVALID_REQUEST;
+
+/**
+ * 授权确认前后端分离适配响应处理
+ */
+@RequiredArgsConstructor
+public class ConsentAuthorizationResponseHandler implements AuthenticationSuccessHandler {
+
+    /**
+     * 授权确认页面
+     */
+    private final String consentPageUri;
+
+    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+        // 获取将要重定向的回调地址
+        String redirectUri = this.getAuthorizationResponseUri(authentication);
+        if (request.getMethod().equals(HttpMethod.POST.name()) && UrlUtils.isAbsoluteUrl(consentPageUri)) {
+            // 如果是post请求并且CONSENT_PAGE_URI是完整的地址,则响应json
+            Result<String> success = Result.success(redirectUri);
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.getWriter().write(JsonUtils.objectCovertToJson(success));
+            response.getWriter().flush();
+            return;
+        }
+        // 否则重定向至回调地址
+        this.redirectStrategy.sendRedirect(request, response, redirectUri);
+    }
+
+    /**
+     * 获取重定向的回调地址
+     *
+     * @param authentication 认证信息
+     * @return 地址
+     */
+    private String getAuthorizationResponseUri(Authentication authentication) {
+
+        OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+                (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
+        if (ObjectUtils.isEmpty(authorizationCodeRequestAuthentication.getRedirectUri())) {
+            String authorizeUriError = "Redirect uri is not null";
+            throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(INVALID_REQUEST, authorizeUriError, (null)), authorizationCodeRequestAuthentication);
+        }
+
+        if (authorizationCodeRequestAuthentication.getAuthorizationCode() == null) {
+            String authorizeError = "AuthorizationCode is not null";
+            throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(INVALID_REQUEST, authorizeError, (null)), authorizationCodeRequestAuthentication);
+        }
+
+        UriComponentsBuilder uriBuilder = UriComponentsBuilder
+                .fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
+                .queryParam(OAuth2ParameterNames.CODE, authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());
+        if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
+            uriBuilder.queryParam(
+                    OAuth2ParameterNames.STATE,
+                    UriUtils.encode(authorizationCodeRequestAuthentication.getState(), StandardCharsets.UTF_8));
+        }
+        // build(true) -> Components are explicitly encoded
+        return uriBuilder.build(true).toUriString();
+
+    }
+
+}

+ 35 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/handler/DeviceAuthorizationResponseHandler.java

@@ -0,0 +1,35 @@
+package timing.ukulele.auth.authorization.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 校验设备码成功响应类
+ */
+@RequiredArgsConstructor
+public class DeviceAuthorizationResponseHandler implements AuthenticationSuccessHandler {
+
+    /**
+     * 设备码验证成功后跳转地址
+     */
+    private final String deviceActivatedUri;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+        // 写回json数据
+        Result<Object> result = Result.success(deviceActivatedUri);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getWriter().write(JsonUtils.objectCovertToJson(result));
+        response.getWriter().flush();
+    }
+}

+ 85 - 0
security-auth/src/main/java/timing/ukulele/auth/authorization/handler/LoginTargetAuthenticationEntryPoint.java

@@ -0,0 +1,85 @@
+package timing.ukulele.auth.authorization.handler;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.ObjectUtils;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 重定向至登录处理
+ */
+@Slf4j
+public class LoginTargetAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
+
+    /**
+     * 设备码认证页面
+     */
+    private final String deviceActivateUri;
+
+    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+    /**
+     * @param loginFormUrl      URL where the login page can be found. Should either be
+     *                          relative to the web-app context path (include a leading {@code /}) or an absolute
+     *                          URL.
+     * @param deviceActivateUri 设备码验证页面地址
+     */
+    public LoginTargetAuthenticationEntryPoint(String loginFormUrl, String deviceActivateUri) {
+        super(loginFormUrl);
+        this.deviceActivateUri = deviceActivateUri;
+    }
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+        String deviceVerificationUri = "/oauth2/device_verification";
+        // 兼容设备码前后端分离
+        if (request.getRequestURI().equals(deviceVerificationUri)
+                && request.getMethod().equals(HttpMethod.POST.name())
+                && UrlUtils.isAbsoluteUrl(deviceActivateUri)) {
+            // 如果是请求验证设备激活码(user_code)时未登录并且设备码验证页面是前后端分离的那种则写回json
+            Result<String> success = Result.error(HttpStatus.UNAUTHORIZED.value(), ("登录已失效,请重新打开设备提供的验证地址"));
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+            response.getWriter().write(JsonUtils.objectCovertToJson(success));
+            response.getWriter().flush();
+            return;
+        }
+
+        // 获取登录表单的地址
+        String loginForm = determineUrlToUseForThisRequest(request, response, authException);
+        if (!UrlUtils.isAbsoluteUrl(loginForm)) {
+            // 不是绝对路径调用父类方法处理
+            super.commence(request, response, authException);
+            return;
+        }
+
+        StringBuffer requestUrl = request.getRequestURL();
+        if(requestUrl.toString().contains("oauth2/authorize"))
+            requestUrl = new StringBuffer("http://www.test.com:5173/api/oauth2/authorize");
+
+        if (!ObjectUtils.isEmpty(request.getQueryString())) {
+            requestUrl.append("?").append(request.getQueryString());
+        }
+
+        // 绝对路径在重定向前添加target参数
+        String targetParameter = URLEncoder.encode(requestUrl.toString(), StandardCharsets.UTF_8);
+        String targetUrl = loginForm + "?target=" + targetParameter;
+        log.debug("重定向至前后端分离的登录页面:{}", targetUrl);
+        this.redirectStrategy.sendRedirect(request, response, targetUrl);
+    }
+}

+ 117 - 0
security-auth/src/main/java/timing/ukulele/auth/config/BeanConfig.java

@@ -0,0 +1,117 @@
+package timing.ukulele.auth.config;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import timing.ukulele.auth.config.property.TimingSecurityProperties;
+import timing.ukulele.auth.security.filter.JWTSecurityFilter;
+import timing.ukulele.auth.security.handler.LoginFailureHandler;
+import timing.ukulele.auth.security.handler.LoginSuccessHandler;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+import timing.ukulele.auth.security.type.image.ImageCodeAuthenticationSecurityConfig;
+import timing.ukulele.auth.security.type.sms.SmsCodeAuthenticationSecurityConfig;
+import timing.ukulele.auth.security.type.wechat.ThirdQrAuthenticationSecurityConfig;
+
+/**
+ * 将bean注入至ioc的配置类
+ */
+@Configuration
+@EnableConfigurationProperties
+public class BeanConfig {
+
+    public BeanConfig() {
+//        this.securityContextRepository = securityContextRepository;
+    }
+
+    /**
+     * 跨域过滤器配置
+     *
+     * @return CorsFilter
+     */
+    @Bean
+    public CorsFilter corsFilter() {
+
+        // 初始化cors配置对象
+        CorsConfiguration configuration = new CorsConfiguration();
+
+        // 设置允许跨域的域名,如果允许携带cookie的话,路径就不能写*号, *表示所有的域名都可以跨域访问
+        configuration.addAllowedOrigin("*");
+        // 设置跨域访问可以携带cookie
+        configuration.setAllowCredentials(false);
+        // 允许所有的请求方法 ==> GET POST PUT Delete
+        configuration.addAllowedMethod("*");
+        // 允许携带任何头信息
+        configuration.addAllowedHeader("*");
+
+        // 初始化cors配置源对象
+        UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
+
+        // 给配置源对象设置过滤的参数
+        // 参数一: 过滤的路径 == > 所有的路径都要求校验是否跨域
+        // 参数二: 配置类
+        configurationSource.registerCorsConfiguration("/**", configuration);
+
+        // 返回配置好的过滤器
+        return new CorsFilter(configurationSource);
+    }
+
+    @Bean
+    public JWTSecurityFilter jwtAuthenticationFilter(JwtDecoder jwtDecoder) {
+        return new JWTSecurityFilter(jwtDecoder);
+    }
+
+    @Bean
+    public LoginSuccessHandler loginSuccessHandler(JwtEncoder jwtEncoder,
+                                                   TimingSecurityProperties securityProperties) {
+        return new LoginSuccessHandler(jwtEncoder, securityProperties);
+    }
+
+    @Bean
+    public LoginFailureHandler loginFailureHandler() {
+        return new LoginFailureHandler();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Bean
+    public TimingUserDetailService userDetailService(PasswordEncoder passwordEncoder) {
+        return new TimingUserDetailService(passwordEncoder);
+    }
+
+    @Bean
+    public SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig(
+            LoginSuccessHandler loginSuccessHandler,
+            LoginFailureHandler loginFailureHandler,
+            TimingUserDetailService userDetailsService) {
+        return new SmsCodeAuthenticationSecurityConfig(loginFailureHandler, loginSuccessHandler, userDetailsService);
+    }
+
+    @Bean
+    public ImageCodeAuthenticationSecurityConfig imageCodeAuthenticationSecurityConfig(
+            LoginSuccessHandler loginSuccessHandler,
+            LoginFailureHandler loginFailureHandler,
+            TimingUserDetailService userDetailsService,
+            PasswordEncoder passwordEncoder) {
+        return new ImageCodeAuthenticationSecurityConfig(loginFailureHandler, loginSuccessHandler, userDetailsService, passwordEncoder);
+    }
+
+    @Bean
+    public ThirdQrAuthenticationSecurityConfig weChatQrAuthenticationSecurityConfig(
+            LoginSuccessHandler loginSuccessHandler,
+            LoginFailureHandler loginFailureHandler,
+            TimingUserDetailService userDetailsService) {
+        return new ThirdQrAuthenticationSecurityConfig(loginFailureHandler, loginSuccessHandler, userDetailsService);
+    }
+}

+ 79 - 0
security-auth/src/main/java/timing/ukulele/auth/config/RedisConfig.java

@@ -0,0 +1,79 @@
+package timing.ukulele.auth.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisKeyValueAdapter;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.security.jackson2.CoreJackson2Module;
+
+/**
+ * Redis的key序列化配置类
+ */
+@Configuration
+@RequiredArgsConstructor
+@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
+public class RedisConfig {
+
+    private final Jackson2ObjectMapperBuilder builder;
+
+    /**
+     * 默认情况下使用
+     *
+     * @param connectionFactory redis链接工厂
+     * @return RedisTemplate
+     */
+    @Bean
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        // 字符串序列化器
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+
+        // 创建ObjectMapper并添加默认配置
+        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
+
+        // 序列化所有字段
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+
+        // 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
+        //     java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
+        objectMapper.activateDefaultTyping(
+                objectMapper.getPolymorphicTypeValidator(),
+                ObjectMapper.DefaultTyping.NON_FINAL,
+                JsonTypeInfo.As.PROPERTY);
+
+        // 添加Security提供的Jackson Mixin
+        objectMapper.registerModule(new CoreJackson2Module());
+
+        // 存入redis时序列化值的序列化器
+        Jackson2JsonRedisSerializer<Object> valueSerializer =
+                new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
+
+        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
+
+        // 设置值序列化
+        redisTemplate.setValueSerializer(valueSerializer);
+        // 设置hash格式数据值的序列化器
+        redisTemplate.setHashValueSerializer(valueSerializer);
+        // 默认的Key序列化器为:JdkSerializationRedisSerializer
+        redisTemplate.setKeySerializer(stringRedisSerializer);
+        // 设置字符串序列化器
+        redisTemplate.setStringSerializer(stringRedisSerializer);
+        // 设置hash结构的key的序列化器
+        redisTemplate.setHashKeySerializer(stringRedisSerializer);
+
+        // 设置连接工厂
+        redisTemplate.setConnectionFactory(connectionFactory);
+
+        return redisTemplate;
+    }
+
+}

+ 87 - 0
security-auth/src/main/java/timing/ukulele/auth/config/SecurityConfig.java

@@ -0,0 +1,87 @@
+package timing.ukulele.auth.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.filter.CorsFilter;
+import timing.ukulele.auth.config.property.TimingSecurityProperties;
+import timing.ukulele.auth.security.filter.JWTSecurityFilter;
+import timing.ukulele.auth.security.type.image.ImageCodeAuthenticationSecurityConfig;
+import timing.ukulele.auth.security.type.sms.SmsCodeAuthenticationSecurityConfig;
+import timing.ukulele.auth.security.type.wechat.ThirdQrAuthenticationSecurityConfig;
+
+/**
+ * 资源服务器配置
+ * <p>
+ * {@link EnableMethodSecurity} 开启全局方法认证,启用JSR250注解支持,启用注解 {@link Secured} 支持,
+ * 在Spring Security 6.0版本中将@Configuration注解从@EnableWebSecurity, @EnableMethodSecurity, @EnableGlobalMethodSecurity
+ * 和 @EnableGlobalAuthentication 中移除,使用这些注解需手动添加 @Configuration 注解
+ * {@link EnableWebSecurity} 注解有两个作用:
+ * 1. 加载了WebSecurityConfiguration配置类, 配置安全认证策略。
+ * 2. 加载了AuthenticationConfiguration, 配置了认证信息。
+ */
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
+public class SecurityConfig {
+
+    /**
+     * 不需要认证即可访问的路径
+     */
+    private final TimingSecurityProperties customSecurityProperties;
+    private final SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
+    private final ImageCodeAuthenticationSecurityConfig imageCodeAuthenticationSecurityConfig;
+    private final ThirdQrAuthenticationSecurityConfig thirdQrAuthenticationSecurityConfig;
+
+    /**
+     * 配置认证相关的过滤器链(资源服务,客户端配置)
+     *
+     * @param http spring security核心配置类
+     * @return 过滤器链
+     * @throws Exception 抛出
+     */
+    @Bean
+    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
+                                                        CorsFilter corsFilter,
+                                                          JwtDecoder jwtDecoder) throws Exception {
+        // 添加基础的认证配置
+        http.addFilter(corsFilter);
+
+        // 禁用 csrf 与 cors
+        http.csrf(AbstractHttpConfigurer::disable);
+//        http.cors(AbstractHttpConfigurer::disable);
+
+        http.httpBasic(Customizer.withDefaults())
+                .authorizeHttpRequests((authorize) -> authorize
+                        // 放行静态资源和不需要认证的url
+                        .requestMatchers(customSecurityProperties.getIgnoreUriList().toArray(new String[0])).permitAll()
+                        .anyRequest().authenticated()
+                )
+                .formLogin(AbstractHttpConfigurer::disable);
+//        http.sessionManagement(AbstractHttpConfigurer::disable);
+        http.sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
+
+        http.apply(smsCodeAuthenticationSecurityConfig);
+        http.apply(imageCodeAuthenticationSecurityConfig);
+        http.apply(thirdQrAuthenticationSecurityConfig);
+
+
+        JWTSecurityFilter jwtFilter = new JWTSecurityFilter(jwtDecoder);
+        http.addFilterAt(jwtFilter, UsernamePasswordAuthenticationFilter.class);
+
+        DefaultSecurityFilterChain build = http.build();
+        return build;
+    }
+}

+ 58 - 0
security-auth/src/main/java/timing/ukulele/auth/config/property/TimingSecurityProperties.java

@@ -0,0 +1,58 @@
+package timing.ukulele.auth.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * 自定义认证配置类
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = TimingSecurityProperties.PREFIX)
+public class TimingSecurityProperties {
+
+    static final String PREFIX = "timing.security";
+
+    /**
+     * 登录页面地址
+     * 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!!
+     * 错误e.g. http://当前项目IP:当前项目端口/login
+     */
+    private String loginUrl = "/login";
+
+    /**
+     * 授权确认页面
+     * 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!!
+     * 错误e.g. http://当前项目IP:当前项目端口/oauth2/consent
+     */
+    private String consentPageUri = "/oauth2/consent";
+
+    /**
+     * 授权码验证页面
+     * 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!!
+     * 错误e.g. http://当前项目IP:当前项目端口/activate
+     */
+    private String deviceActivateUri = "/activate";
+
+    /**
+     * 授权码验证成功后页面
+     * 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!!
+     * 错误e.g. http://当前项目IP:当前项目端口/activated
+     */
+    private String deviceActivatedUri = "/activated";
+
+    /**
+     * 不需要认证的路径
+     */
+    private List<String> ignoreUriList;
+
+    /**
+     * 设置token签发地址(http(s)://{ip}:{port}/context-path, http(s)://domain.com/context-path)
+     * 如果需要通过ip访问这里就是ip,如果是有域名映射就填域名,通过什么方式访问该服务这里就填什么
+     */
+    private String issuerUrl;
+
+}

+ 33 - 0
security-auth/src/main/java/timing/ukulele/auth/constant/RedisConstants.java

@@ -0,0 +1,33 @@
+package timing.ukulele.auth.constant;
+
+/**
+ * Redis相关常量
+ */
+public final class RedisConstants {
+
+    /**
+     * jwk set缓存前缀
+     */
+    public static final String AUTHORIZATION_JWS_PREFIX_KEY = "authorization_jws";
+
+    /**
+     * 认证信息存储前缀
+     */
+    public static final String SECURITY_CONTEXT_PREFIX_KEY = "security_context:";
+
+    /**
+     * 短信验证码前缀
+     */
+    public static final String SMS_CAPTCHA_PREFIX_KEY = "sms_captcha:";
+
+    /**
+     * 图形验证码前缀
+     */
+    public static final String IMAGE_CAPTCHA_PREFIX_KEY = "image_captcha:";
+
+    /**
+     * 默认过期时间,默认五分钟
+     */
+    public static final int DEFAULT_TIMEOUT_SECONDS = 60 * 5;
+
+}

+ 104 - 0
security-auth/src/main/java/timing/ukulele/auth/constant/SecurityConstants.java

@@ -0,0 +1,104 @@
+package timing.ukulele.auth.constant;
+
+/**
+ * security 常量类
+ */
+public final class SecurityConstants {
+
+    /**
+     * 微信登录相关参数——openid:用户唯一id
+     */
+    public static final String OAUTH_LOGIN_TYPE = "loginType";
+
+    /**
+     * 微信登录相关参数——openid:用户唯一id
+     */
+    public static final String TOKEN_UNIQUE_ID = "uniqueId";
+
+    /**
+     * 微信登录相关参数——openid:用户唯一id
+     */
+    public static final String WECHAT_PARAMETER_OPENID = "openid";
+
+    /**
+     * 微信登录相关参数——forcePopup:强制此次授权需要用户弹窗确认
+     */
+    public static final String WECHAT_PARAMETER_FORCE_POPUP = "forcePopup";
+
+    /**
+     * 微信登录相关参数——secret:微信的应用秘钥
+     */
+    public static final String WECHAT_PARAMETER_SECRET = "secret";
+
+    /**
+     * 微信登录相关参数——appid:微信的应用id
+     */
+    public static final String WECHAT_PARAMETER_APPID = "appid";
+
+    /**
+     * 三方登录类型——微信
+     */
+    public static final String THIRD_LOGIN_WECHAT = "wechat";
+
+    /**
+     * 随机字符串请求头名字
+     */
+//    public static final String NONCE_HEADER_NAME = "nonceId";
+    public static final String NONCE_HEADER_NAME = "JSESSIONID";
+
+    /**
+     * 登录方式入参名
+     */
+    public static final String LOGIN_TYPE_NAME = "loginType";
+
+    /**
+     * 验证码id入参名
+     */
+    public static final String CAPTCHA_ID_NAME = "captchaId";
+
+    /**
+     * 验证码值入参名
+     */
+    public static final String CAPTCHA_CODE_NAME = "code";
+
+    /**
+     * 登录方式——短信验证码
+     */
+    public static final String SMS_LOGIN_TYPE = "smsCaptcha";
+
+    /**
+     * 登录方式——账号密码登录
+     */
+    public static final String PASSWORD_LOGIN_TYPE = "passwordLogin";
+    /**
+     * 登录方式——第三方扫码
+     */
+    public static final String THIRD_QR_LOGIN_TYPE = "thirdQrLogin";
+
+    /**
+     * 权限在token中的key
+     */
+    public static final String AUTHORITIES_KEY = "authorities";
+
+    /**
+     * 自定义 grant type —— 短信验证码
+     */
+    public static final String GRANT_TYPE_SMS_CODE = "urn:ietf:params:oauth:grant-type:sms_code";
+
+    /**
+     * 自定义 grant type —— 短信验证码 —— 手机号的key
+     */
+    public static final String OAUTH_PARAMETER_NAME_PHONE = "phone";
+
+    /**
+     * 自定义 grant type —— 短信验证码 —— 短信验证码的key
+     */
+    public static final String OAUTH_PARAMETER_NAME_SMS_CAPTCHA = "sms_captcha";
+
+
+    /**
+     * 自定义 grant type —— 三方二维码(微信扫码登录等)
+     */
+    public static final String GRANT_TYPE_THIRD_QR = "urn:ietf:params:oauth:grant-type:third_qr";
+
+}

+ 198 - 0
security-auth/src/main/java/timing/ukulele/auth/controller/AuthorizationController.java

@@ -0,0 +1,198 @@
+package timing.ukulele.auth.controller;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.Data;
+import lombok.SneakyThrows;
+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.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.ui.Model;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.util.UriUtils;
+import timing.ukulele.auth.config.property.TimingSecurityProperties;
+import timing.ukulele.auth.model.Result;
+
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.*;
+
+/**
+ * 认证服务器相关自定接口
+ */
+@RestController
+public class AuthorizationController {
+    private final TimingSecurityProperties securityProperties;
+    private final RegisteredClientRepository registeredClientRepository;
+    private final OAuth2AuthorizationConsentService authorizationConsentService;
+
+    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+    public AuthorizationController(
+            TimingSecurityProperties securityProperties,
+            RegisteredClientRepository registeredClientRepository,
+            OAuth2AuthorizationConsentService authorizationConsentService) {
+        this.securityProperties = securityProperties;
+        this.registeredClientRepository = registeredClientRepository;
+        this.authorizationConsentService = authorizationConsentService;
+    }
+
+    @SneakyThrows
+    @ResponseBody
+    @GetMapping(value = "/oauth2/consent/redirect")
+    public Result<String> consentRedirect(HttpServletRequest request,
+                                          HttpServletResponse response,
+                                          @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
+                                          @RequestParam(OAuth2ParameterNames.STATE) String state,
+                                          @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
+                                          @RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) {
+
+        // 携带当前请求参数重定向至前端页面
+        UriComponentsBuilder uriBuilder = UriComponentsBuilder
+                .fromUriString(securityProperties.getConsentPageUri())
+                .queryParam(OAuth2ParameterNames.SCOPE, UriUtils.encode(scope, StandardCharsets.UTF_8))
+                .queryParam(OAuth2ParameterNames.STATE, UriUtils.encode(state, StandardCharsets.UTF_8))
+                .queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
+                .queryParam(OAuth2ParameterNames.USER_CODE, userCode);
+
+        String uriString = uriBuilder.build(Boolean.TRUE).toUriString();
+        if (ObjectUtils.isEmpty(userCode) || !UrlUtils.isAbsoluteUrl(securityProperties.getDeviceActivateUri())) {
+            // 不是设备码模式或者设备码验证页面不是前后端分离的,无需返回json,直接重定向
+            redirectStrategy.sendRedirect(request, response, uriString);
+            return null;
+        }
+        // 兼容设备码,需响应JSON,由前端进行跳转
+        return Result.success(uriString);
+    }
+
+    @ResponseBody
+    @GetMapping(value = "/oauth2/consent/parameters")
+    public Result<Map<String, Object>> consentParameters(Principal principal,
+                                                         @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) {
+
+        // 获取consent页面所需的参数
+        Map<String, Object> consentParameters = getConsentParameters(scope, state, clientId, userCode, principal);
+
+        return Result.success(consentParameters);
+    }
+
+    /**
+     * 根据授权确认相关参数获取授权确认与未确认的scope相关参数
+     *
+     * @param scope     scope权限
+     * @param state     state
+     * @param clientId  客户端id
+     * @param userCode  设备码授权流程中的用户码
+     * @param principal 当前认证信息
+     * @return 页面所需数据
+     */
+    private Map<String, Object> getConsentParameters(String scope,
+                                                     String state,
+                                                     String clientId,
+                                                     String userCode,
+                                                     Principal principal) {
+
+        if (principal == null) {
+            throw new RuntimeException("认证信息已失效.");
+        }
+
+        // Remove scopes that were already approved
+        Set<String> scopesToApprove = new HashSet<>();
+        Set<String> previouslyApprovedScopes = new HashSet<>();
+        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
+        if (registeredClient == null) {
+            throw new RuntimeException("客户端不存在");
+        }
+        OAuth2AuthorizationConsent currentAuthorizationConsent =
+                this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
+        Set<String> authorizedScopes;
+        if (currentAuthorizationConsent != null) {
+            authorizedScopes = currentAuthorizationConsent.getScopes();
+        } else {
+            authorizedScopes = Collections.emptySet();
+        }
+        for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
+            if (OidcScopes.OPENID.equals(requestedScope)) {
+                continue;
+            }
+            if (authorizedScopes.contains(requestedScope)) {
+                previouslyApprovedScopes.add(requestedScope);
+            } else {
+                scopesToApprove.add(requestedScope);
+            }
+        }
+
+        Map<String, Object> parameters = new HashMap<>(7);
+        parameters.put("clientId", registeredClient.getClientId());
+        parameters.put("clientName", registeredClient.getClientName());
+        parameters.put("state", state);
+        parameters.put("scopes", withDescription(scopesToApprove));
+        parameters.put("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));
+        parameters.put("principalName", principal.getName());
+        parameters.put("userCode", userCode);
+        if (StringUtils.hasText(userCode)) {
+            parameters.put("requestURI", "/oauth2/device_verification");
+        } else {
+            parameters.put("requestURI", "/oauth2/authorize");
+        }
+        return parameters;
+    }
+
+    private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
+        Set<ScopeWithDescription> scopeWithDescriptions = new HashSet<>();
+        for (String scope : scopes) {
+            scopeWithDescriptions.add(new ScopeWithDescription(scope));
+
+        }
+        return scopeWithDescriptions;
+    }
+
+    @Data
+    public static class ScopeWithDescription {
+        private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this.";
+        private static final Map<String, String> scopeDescriptions = new HashMap<>();
+
+        static {
+            scopeDescriptions.put(
+                    OidcScopes.PROFILE,
+                    "This application will be able to read your profile information."
+            );
+            scopeDescriptions.put(
+                    "message.read",
+                    "This application will be able to read your message."
+            );
+            scopeDescriptions.put(
+                    "message.write",
+                    "This application will be able to add new messages. It will also be able to edit and delete existing messages."
+            );
+            scopeDescriptions.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 = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
+        }
+    }
+}

+ 44 - 0
security-auth/src/main/java/timing/ukulele/auth/controller/CommonController.java

@@ -0,0 +1,44 @@
+package timing.ukulele.auth.controller;
+
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.ShearCaptcha;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import timing.ukulele.auth.model.CaptchaResult;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.support.RedisOperator;
+
+import java.util.UUID;
+
+import static timing.ukulele.auth.constant.RedisConstants.*;
+
+@RestController
+public class CommonController {
+
+    private final RedisOperator<String> redisOperator;
+
+    public CommonController(RedisOperator<String> redisOperator) {
+        this.redisOperator = redisOperator;
+    }
+
+    @GetMapping("/getSmsCaptcha")
+    public Result<String> getSmsCaptcha(String phone) {
+        // 示例项目,固定1234
+        String smsCaptcha = "1234";
+        // 存入缓存中,5分钟后过期
+        redisOperator.set((SMS_CAPTCHA_PREFIX_KEY + phone), smsCaptcha, DEFAULT_TIMEOUT_SECONDS);
+        return Result.success("获取短信验证码成功.", smsCaptcha);
+    }
+
+    @GetMapping("/getCaptcha")
+    public Result<CaptchaResult> getCaptcha() {
+        // 使用huTool-captcha生成图形验证码
+        // 定义图形验证码的长、宽、验证码字符数、干扰线宽度
+        ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(130, 34, 4, 2);
+        // 生成一个唯一id
+        String id = UUID.randomUUID().toString();
+        // 存入缓存中,5分钟后过期
+        redisOperator.set((IMAGE_CAPTCHA_PREFIX_KEY + id), captcha.getCode(), DEFAULT_TIMEOUT_SECONDS);
+        return Result.success("获取验证码成功.", new CaptchaResult(String.valueOf(id), captcha.getCode(), captcha.getImageBase64Data()));
+    }
+}

+ 36 - 0
security-auth/src/main/java/timing/ukulele/auth/controller/TestController.java

@@ -0,0 +1,36 @@
+package timing.ukulele.auth.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 测试接口
+ */
+@RestController
+public class TestController {
+
+    @GetMapping("/test01")
+    @PreAuthorize("hasAuthority('SCOPE_message.read')")
+    public String test01() {
+        return "test01";
+    }
+
+    @GetMapping("/test02")
+    @PreAuthorize("hasAuthority('SCOPE_message.write')")
+    public String test02() {
+        return "test02";
+    }
+
+    @GetMapping("/app")
+    @PreAuthorize("hasAuthority('app')")
+    public String app() {
+        return "app";
+    }
+
+    @GetMapping("/test03")
+    public String test03() {
+        return "test03";
+    }
+
+}

+ 31 - 0
security-auth/src/main/java/timing/ukulele/auth/http/ExchangeBeanConfig.java

@@ -0,0 +1,31 @@
+package timing.ukulele.auth.http;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.support.WebClientAdapter;
+import org.springframework.web.service.invoker.HttpServiceProxyFactory;
+import timing.ukulele.auth.http.wechat.WeChatExchange;
+
+/**
+ * Http Interface注入ioc配置
+ */
+@Configuration
+@RequiredArgsConstructor
+public class ExchangeBeanConfig {
+
+    /**
+     * 注入WeChatExchange
+     *
+     * @return WeChatExchange
+     */
+    @Bean
+    public WeChatExchange weChatExchange() {
+        WebClient webClient = WebClient.builder().baseUrl("http://ssss.com").build();
+        HttpServiceProxyFactory httpServiceProxyFactory =
+                HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
+                        .build();
+        return httpServiceProxyFactory.createClient(WeChatExchange.class);
+    }
+}

+ 21 - 0
security-auth/src/main/java/timing/ukulele/auth/http/wechat/WeChatExchange.java

@@ -0,0 +1,21 @@
+package timing.ukulele.auth.http.wechat;
+
+import org.springframework.web.service.annotation.GetExchange;
+import org.springframework.web.service.annotation.HttpExchange;
+import timing.ukulele.auth.model.Result;
+
+/**
+ * 微信接口调用
+ */
+@HttpExchange
+public interface WeChatExchange {
+
+    /**
+     * 调用当前项目的获取验证码方法
+     *
+     * @return 统一响应类
+     */
+    @GetExchange("/getCaptcha")
+    Result<String> getCaptcha();
+
+}

+ 30 - 0
security-auth/src/main/java/timing/ukulele/auth/model/CaptchaResult.java

@@ -0,0 +1,30 @@
+package timing.ukulele.auth.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 获取验证码返回
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CaptchaResult {
+
+    /**
+     * 验证码id
+     */
+    private String captchaId;
+
+    /**
+     * 验证码的值
+     */
+    private String code;
+
+    /**
+     * 图片验证码的base64值
+     */
+    private String imageData;
+
+}

+ 110 - 0
security-auth/src/main/java/timing/ukulele/auth/model/Result.java

@@ -0,0 +1,110 @@
+package timing.ukulele.auth.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+import java.io.Serializable;
+
+/**
+ * 公共响应类
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result<T> implements Serializable {
+
+    /**
+     * 响应状态码
+     */
+    private Integer code;
+
+    /**
+     * 响应信息
+     */
+    private String message;
+
+    /**
+     * 接口是否处理成功
+     */
+    private Boolean success;
+
+    /**
+     * 接口响应时携带的数据
+     */
+    private T data;
+
+    /**
+     * 操作成功携带数据
+     * @param data 数据
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> success(T data) {
+        return new Result<>(HttpStatus.OK.value(), ("操作成功."),Boolean.TRUE, data);
+    }
+
+    /**
+     * 操作成功不带数据
+     * @return 返回统一响应
+     */
+    public static Result<String> success() {
+        return new Result<>(HttpStatus.OK.value(), ("操作成功."), Boolean.TRUE, (null));
+    }
+
+    /**
+     * 操作成功携带数据
+     * @param message 成功提示消息
+     * @param data 成功携带数据
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> success(String message, T data) {
+        return new Result<>(HttpStatus.OK.value(), message, Boolean.TRUE, data);
+    }
+
+    /**
+     * 操作失败返回
+     * @param message 成功提示消息
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> error(String message) {
+        return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), message, Boolean.FALSE, (null));
+    }
+
+    /**
+     * 操作失败返回
+     * @param code 错误码
+     * @param message 成功提示消息
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> error(Integer code, String message) {
+        return new Result<>(code, message, Boolean.FALSE, (null));
+    }
+
+    /**
+     * oauth2 问题
+     * @param message 失败提示消息
+     * @param data 具体的错误信息
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> oauth2Error(Integer code, String message, T data) {
+        return new Result<>(code, message, Boolean.FALSE, data);
+    }
+
+    /**
+     * oauth2 问题
+     * @param message 失败提示消息
+     * @param data 具体的错误信息
+     * @param <T> 类型
+     * @return 返回统一响应
+     */
+    public static <T> Result<T> oauth2Error(String message, T data) {
+        return new Result<>(HttpStatus.UNAUTHORIZED.value(), message, Boolean.FALSE, data);
+    }
+
+}

+ 44 - 0
security-auth/src/main/java/timing/ukulele/auth/security/LoginTypeEnum.java

@@ -0,0 +1,44 @@
+package timing.ukulele.auth.security;
+
+/**
+ * 登录方式
+ */
+public enum LoginTypeEnum {
+    IMAGE_CODE("image_code", "用户名密码和图片验证码",  "/authentication/captcha"),
+    MOBILE_CODE("mobile_code", "短信验证码",  "/authentication/mobile"),
+    THIRD_QR("third_qr", "三方扫码登录",  "/authentication/thirdQr");
+
+    private String code;
+    private String name;
+    private String path;
+
+    LoginTypeEnum(String code, String name, String path) {
+        this.code = code;
+        this.name = name;
+        this.path = path;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+}

+ 41 - 0
security-auth/src/main/java/timing/ukulele/auth/security/SecurityUtils.java

@@ -0,0 +1,41 @@
+package timing.ukulele.auth.security;
+
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import java.util.Map;
+
+/**
+ * 认证鉴权工具
+ */
+@Slf4j
+public class SecurityUtils {
+
+    private static final String CUSTOM_DEVICE_REDIRECT_URI = "/activate/redirect";
+
+    private SecurityUtils() {
+        // 禁止实例化工具类
+        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
+    }
+
+    /**
+     * 提取请求中的参数并转为一个map返回
+     *
+     * @param request 当前请求
+     * @return 请求中的参数
+     */
+    public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
+        Map<String, String[]> parameterMap = request.getParameterMap();
+        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
+        parameterMap.forEach((key, values) -> {
+            for (String value : values) {
+                parameters.add(key, value);
+            }
+        });
+        return parameters;
+    }
+
+}

+ 42 - 0
security-auth/src/main/java/timing/ukulele/auth/security/ThirdTypeEnum.java

@@ -0,0 +1,42 @@
+package timing.ukulele.auth.security;
+
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public enum ThirdTypeEnum {
+    WECHAT_WEB("0", "微信网页"),
+    WECHAT_APP("1", "微信小程序"),
+    WECHAT_MP("2", "微信公众号");
+    private String code;
+    private String name;
+
+    ThirdTypeEnum(String code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public static ThirdTypeEnum getByCode(String code) {
+        if (!StringUtils.hasLength(code))
+            return null;
+        Optional<ThirdTypeEnum> first = Arrays.stream(values()).filter(item -> item.getCode().equals(code)).findFirst();
+        return first.orElse(null);
+    }
+}

+ 63 - 0
security-auth/src/main/java/timing/ukulele/auth/security/filter/JWTSecurityFilter.java

@@ -0,0 +1,63 @@
+package timing.ukulele.auth.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+import timing.ukulele.auth.security.model.CustomGrantedAuthority;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class JWTSecurityFilter extends OncePerRequestFilter {
+
+    private final JwtDecoder jwtDecoder;
+
+    public JWTSecurityFilter(
+            JwtDecoder jwtDecoder) {
+        this.jwtDecoder = jwtDecoder;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+//        //如果是要登录,放行到登录页面
+//        // 从请求头中获取认证信息
+        String authHeader = request.getHeader("Authorization");
+        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        String token = authHeader.substring(7);
+        if (!StringUtils.hasLength(token)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        Jwt decode = jwtDecoder.decode(token);
+        if (decode == null || decode.getExpiresAt() == null || decode.getExpiresAt().compareTo(Instant.now()) <= 0) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        // 将UserDetails存储到SecurityContextHolder中
+        List<CustomGrantedAuthority> authorityList = new ArrayList<>();
+        List<String> scopeList = decode.getClaimAsStringList("scope");
+        for (String scope : scopeList) {
+            CustomGrantedAuthority auth = new CustomGrantedAuthority(scope);
+            authorityList.add(auth);
+        }
+        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(decode.getClaimAsString("sub"), null, authorityList);
+        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        filterChain.doFilter(request, response);
+    }
+}
+

+ 33 - 0
security-auth/src/main/java/timing/ukulele/auth/security/handler/LoginFailureHandler.java

@@ -0,0 +1,33 @@
+package timing.ukulele.auth.security.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 登录失败处理类
+ */
+@Slf4j
+public class LoginFailureHandler implements AuthenticationFailureHandler {
+
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
+        log.debug("登录失败,原因:{}", exception.getMessage());
+        // 登录失败,写回401与具体的异常
+        Result<String> success = Result.error(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getWriter().write(JsonUtils.objectCovertToJson(success));
+        response.getWriter().flush();
+    }
+
+}

+ 61 - 0
security-auth/src/main/java/timing/ukulele/auth/security/handler/LoginSuccessHandler.java

@@ -0,0 +1,61 @@
+package timing.ukulele.auth.security.handler;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.jwt.*;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import timing.ukulele.auth.config.property.TimingSecurityProperties;
+import timing.ukulele.auth.model.Result;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 登录成功处理类
+ */
+@Slf4j
+public class LoginSuccessHandler implements AuthenticationSuccessHandler {
+    private final JwtEncoder encoder;
+    private final TimingSecurityProperties securityProperties;
+
+    public LoginSuccessHandler(JwtEncoder encoder, TimingSecurityProperties securityProperties) {
+        this.encoder = encoder;
+        this.securityProperties = securityProperties;
+    }
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+        log.debug("登录成功.");
+        Result<String> success = Result.success();
+        Instant now = Instant.now();
+        long expiry = 36000L;
+        // @formatter:off
+        List<String> scopes = authentication.getAuthorities().stream()
+                .map(GrantedAuthority::getAuthority)
+                .collect(Collectors.toList());
+        JwtClaimsSet claims = JwtClaimsSet.builder()
+                .issuer(securityProperties.getIssuerUrl())
+                .issuedAt(now)
+                .expiresAt(now.plusSeconds(expiry))
+                .subject(authentication.getName())
+                .claim("scope", scopes)
+                .claim("userInfo", authentication.getPrincipal())
+                .claim("authorities", authentication.getAuthorities())
+                .build();
+        String tokenValue = encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+        success.setData(tokenValue);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.getWriter().write(JsonUtils.objectCovertToJson(success));
+        response.getWriter().flush();
+    }
+
+}

+ 24 - 0
security-auth/src/main/java/timing/ukulele/auth/security/model/CustomGrantedAuthority.java

@@ -0,0 +1,24 @@
+package timing.ukulele.auth.security.model;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+
+/**
+ * 自定义权限类
+ */
+@Data
+@JsonSerialize
+@NoArgsConstructor
+@AllArgsConstructor
+public class CustomGrantedAuthority implements GrantedAuthority {
+
+    private String authority;
+
+    @Override
+    public String getAuthority() {
+        return this.authority;
+    }
+}

+ 60 - 0
security-auth/src/main/java/timing/ukulele/auth/security/model/SupplierDeferredSecurityContext.java

@@ -0,0 +1,60 @@
+package timing.ukulele.auth.security.model;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.core.context.DeferredSecurityContext;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+
+import java.util.function.Supplier;
+
+/**
+ * @author Steve Riesenberg
+ * @since 5.8
+ */
+public final class SupplierDeferredSecurityContext implements DeferredSecurityContext {
+
+	private static final Log logger = LogFactory.getLog(SupplierDeferredSecurityContext.class);
+
+	private final Supplier<SecurityContext> supplier;
+
+	private final SecurityContextHolderStrategy strategy;
+
+	private SecurityContext securityContext;
+
+	private boolean missingContext;
+
+	public SupplierDeferredSecurityContext(Supplier<SecurityContext> supplier, SecurityContextHolderStrategy strategy) {
+		this.supplier = supplier;
+		this.strategy = strategy;
+	}
+
+	@Override
+	public SecurityContext get() {
+		init();
+		return this.securityContext;
+	}
+
+	@Override
+	public boolean isGenerated() {
+		init();
+		return this.missingContext;
+	}
+
+	private void init() {
+		if (this.securityContext != null) {
+			return;
+		}
+
+		this.securityContext = this.supplier.get();
+		this.missingContext = (this.securityContext == null);
+		if (this.missingContext) {
+			this.securityContext = this.strategy.createEmptyContext();
+			if (logger.isTraceEnabled()) {
+				logger.trace(LogMessage.format("Created %s", this.securityContext));
+			}
+		}
+	}
+
+}

+ 35 - 0
security-auth/src/main/java/timing/ukulele/auth/security/service/TimingUserDetailService.java

@@ -0,0 +1,35 @@
+package timing.ukulele.auth.security.service;
+
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import timing.ukulele.auth.security.model.CustomGrantedAuthority;
+
+public class TimingUserDetailService implements UserDetailsService {
+    private final PasswordEncoder passwordEncoder;
+
+    public TimingUserDetailService(PasswordEncoder passwordEncoder) {
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        UserDetails user = User.withUsername("admin")
+                .password(passwordEncoder.encode("123456"))
+                .roles("admin", "normal")
+                .authorities(new CustomGrantedAuthority("app"),new CustomGrantedAuthority("web"), new CustomGrantedAuthority("system"))
+                .build();
+        return user;
+    }
+
+    public UserDetails loadUserByMobile(String mobile) {
+        UserDetails user = User.withUsername("admin")
+                .password(passwordEncoder.encode("123456"))
+                .roles("admin", "normal")
+                .authorities(new CustomGrantedAuthority("app"),new CustomGrantedAuthority("web"), new CustomGrantedAuthority("system"))
+                .build();
+        return user;
+    }
+}

+ 75 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationFilter.java

@@ -0,0 +1,75 @@
+package timing.ukulele.auth.security.type.image;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.StringUtils;
+import timing.ukulele.auth.security.LoginTypeEnum;
+
+public class ImageCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
+    private boolean postOnly = true;
+    private final String usernameParameter = "username";
+    private final String passwordParameter = "password";
+    private final String captchaParameter = "captcha";
+    private final String captchaIdParameter = "captchaId";
+
+    public ImageCodeAuthenticationFilter() {
+        super(new AntPathRequestMatcher(LoginTypeEnum.IMAGE_CODE.getPath(), "POST"));
+    }
+
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+        if (this.postOnly && !request.getMethod().equals("POST")) {
+            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
+        } else {
+            String captcha = this.obtainCaptcha(request);
+            captcha = captcha != null ? captcha : "";
+            String captchaId = this.obtainCaptchaId(request);
+            captchaId = captchaId != null ? captchaId : "";
+            if (!(StringUtils.hasLength(captcha) && StringUtils.hasLength(captchaId))) {
+                throw new AuthenticationServiceException("参数错误");
+            } else {
+                // TODO 校验图形验证码
+            }
+            String username = this.obtainUsername(request);
+            username = username != null ? username.trim() : "";
+            String password = this.obtainPassword(request);
+            password = password != null ? password : "";
+            if (!(StringUtils.hasLength(username) && StringUtils.hasLength(password))) {
+                throw new AuthenticationServiceException("参数错误");
+            }
+            ImageCodeAuthenticationToken authRequest = new ImageCodeAuthenticationToken(username, password);
+            this.setDetails(request, authRequest);
+            return this.getAuthenticationManager().authenticate(authRequest);
+        }
+    }
+
+
+    protected String obtainPassword(HttpServletRequest request) {
+        return request.getParameter(this.passwordParameter);
+    }
+
+    protected String obtainUsername(HttpServletRequest request) {
+        return request.getParameter(this.usernameParameter);
+    }
+
+    protected String obtainCaptcha(HttpServletRequest request) {
+        return request.getParameter(this.captchaParameter);
+    }
+
+    protected String obtainCaptchaId(HttpServletRequest request) {
+        return request.getParameter(this.captchaIdParameter);
+    }
+
+    protected void setDetails(HttpServletRequest request, ImageCodeAuthenticationToken authRequest) {
+        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+    }
+
+    public void setPostOnly(boolean postOnly) {
+        this.postOnly = postOnly;
+    }
+}

+ 48 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationProvider.java

@@ -0,0 +1,48 @@
+package timing.ukulele.auth.security.type.image;
+
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.util.ObjectUtils;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class ImageCodeAuthenticationProvider implements AuthenticationProvider {
+
+    private final TimingUserDetailService userService;
+    private final PasswordEncoder passwordEncoder;
+
+    public ImageCodeAuthenticationProvider(TimingUserDetailService userService, PasswordEncoder passwordEncoder) {
+        this.userService = userService;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        ImageCodeAuthenticationToken authenticationToken = (ImageCodeAuthenticationToken) authentication;
+        UserDetails user = userService.loadUserByUsername((String) authenticationToken.getPrincipal());
+        if (user == null) {
+            throw new InternalAuthenticationServiceException("无法获取用户信息");
+        }
+        if (ObjectUtils.isEmpty(authentication.getCredentials())) {
+            throw new AuthenticationCredentialsNotFoundException("密码不能为空");
+        }
+        String password = (String) authentication.getCredentials();
+        // TODO 锁定,过期等
+        if (!passwordEncoder.matches(password, user.getPassword())) {
+            throw new BadCredentialsException("用户名或密码错误");
+        }
+        ImageCodeAuthenticationToken result = new ImageCodeAuthenticationToken(user, user.getAuthorities());
+        result.setDetails(authenticationToken.getDetails());
+        return result;
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return ImageCodeAuthenticationToken.class.isAssignableFrom(aClass);
+    }
+}

+ 50 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationSecurityConfig.java

@@ -0,0 +1,50 @@
+package timing.ukulele.auth.security.type.image;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.SecurityConfigurer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.context.SecurityContextRepository;
+import timing.ukulele.auth.security.handler.LoginFailureHandler;
+import timing.ukulele.auth.security.handler.LoginSuccessHandler;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class ImageCodeAuthenticationSecurityConfig implements SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> {
+
+    private final LoginFailureHandler loginFailHandler;
+    private final LoginSuccessHandler loginSuccessHandler;
+    private final TimingUserDetailService userService;
+    private final PasswordEncoder passwordEncoder;
+
+    public ImageCodeAuthenticationSecurityConfig(LoginFailureHandler loginFailHandler,
+                                                 LoginSuccessHandler loginSuccessHandler,
+                                                 TimingUserDetailService userService,
+                                                 PasswordEncoder passwordEncoder) {
+        this.loginFailHandler = loginFailHandler;
+        this.loginSuccessHandler = loginSuccessHandler;
+        this.userService = userService;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+
+    @Override
+    public void init(HttpSecurity http) {
+
+    }
+
+    @Override
+    public void configure(HttpSecurity http) {
+        ImageCodeAuthenticationFilter authenticationFilter = new ImageCodeAuthenticationFilter();
+        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
+        authenticationFilter.setAuthenticationManager(authenticationManager);
+        authenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
+        authenticationFilter.setAuthenticationFailureHandler(loginFailHandler);
+
+        ImageCodeAuthenticationProvider authenticationProvider = new ImageCodeAuthenticationProvider(userService, passwordEncoder);
+
+        http.authenticationProvider(authenticationProvider)
+                .addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
+    }
+}

+ 47 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationToken.java

@@ -0,0 +1,47 @@
+package timing.ukulele.auth.security.type.image;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serial;
+import java.util.Collection;
+
+public class ImageCodeAuthenticationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 500L;
+    private final Object principal;
+    private Object credentials;
+
+    public ImageCodeAuthenticationToken(Object principal, Object credentials) {
+        super(null);
+        this.principal = principal;
+        this.credentials = credentials;
+        this.setAuthenticated(false);
+    }
+
+    public ImageCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        super.setAuthenticated(true);
+    }
+
+    public Object getCredentials() {
+        return credentials;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+        if (isAuthenticated) {
+            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+        } else {
+            super.setAuthenticated(false);
+        }
+    }
+
+    public void eraseCredentials() {
+        super.eraseCredentials();
+    }
+}

+ 56 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationFilter.java

@@ -0,0 +1,56 @@
+package timing.ukulele.auth.security.type.sms;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.StringUtils;
+import timing.ukulele.auth.security.LoginTypeEnum;
+
+public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
+    private boolean postOnly = true;
+    private final String mobileParameter = "mobile";
+    private final String codeParameter = "code";
+
+    public SmsCodeAuthenticationFilter() {
+        super(new AntPathRequestMatcher(LoginTypeEnum.MOBILE_CODE.getPath(), "POST"));
+    }
+
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+        if (this.postOnly && !request.getMethod().equals("POST")) {
+            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
+        } else {
+            String mobile = this.obtainMobile(request);
+            mobile = mobile != null ? mobile : "";
+            String code = this.obtainCode(request);
+            code = code != null ? code : "";
+            if (!(StringUtils.hasLength(mobile) && StringUtils.hasLength(code))) {
+                throw new AuthenticationServiceException("参数错误");
+            } else {
+                // TODO 校验短信验证码
+            }
+            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
+            this.setDetails(request, authRequest);
+            return this.getAuthenticationManager().authenticate(authRequest);
+        }
+    }
+
+    protected String obtainMobile(HttpServletRequest request) {
+        return request.getParameter(this.mobileParameter);
+    }
+
+    protected String obtainCode(HttpServletRequest request) {
+        return request.getParameter(this.codeParameter);
+    }
+
+    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
+        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+    }
+
+    public void setPostOnly(boolean postOnly) {
+        this.postOnly = postOnly;
+    }
+}

+ 38 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationProvider.java

@@ -0,0 +1,38 @@
+package timing.ukulele.auth.security.type.sms;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
+
+    private TimingUserDetailService userService;
+
+    public TimingUserDetailService getUserService() {
+        return userService;
+    }
+
+    public void setUserService(TimingUserDetailService myUserService) {
+        this.userService = myUserService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
+        UserDetails user = userService.loadUserByMobile((String) smsCodeAuthenticationToken.getPrincipal());
+        if (user == null) {
+            throw new InternalAuthenticationServiceException("无法获取用户信息");
+        }
+        SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities());
+        result.setDetails(smsCodeAuthenticationToken.getDetails());
+        return result;
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
+    }
+}

+ 47 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationSecurityConfig.java

@@ -0,0 +1,47 @@
+package timing.ukulele.auth.security.type.sms;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.SecurityConfigurer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.context.SecurityContextRepository;
+import timing.ukulele.auth.security.handler.LoginFailureHandler;
+import timing.ukulele.auth.security.handler.LoginSuccessHandler;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class SmsCodeAuthenticationSecurityConfig implements SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> {
+
+    private final LoginFailureHandler loginFailHandler;
+    private final LoginSuccessHandler loginSuccessHandler;
+    private final TimingUserDetailService userService;
+
+    public SmsCodeAuthenticationSecurityConfig(
+            LoginFailureHandler loginFailHandler,
+            LoginSuccessHandler loginSuccessHandler,
+            TimingUserDetailService userService) {
+        this.loginFailHandler = loginFailHandler;
+        this.loginSuccessHandler = loginSuccessHandler;
+        this.userService = userService;
+    }
+
+
+    @Override
+    public void init(HttpSecurity http) {
+
+    }
+
+    @Override
+    public void configure(HttpSecurity http) {
+        SmsCodeAuthenticationFilter authenticationFilter = new SmsCodeAuthenticationFilter();
+        authenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
+        authenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
+        authenticationFilter.setAuthenticationFailureHandler(loginFailHandler);
+
+        SmsCodeAuthenticationProvider authenticationProvider = new SmsCodeAuthenticationProvider();
+        authenticationProvider.setUserService(userService);
+
+        http.authenticationProvider(authenticationProvider)
+                .addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
+    }
+}

+ 45 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationToken.java

@@ -0,0 +1,45 @@
+package timing.ukulele.auth.security.type.sms;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.io.Serial;
+import java.util.Collection;
+
+public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 500L;
+    private final Object principal;
+
+    public SmsCodeAuthenticationToken(Object mobile) {
+        super(null);
+        this.principal = mobile;
+        this.setAuthenticated(false);
+    }
+
+    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        super.setAuthenticated(true);
+    }
+
+    public Object getCredentials() {
+        return null;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+        if (isAuthenticated) {
+            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+        } else {
+            super.setAuthenticated(false);
+        }
+    }
+
+    public void eraseCredentials() {
+        super.eraseCredentials();
+    }
+}

+ 59 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationFilter.java

@@ -0,0 +1,59 @@
+package timing.ukulele.auth.security.type.wechat;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.util.StringUtils;
+import timing.ukulele.auth.security.LoginTypeEnum;
+import timing.ukulele.auth.security.ThirdTypeEnum;
+
+public class ThirdQrAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
+    private boolean postOnly = true;
+    private final String thirdTypeParameter = "thirdType";
+    private final String codeParameter = "code";
+
+    public ThirdQrAuthenticationFilter() {
+        super(new AntPathRequestMatcher(LoginTypeEnum.THIRD_QR.getPath(), "POST"));
+    }
+
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+        if (this.postOnly && !request.getMethod().equals("POST")) {
+            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
+        } else {
+            String code = this.obtainCode(request);
+            code = code != null ? code : "";
+            ThirdTypeEnum type = this.obtainThirdType(request);
+            if (type == null) {
+                throw new InsufficientAuthenticationException("暂不支持该平台");
+            }
+            if (!StringUtils.hasLength(code)) {
+                throw new AuthenticationServiceException("参数错误");
+            }
+            ThirdQrAuthenticationToken authRequest = new ThirdQrAuthenticationToken(code);
+            this.setDetails(request, authRequest);
+            return this.getAuthenticationManager().authenticate(authRequest);
+        }
+    }
+
+    protected ThirdTypeEnum obtainThirdType(HttpServletRequest request) {
+        String parameter = request.getParameter(this.thirdTypeParameter);
+        return ThirdTypeEnum.getByCode(parameter);
+    }
+
+    protected String obtainCode(HttpServletRequest request) {
+        return request.getParameter(this.codeParameter);
+    }
+
+    protected void setDetails(HttpServletRequest request, ThirdQrAuthenticationToken authRequest) {
+        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+    }
+
+    public void setPostOnly(boolean postOnly) {
+        this.postOnly = postOnly;
+    }
+}

+ 40 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationProvider.java

@@ -0,0 +1,40 @@
+package timing.ukulele.auth.security.type.wechat;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.InternalAuthenticationServiceException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import timing.ukulele.auth.security.ThirdTypeEnum;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class ThirdQrAuthenticationProvider implements AuthenticationProvider {
+
+    private TimingUserDetailService userService;
+
+    public TimingUserDetailService getUserService() {
+        return userService;
+    }
+
+    public void setUserService(TimingUserDetailService myUserService) {
+        this.userService = myUserService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        ThirdQrAuthenticationToken authenticationToken = (ThirdQrAuthenticationToken) authentication;
+        ThirdTypeEnum thirdType = authenticationToken.getThirdType();
+        UserDetails user = userService.loadUserByUsername((String) authenticationToken.getPrincipal());
+        if (user == null) {
+            throw new InternalAuthenticationServiceException("无法获取用户信息");
+        }
+        ThirdQrAuthenticationToken result = new ThirdQrAuthenticationToken(user, user.getAuthorities());
+        result.setDetails(authenticationToken.getDetails());
+        return result;
+    }
+
+    @Override
+    public boolean supports(Class<?> aClass) {
+        return ThirdQrAuthenticationToken.class.isAssignableFrom(aClass);
+    }
+}

+ 47 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationSecurityConfig.java

@@ -0,0 +1,47 @@
+package timing.ukulele.auth.security.type.wechat;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.SecurityConfigurer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.DefaultSecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.context.SecurityContextRepository;
+import timing.ukulele.auth.security.handler.LoginFailureHandler;
+import timing.ukulele.auth.security.handler.LoginSuccessHandler;
+import timing.ukulele.auth.security.service.TimingUserDetailService;
+
+public class ThirdQrAuthenticationSecurityConfig implements SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> {
+
+    private final LoginFailureHandler loginFailHandler;
+    private final LoginSuccessHandler loginSuccessHandler;
+    private final TimingUserDetailService userService;
+
+    public ThirdQrAuthenticationSecurityConfig(
+            LoginFailureHandler loginFailHandler,
+            LoginSuccessHandler loginSuccessHandler,
+            TimingUserDetailService userService) {
+        this.loginFailHandler = loginFailHandler;
+        this.loginSuccessHandler = loginSuccessHandler;
+        this.userService = userService;
+    }
+
+
+    @Override
+    public void init(HttpSecurity http) {
+
+    }
+
+    @Override
+    public void configure(HttpSecurity http) {
+        ThirdQrAuthenticationFilter authenticationFilter = new ThirdQrAuthenticationFilter();
+        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
+        authenticationFilter.setAuthenticationManager(authenticationManager);
+        authenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
+        authenticationFilter.setAuthenticationFailureHandler(loginFailHandler);
+        ThirdQrAuthenticationProvider authenticationProvider = new ThirdQrAuthenticationProvider();
+        authenticationProvider.setUserService(userService);
+
+        http.authenticationProvider(authenticationProvider)
+                .addFilterAt(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
+    }
+}

+ 60 - 0
security-auth/src/main/java/timing/ukulele/auth/security/type/wechat/ThirdQrAuthenticationToken.java

@@ -0,0 +1,60 @@
+package timing.ukulele.auth.security.type.wechat;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import timing.ukulele.auth.security.ThirdTypeEnum;
+
+import java.io.Serial;
+import java.util.Collection;
+
+public class ThirdQrAuthenticationToken extends AbstractAuthenticationToken {
+    @Serial
+    private static final long serialVersionUID = 500L;
+    private final Object principal;
+    private final ThirdTypeEnum thirdType;
+
+    public ThirdQrAuthenticationToken(Object principal) {
+        super(null);
+        this.principal = principal;
+        this.thirdType = null;
+        this.setAuthenticated(false);
+    }
+
+    public ThirdQrAuthenticationToken(Object principal, ThirdTypeEnum thirdType) {
+        super(null);
+        this.principal = principal;
+        this.thirdType = thirdType;
+        this.setAuthenticated(false);
+    }
+
+    public ThirdQrAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
+        super(authorities);
+        this.principal = principal;
+        this.thirdType = null;
+        super.setAuthenticated(true);
+    }
+
+    public Object getCredentials() {
+        return null;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+        if (isAuthenticated) {
+            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+        } else {
+            super.setAuthenticated(false);
+        }
+    }
+
+    public ThirdTypeEnum getThirdType() {
+        return thirdType;
+    }
+
+    public void eraseCredentials() {
+        super.eraseCredentials();
+    }
+}

+ 323 - 0
security-auth/src/main/java/timing/ukulele/auth/support/RedisOperator.java

@@ -0,0 +1,323 @@
+package timing.ukulele.auth.support;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import timing.ukulele.auth.util.JsonUtils;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis操作类
+ *
+ * @param <V> value的类型
+ */
+@Component
+@RequiredArgsConstructor
+public class RedisOperator<V> {
+
+    private final RedisTemplate<Object, V> redisTemplate;
+
+    private final RedisTemplate<Object, Object> redisHashTemplate;
+
+    /**
+     * 设置key的过期时间
+     *
+     * @param key     缓存key
+     * @param timeout 存活时间
+     * @param unit    时间单位
+     */
+    public void setExpire(String key, long timeout, TimeUnit unit) {
+        redisHashTemplate.expire(key, timeout, unit);
+    }
+
+    /**
+     * 根据key删除缓存
+     *
+     * @param keys 要删除的key,可变参数列表
+     * @return 删除的缓存数量
+     */
+    public Long delete(String... keys) {
+        if (ObjectUtils.isEmpty(keys)) {
+            return 0L;
+        }
+        return redisTemplate.delete(Arrays.asList(keys));
+    }
+
+    /**
+     * 存入值
+     *
+     * @param key   缓存中的key
+     * @param value 存入的value
+     */
+    public void set(String key, V value) {
+        valueOperations().set(key, value);
+    }
+
+    /**
+     * 根据key取值
+     *
+     * @param key 缓存中的key
+     * @return 返回键值对应缓存
+     */
+    public V get(String key) {
+        return valueOperations().get(key);
+    }
+
+    /**
+     * 设置键值并设置过期时间
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeout 过期时间
+     * @param unit    过期时间的单位
+     */
+    public void set(String key, V value, long timeout, TimeUnit unit) {
+        valueOperations().set(key, value, timeout, unit);
+    }
+
+    /**
+     * 设置键值并设置过期时间(单位秒)
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeout 过期时间,单位:秒
+     */
+    public void set(String key, V value, long timeout) {
+        this.set(key, value, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 根据key获取缓存并删除缓存
+     *
+     * @param key 要获取缓存的key
+     * @return key对应的缓存
+     */
+    public V getAndDelete(String key) {
+        if (ObjectUtils.isEmpty(key)) {
+            return null;
+        }
+        V value = valueOperations().get(key);
+        this.delete(key);
+        return value;
+    }
+
+    /**
+     * 往hash类型的数据中存值
+     *
+     * @param key   缓存中的key
+     * @param field hash结构的key
+     * @param value 存入的value
+     */
+    public void setHash(String key, String field, V value) {
+        hashOperations().put(key, field, value);
+    }
+
+    /**
+     * 根据key取值
+     *
+     * @param key 缓存中的key
+     * @return 缓存key对应的hash数据中field属性的值
+     */
+    public Object getHash(String key, String field) {
+        return hashOperations().hasKey(key, field) ? hashOperations().get(key, field) : null;
+    }
+
+    /**
+     * 以hash格式存入redis
+     *
+     * @param key   缓存中的key
+     * @param value 存入的对象
+     */
+    public void setHashAll(String key, Object value) {
+        Map<String, Object> map = JsonUtils.objectCovertToObject(value, Map.class, String.class, Object.class);
+        hashOperations().putAll(key, map);
+    }
+
+    /**
+     * 设置键值并设置过期时间
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeout 过期时间
+     * @param unit    过期时间的单位
+     */
+    public void setHashAll(String key, Object value, long timeout, TimeUnit unit) {
+        this.setHashAll(key, value);
+        this.setExpire(key, timeout, unit);
+    }
+
+    /**
+     * 设置键值并设置过期时间(单位秒)
+     *
+     * @param key     键
+     * @param value   值
+     * @param timeout 过期时间,单位:秒
+     */
+    public void setHashAll(String key, Object value, long timeout) {
+        this.setHashAll(key, value, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 从redis中获取hash类型数据
+     *
+     * @param key 缓存中的key
+     * @return redis 中hash数据
+     */
+    public Map<String, Object> getMapHashAll(String key) {
+        return hashOperations().entries(key);
+    }
+
+    /**
+     * 根据指定clazz类型从redis中获取对应的实例
+     *
+     * @param key   缓存key
+     * @param clazz hash对应java类的class
+     * @param <T>   redis中hash对应的java类型
+     * @return clazz实例
+     */
+    public <T> T getHashAll(String key, Class<T> clazz) {
+        Map<String, Object> entries = hashOperations().entries(key);
+        if (ObjectUtils.isEmpty(entries)) {
+            return null;
+        }
+        return JsonUtils.objectCovertToObject(entries, clazz);
+    }
+
+    /**
+     * 根据key删除缓存
+     *
+     * @param key    要删除的key
+     * @param fields key对应的hash数据的键值(HashKey),可变参数列表
+     * @return hash删除的属性数量
+     */
+    public Long deleteHashField(String key, String... fields) {
+        if (ObjectUtils.isEmpty(key) || ObjectUtils.isEmpty(fields)) {
+            return 0L;
+        }
+        return hashOperations().delete(key, (Object[]) fields);
+    }
+
+    /**
+     * 将value添加至key对应的列表中
+     *
+     * @param key   缓存key
+     * @param value 值
+     */
+    public void listPush(String key, V value) {
+        listOperations().rightPush(key, value);
+    }
+
+    /**
+     * 将value添加至key对应的列表中,并添加过期时间
+     *
+     * @param key     缓存key
+     * @param value   值
+     * @param timeout key的存活时间
+     * @param unit    时间单位
+     */
+    public void listPush(String key, V value, long timeout, TimeUnit unit) {
+        listOperations().rightPush(key, value);
+        this.setExpire(key, timeout, unit);
+    }
+
+    /**
+     * 将value添加至key对应的列表中,并添加过期时间
+     * 默认单位是秒(s)
+     *
+     * @param key     缓存key
+     * @param value   值
+     * @param timeout key的存活时间
+     */
+    public void listPush(String key, V value, long timeout) {
+        this.listPush(key, value, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 将传入的参数列表添加至key的列表中
+     *
+     * @param key    缓存key
+     * @param values 值列表
+     * @return 存入数据的长度
+     */
+    public Long listPushAll(String key, Collection<V> values) {
+        return listOperations().rightPushAll(key, values);
+    }
+
+    /**
+     * 将传入的参数列表添加至key的列表中,并设置key的存活时间
+     *
+     * @param key     缓存key
+     * @param values  值列表
+     * @param timeout key的存活时间
+     * @param unit    时间单位
+     * @return 存入数据的长度
+     */
+    public Long listPushAll(String key, Collection<V> values, long timeout, TimeUnit unit) {
+        Long count = listOperations().rightPushAll(key, values);
+        this.setExpire(key, timeout, unit);
+        return count;
+    }
+
+    /**
+     * 将传入的参数列表添加至key的列表中,并设置key的存活时间
+     * 默认单位是秒(s)
+     *
+     * @param key     缓存key
+     * @param values  值列表
+     * @param timeout key的存活时间
+     * @return 存入数据的长度
+     */
+    public Long listPushAll(String key, Collection<V> values, long timeout) {
+        return this.listPushAll(key, values, timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 根据key获取list列表
+     *
+     * @param key 缓存key
+     * @return key对应的list列表
+     */
+    public Collection<V> getList(String key) {
+        Long size = listOperations().size(key);
+        if (size == null || size == 0) {
+            return null;
+        }
+        return listOperations().range(key, 0, (size - 1));
+    }
+
+    /**
+     * value操作集
+     *
+     * @return ValueOperations
+     */
+    private ValueOperations<Object, V> valueOperations() {
+        return redisTemplate.opsForValue();
+    }
+
+    /**
+     * hash操作集
+     *
+     * @return ValueOperations
+     */
+    private HashOperations<Object, String, Object> hashOperations() {
+        return redisHashTemplate.opsForHash();
+    }
+
+    /**
+     * hash操作集
+     *
+     * @return ValueOperations
+     */
+    private ListOperations<Object, V> listOperations() {
+        return redisTemplate.opsForList();
+    }
+
+}

+ 104 - 0
security-auth/src/main/java/timing/ukulele/auth/support/RedisSessionSecurityContextRepository.java

@@ -0,0 +1,104 @@
+package timing.ukulele.auth.support;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.context.DeferredSecurityContext;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.web.context.HttpRequestResponseHolder;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import timing.ukulele.auth.security.model.SupplierDeferredSecurityContext;
+
+import java.util.function.Supplier;
+
+import static timing.ukulele.auth.constant.RedisConstants.DEFAULT_TIMEOUT_SECONDS;
+import static timing.ukulele.auth.constant.RedisConstants.SECURITY_CONTEXT_PREFIX_KEY;
+
+/**
+ * 基于redis存储认证信息
+ *
+ * @author vains
+ */
+@Component
+@RequiredArgsConstructor
+public class RedisSessionSecurityContextRepository implements SecurityContextRepository {
+
+    private final RedisOperator<SecurityContext> redisOperator;
+
+    private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
+            .getContextHolderStrategy();
+
+
+    @Override
+    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
+        // 方法已过时,使用 loadDeferredContext 方法
+        throw new UnsupportedOperationException("Method deprecated.");
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+        String session = getSession(request);
+        if (ObjectUtils.isEmpty(session)) {
+            return;
+        }
+
+        // 如果当前的context是空的,则移除
+        SecurityContext emptyContext = this.securityContextHolderStrategy.createEmptyContext();
+        if (emptyContext.equals(context)) {
+            redisOperator.delete((SECURITY_CONTEXT_PREFIX_KEY + session));
+        } else {
+            // 保存认证信息
+            redisOperator.set((SECURITY_CONTEXT_PREFIX_KEY + session), context, DEFAULT_TIMEOUT_SECONDS);
+        }
+    }
+
+    @Override
+    public boolean containsContext(HttpServletRequest request) {
+        String session = getSession(request);
+        if (ObjectUtils.isEmpty(session)) {
+            return false;
+        }
+        // 检验当前请求是否有认证信息
+        return redisOperator.get((SECURITY_CONTEXT_PREFIX_KEY + session)) != null;
+    }
+
+    @Override
+    public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
+        Supplier<SecurityContext> supplier = () -> readSecurityContextFromRedis(request);
+        return new SupplierDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
+    }
+
+    /**
+     * 从redis中获取认证信息
+     *
+     * @param request 当前请求
+     * @return 认证信息
+     */
+    private SecurityContext readSecurityContextFromRedis(HttpServletRequest request) {
+        if (request == null) {
+            return null;
+        }
+
+        String session = getSession(request);
+        if (ObjectUtils.isEmpty(session)) {
+            return null;
+        }
+
+        // 根据缓存id获取认证信息
+        return redisOperator.get((SECURITY_CONTEXT_PREFIX_KEY + session));
+    }
+
+    private String getSession(HttpServletRequest request) {
+        HttpSession session = request.getSession(Boolean.FALSE);
+        if (session != null) {
+            return session.getId();
+        }
+        return null;
+    }
+
+}

+ 167 - 0
security-auth/src/main/java/timing/ukulele/auth/util/JsonUtils.java

@@ -0,0 +1,167 @@
+package timing.ukulele.auth.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+
+/**
+ * <p>
+ * JSON与对象互转帮助类
+ * </p>
+ */
+@Slf4j
+public class JsonUtils {
+
+    private JsonUtils() {
+        // 禁止实例化工具类
+        throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
+    }
+
+    /**
+     * 设置为public是为了提供给redis的序列化器
+     */
+    private final static ObjectMapper MAPPER = new ObjectMapper();
+
+    static {
+        // 对象的所有字段全部列入,还是其他的选项,可以忽略null等
+        MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
+        // 取消默认的时间转换为timeStamp格式
+        MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+        // 设置Date类型的序列化及反序列化格式
+        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+        // 忽略空Bean转json的错误
+        MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        // 忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
+        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        // 添加java8序列化支持和新版时间对象序列化支持
+        MAPPER.registerModule(new Jdk8Module());
+        MAPPER.registerModule(new JavaTimeModule());
+    }
+
+    /**
+     * json字符串转为对象
+     *
+     * @param json  json
+     * @param clazz T类的class文件
+     * @param <T>   泛型, 代表返回参数的类型
+     * @return 返回T的实例
+     */
+    public static <T> T jsonCovertToObject(String json, Class<T> clazz) {
+        if (json == null || clazz == null) {
+            return null;
+        }
+        try {
+            return MAPPER.readValue(json, clazz);
+        } catch (IOException e) {
+            log.error("json转换失败,原因:", e);
+        }
+        return null;
+    }
+
+    /**
+     * json字符串转为对象
+     *
+     * @param json json
+     * @param type 对象在Jackson中的类型
+     * @param <T>  泛型, 代表返回参数的类型
+     * @return 返回T的实例
+     */
+    public static <T> T jsonCovertToObject(String json, TypeReference<T> type) {
+        if (json == null || type == null) {
+            return null;
+        }
+        try {
+            return MAPPER.readValue(json, type);
+        } catch (IOException e) {
+            log.error("json转换失败,原因:", e);
+        }
+        return null;
+    }
+
+    /**
+     * 将流中的数据转为java对象
+     *
+     * @param inputStream 输入流
+     * @param clazz       类的class
+     * @param <T>         泛型, 代表返回参数的类型
+     * @return 返回对象 如果参数任意一个为 null则返回null
+     */
+    public static <T> T covertStreamToObject(InputStream inputStream, Class<T> clazz) {
+        if (inputStream == null || clazz == null) {
+            return null;
+        }
+        try {
+            return MAPPER.readValue(inputStream, clazz);
+        } catch (IOException e) {
+            log.error("json转换失败,原因:", e);
+        }
+        return null;
+    }
+
+    /**
+     * json字符串转为复杂类型List
+     *
+     * @param json            json
+     * @param collectionClazz 集合的class
+     * @param elementsClazz   集合中泛型的class
+     * @param <T>             泛型, 代表返回参数的类型
+     * @return 返回T的实例
+     */
+    public static <T> T jsonCovertToObject(String json, Class<?> collectionClazz, Class<?>... elementsClazz) {
+        if (json == null || collectionClazz == null || elementsClazz == null || elementsClazz.length == 0) {
+            return null;
+        }
+        try {
+            JavaType javaType = MAPPER.getTypeFactory().constructParametricType(collectionClazz, elementsClazz);
+            return MAPPER.readValue(json, javaType);
+        } catch (IOException e) {
+            log.error("json转换失败,原因:", e);
+        }
+        return null;
+    }
+
+    /**
+     * 对象转为json字符串
+     *
+     * @param o 将要转化的对象
+     * @return 返回json字符串
+     */
+    public static String objectCovertToJson(Object o) {
+        if (o == null) {
+            return null;
+        }
+        try {
+            return o instanceof String ? (String) o : MAPPER.writeValueAsString(o);
+        } catch (IOException e) {
+            log.error("json转换失败,原因:", e);
+        }
+        return null;
+    }
+
+    /**
+     * 将对象转为另一个对象
+     * 切记,两个对象结构要一致
+     * 多用于Object转为具体的对象
+     *
+     * @param o               将要转化的对象
+     * @param collectionClazz 集合的class
+     * @param elementsClazz   集合中泛型的class
+     * @param <T>             泛型, 代表返回参数的类型
+     * @return 返回T的实例
+     */
+    public static <T> T objectCovertToObject(Object o, Class<?> collectionClazz, Class<?>... elementsClazz) {
+        String json = objectCovertToJson(o);
+        return jsonCovertToObject(json, collectionClazz, elementsClazz);
+    }
+
+}

+ 32 - 0
security-auth/src/main/resources/application.yml

@@ -0,0 +1,32 @@
+spring:
+  security:
+    oauth2:
+      authorizationserver:
+        issuer: http://www.test.com:8080
+  jackson:
+    default-property-inclusion: non_null
+  data:
+    redis:
+      url: redis://127.0.0.1:6379
+
+timing:
+  # 自定义认证配置
+  security:
+    # 登录页面路径
+    login-url: http://www.test.com:5173/login
+    # 授权确认页面路径
+    consent-page-uri: http://www.test.com:5173/consent
+    # 设备码验证页面
+    device-activate-uri: http://www.test.com:5173/activate
+    # 设备码验证成功页面
+    device-activated-uri: http://www.test.com:5173/activated
+    # 不需要认证的地址
+    ignore-uri-list: /authentication/**,/oauth2/consent/parameters,/login, /getCaptcha, /getSmsCaptcha, /error, /favicon.ico
+    # 当前认证服务访问的路径
+    issuer-url: http://www.test.com:8080
+
+server:
+  servlet:
+    session:
+      cookie:
+        domain: www.test.com

+ 48 - 0
security-auth/target/classes/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,48 @@
+{
+  "groups": [
+    {
+      "name": "timing.security",
+      "type": "timing.ukulele.auth.config.property.TimingSecurityProperties",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    }
+  ],
+  "properties": [
+    {
+      "name": "timing.security.consent-page-uri",
+      "type": "java.lang.String",
+      "description": "授权确认页面 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!! 错误e.g. http:\/\/当前项目IP:当前项目端口\/oauth2\/consent",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    },
+    {
+      "name": "timing.security.device-activate-uri",
+      "type": "java.lang.String",
+      "description": "授权码验证页面 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!! 错误e.g. http:\/\/当前项目IP:当前项目端口\/activate",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    },
+    {
+      "name": "timing.security.device-activated-uri",
+      "type": "java.lang.String",
+      "description": "授权码验证成功后页面 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!! 错误e.g. http:\/\/当前项目IP:当前项目端口\/activated",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    },
+    {
+      "name": "timing.security.ignore-uri-list",
+      "type": "java.util.List<java.lang.String>",
+      "description": "不需要认证的路径",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    },
+    {
+      "name": "timing.security.issuer-url",
+      "type": "java.lang.String",
+      "description": "设置token签发地址(http(s):\/\/{ip}:{port}\/context-path, http(s):\/\/domain.com\/context-path) 如果需要通过ip访问这里就是ip,如果是有域名映射就填域名,通过什么方式访问该服务这里就填什么",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    },
+    {
+      "name": "timing.security.login-url",
+      "type": "java.lang.String",
+      "description": "登录页面地址 注意:不是前后端分离的项目不要写完整路径,当前项目部署的IP也不行!!! 错误e.g. http:\/\/当前项目IP:当前项目端口\/login",
+      "sourceType": "timing.ukulele.auth.config.property.TimingSecurityProperties"
+    }
+  ],
+  "hints": []
+}

+ 32 - 0
security-auth/target/classes/application.yml

@@ -0,0 +1,32 @@
+spring:
+  security:
+    oauth2:
+      authorizationserver:
+        issuer: http://www.test.com:8080
+  jackson:
+    default-property-inclusion: non_null
+  data:
+    redis:
+      url: redis://127.0.0.1:6379
+
+timing:
+  # 自定义认证配置
+  security:
+    # 登录页面路径
+    login-url: http://www.test.com:5173/login
+    # 授权确认页面路径
+    consent-page-uri: http://www.test.com:5173/consent
+    # 设备码验证页面
+    device-activate-uri: http://www.test.com:5173/activate
+    # 设备码验证成功页面
+    device-activated-uri: http://www.test.com:5173/activated
+    # 不需要认证的地址
+    ignore-uri-list: /authentication/**,/oauth2/consent/parameters,/login, /getCaptcha, /getSmsCaptcha, /error, /favicon.ico
+    # 当前认证服务访问的路径
+    issuer-url: http://www.test.com:8080
+
+server:
+  servlet:
+    session:
+      cookie:
+        domain: www.test.com

BIN
security-auth/target/classes/timing/ukulele/auth/AuthorizationApplication.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/AuthorizationConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/AuthorizationUtil.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/JwtAuthorizationToken.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationConverter.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationProvider.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/device/DeviceClientAuthenticationToken.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/filter/JWTAuthorizationFilter.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/handler/ConsentAuthenticationFailureHandler.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/handler/ConsentAuthorizationResponseHandler.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/handler/DeviceAuthorizationResponseHandler.class


BIN
security-auth/target/classes/timing/ukulele/auth/authorization/handler/LoginTargetAuthenticationEntryPoint.class


BIN
security-auth/target/classes/timing/ukulele/auth/config/BeanConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/config/RedisConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/config/SecurityConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/config/property/TimingSecurityProperties.class


BIN
security-auth/target/classes/timing/ukulele/auth/constant/RedisConstants.class


BIN
security-auth/target/classes/timing/ukulele/auth/constant/SecurityConstants.class


BIN
security-auth/target/classes/timing/ukulele/auth/controller/AuthorizationController$ScopeWithDescription.class


BIN
security-auth/target/classes/timing/ukulele/auth/controller/AuthorizationController.class


BIN
security-auth/target/classes/timing/ukulele/auth/controller/CommonController.class


BIN
security-auth/target/classes/timing/ukulele/auth/controller/TestController.class


BIN
security-auth/target/classes/timing/ukulele/auth/http/ExchangeBeanConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/http/wechat/WeChatExchange.class


BIN
security-auth/target/classes/timing/ukulele/auth/model/CaptchaResult.class


BIN
security-auth/target/classes/timing/ukulele/auth/model/Result.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/LoginTypeEnum.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/SecurityUtils.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/ThirdTypeEnum.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/filter/JWTSecurityFilter.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/handler/LoginFailureHandler.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/handler/LoginSuccessHandler.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/model/CustomGrantedAuthority.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/model/SupplierDeferredSecurityContext.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/service/TimingUserDetailService.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationFilter.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationProvider.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationSecurityConfig.class


BIN
security-auth/target/classes/timing/ukulele/auth/security/type/image/ImageCodeAuthenticationToken.class


+ 0 - 0
security-auth/target/classes/timing/ukulele/auth/security/type/sms/SmsCodeAuthenticationFilter.class


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