commit 5d287adba3a89834a1a649ed09e1dd2faf51c60e Author: wangjianhong <546732225seven@gmail.com> Date: Sun Jul 13 21:17:16 2025 +0800 init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..2f94e61 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.10/apache-maven-3.9.10-bin.zip diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..402f531 --- /dev/null +++ b/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.3 + + + com.arrokoth + authorization-server-standalone + 0.0.1-SNAPSHOT + authorization-server-standalone + authorization-server-standalone + + + + + + + + + + + + + + + UTF-8 + UTF-8 + UTF-8 + 17 + 17 + 3.2.0 + 2.0.9 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + + + + + cn.hutool + hutool-all + 5.8.39 + + + + + + org.apache.commons + commons-pool2 + 2.11.1 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.4.0 + + + + + org.springframework.session + spring-session-data-redis + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + build-info + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + true + 17 + 17 + true + true + UTF-8 + false + + + + org.apache.maven.plugins + maven-surefire-plugin + + 2.22.0 + + + + + diff --git a/src/main/java/com/arrokoth/standalone/authorization/StandaloneServerApplication.java b/src/main/java/com/arrokoth/standalone/authorization/StandaloneServerApplication.java new file mode 100644 index 0000000..df45f88 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/StandaloneServerApplication.java @@ -0,0 +1,13 @@ +package com.arrokoth.standalone.authorization; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class StandaloneServerApplication { + + public static void main(String[] args) { + SpringApplication.run(StandaloneServerApplication.class, args); + } + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/config/AuthorizationServerAutoConfigurer.java b/src/main/java/com/arrokoth/standalone/authorization/config/AuthorizationServerAutoConfigurer.java new file mode 100644 index 0000000..190f897 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/config/AuthorizationServerAutoConfigurer.java @@ -0,0 +1,94 @@ +package com.arrokoth.standalone.authorization.config; + +import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties; +import com.arrokoth.standalone.authorization.properties.SecurityWebProperties; +import lombok.AllArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; + +@Configuration +@EnableWebSecurity +@AllArgsConstructor +@EnableMethodSecurity +@EnableConfigurationProperties(AuthorizationServerProperties.class) +public class AuthorizationServerAutoConfigurer { + + private AuthorizationServerProperties authorizationServerProperties; + private SecurityWebProperties securityWebProperties; + + + /** + * 配置端点的过滤器链 + * + * @param http spring security核心配置类 + * @return 过滤器链 + * @throws Exception 抛出 + */ + @Bean + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + // 1. 配置 OAuth2 Authorization Server 端点 + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); + // 配置授权确认页面路径 + authorizationServerConfigurer.authorizationEndpoint( + authorizationEndpoint -> authorizationEndpoint.consentPage(authorizationServerProperties.getConsentPage())); + + // 开始构建 HTTP 安全配置 + http + .csrf(AbstractHttpConfigurer::disable) // 暂时禁用 CSRF 保护(可根据需要启用) + // 仅匹配 OAuth2 授权服务器端点(如 /oauth2/authorize, /token 等) + .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) + // 请求授权规则:所有匹配该过滤链的请求都必须经过身份验证 + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + // 配置 OAuth2 授权服务器核心功能,启用 OpenID Connect 支持 + .with(authorizationServerConfigurer, (authorizationServer) -> + authorizationServer.oidc(Customizer.withDefaults()) + ); + // 2. 异常处理:未登录访问认证端点时跳转到登录页 + http.exceptionHandling(exceptions -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ); + + // 3. Resource Server 配置(用于支持 UserInfo 和 ClientRegistration 端点) + http.oauth2ResourceServer(oauth2 -> oauth2 + .jwt(Customizer.withDefaults()) + ); + + return http.build(); + } + + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder() + .authorizationEndpoint(authorizationServerProperties.getAuthorizationEndpoint()) // 授权端点路径 + .issuer(authorizationServerProperties.getIssuer()) // 授权服务器的唯一标识符(Issuer) + .build(); + } + + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); // 仅用于演示,生产环境请使用BCryptPasswordEncoder + } + + +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/config/CorsConfig.java b/src/main/java/com/arrokoth/standalone/authorization/config/CorsConfig.java new file mode 100644 index 0000000..485f67a --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/config/CorsConfig.java @@ -0,0 +1,77 @@ +package com.arrokoth.standalone.authorization.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * CorsConfig 是一个 Spring 配置类。 + * 用于全局配置跨域资源共享(CORS),允许前端应用访问后端 API。 + * + * 此配置适用于前后端分离架构中常见的跨域请求问题。 + */ +@Slf4j // 使用 Lombok 提供的日志记录器 +@Configuration // 标记为 Spring 配置类 +public class CorsConfig { + + /** + * 默认允许的跨域源(Origin):所有域名都允许访问。 + */ + private static final String DEFAULT_ALLOWED_ORIGINS = "*"; + + /** + * 默认允许的请求头(Headers):接受所有类型的请求头。 + */ + private static final String DEFAULT_ALLOWED_HEADERS = "*"; + + /** + * 默认允许的 HTTP 方法:包括 OPTIONS, HEAD, GET, POST, PUT, DELETE, PATCH。 + */ + private static final String DEFAULT_ALLOWED_METHODS = "OPTIONS,HEAD,GET,POST,PUT,DELETE,PATCH"; + + /** + * 预检请求(preflight)的最大缓存时间(单位:秒),默认为 3600 秒(1 小时)。 + */ + private static final String DEFAULT_MAX_AGE = "3600"; + + /** + * 构建并返回一个 CORS 配置对象。 + * 该方法设置跨域请求的基本策略: + * - 允许任何来源 + * - 允许任何请求头 + * - 允许指定的 HTTP 方法 + * - 设置预检请求最大存活时间 + * + * @return CorsConfiguration 实例 + */ + private CorsConfiguration buildConfig() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin(DEFAULT_ALLOWED_ORIGINS); // 允许任意来源 + corsConfiguration.addAllowedHeader(DEFAULT_ALLOWED_HEADERS); // 允许任意请求头 + corsConfiguration.addAllowedMethod(DEFAULT_ALLOWED_METHODS); // 允许指定的方法 + corsConfiguration.setMaxAge(Long.parseLong(DEFAULT_MAX_AGE)); // 设置预检请求缓存时间 + return corsConfiguration; + } + + /** + * 创建并注册一个全局的 CorsFilter Bean。 + * 该过滤器将上述 CORS 策略应用到所有路径(/**)上。 + * + * @return CorsFilter 实例 + */ + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", buildConfig()); // 对所有路径启用 CORS 配置 + + // 记录日志信息 + log.debug("Initializing CorsFilter , the default allowed origins is `{}`," + + " allowed headers is `{}`, allowed methods is `{}`, max age is `{}`", + DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HEADERS, DEFAULT_ALLOWED_METHODS, DEFAULT_MAX_AGE); + + return new CorsFilter(source); + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/config/RedisSessionConfig.java b/src/main/java/com/arrokoth/standalone/authorization/config/RedisSessionConfig.java new file mode 100644 index 0000000..39f126c --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/config/RedisSessionConfig.java @@ -0,0 +1,17 @@ +package com.arrokoth.standalone.authorization.config; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.session.FlushMode; +import org.springframework.session.SaveMode; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; + +@Configuration +@EnableRedisHttpSession( + maxInactiveIntervalInSeconds = 3600, // 1小时过期 + redisNamespace = "spring:session", // 自定义命名空间 + flushMode = FlushMode.IMMEDIATE, + saveMode = SaveMode.ALWAYS +) +public class RedisSessionConfig { +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/config/SecurityWebAutoConfigurer.java b/src/main/java/com/arrokoth/standalone/authorization/config/SecurityWebAutoConfigurer.java new file mode 100644 index 0000000..c85d61d --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/config/SecurityWebAutoConfigurer.java @@ -0,0 +1,104 @@ +package com.arrokoth.standalone.authorization.config; + +import com.arrokoth.standalone.authorization.filter.JwtRequestFilter; +import com.arrokoth.standalone.authorization.properties.AuthorizationServerProperties; +import com.arrokoth.standalone.authorization.properties.SecurityWebProperties; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Configuration +@AllArgsConstructor +@EnableConfigurationProperties(SecurityWebProperties.class) +public class SecurityWebAutoConfigurer { + + private final SecurityWebProperties securityWebProperties; + private final AuthorizationServerProperties authorizationServerProperties; + +// @Bean +// public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { +// http +// .securityMatcher("/**") +// .authorizeHttpRequests(auth -> auth +// .requestMatchers(HttpMethod.OPTIONS).permitAll() +// .requestMatchers(securityWebProperties.getMergedPermittedUrls().toArray(new String[0])).permitAll() +// .anyRequest().authenticated() +// ) +// .formLogin(form -> form +// .loginPage(authorizationServerProperties.getLoginPage()) +// .permitAll() +// ) +// .logout(logout -> logout +// .logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl()) +// .permitAll() +// ); +// +// return http.build(); +// } + + + private final JwtRequestFilter jwtRequestFilter; // 你的 Token 过滤器 + + @Bean + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + log.debug("Configuring default security filter chain"); + http + .csrf(AbstractHttpConfigurer::disable) // 前后端分离通常关闭CSRF + .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用 CORS 并使用自定义配置 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) // 无状态Session + .authorizeHttpRequests(auth -> auth + .requestMatchers(HttpMethod.OPTIONS).permitAll() + .requestMatchers(securityWebProperties.getMergedPermittedUrls().toArray(new String[0])).permitAll() + .anyRequest().authenticated()) + .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // 插入Token验证Filter + + http.formLogin(form -> form + .loginPage(securityWebProperties.getLoginPage()) + .loginProcessingUrl(securityWebProperties.getLoginProcessingUrl()) + .permitAll() + ) + .logout(logout -> logout + .logoutSuccessUrl(securityWebProperties.getLogoutSuccessUrl()) + .permitAll() + ); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + log.info("Configuring authentication manager for authentication configuration"); + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + log.info("Configuring cors configuration source"); + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("*")); // 替换为前端域名 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With", "accept")); + configuration.setExposedHeaders(List.of("Authorization")); // 允许前端访问Authorization头 + configuration.setAllowCredentials(false); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/controller/AuthorizationController.java b/src/main/java/com/arrokoth/standalone/authorization/controller/AuthorizationController.java new file mode 100644 index 0000000..3a46ff3 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/controller/AuthorizationController.java @@ -0,0 +1,77 @@ +package com.arrokoth.standalone.authorization.controller; + +import com.arrokoth.standalone.authorization.domain.request.LoginRequest; +import com.arrokoth.standalone.authorization.domain.response.BasicUser; +import com.arrokoth.standalone.authorization.domain.response.RestResponse; +import com.arrokoth.standalone.authorization.domain.response.Token; +import com.arrokoth.standalone.authorization.properties.SecurityWebProperties; +import com.arrokoth.standalone.authorization.service.AuthorizationService; +import com.arrokoth.standalone.authorization.service.OAuth2ConsentService; +import com.arrokoth.standalone.authorization.util.JwtUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + + +@Controller +@RequiredArgsConstructor +public class AuthorizationController { + + + private final AuthorizationService authorizationService; + private final OAuth2ConsentService oAuth2ConsentService; + + + @GetMapping("/login") + public String home() { + return "login"; // 对应templates/login.html + } + + @PostMapping(SecurityWebProperties.AXIOS_LOGIN_PROCESSING_URL) + @ResponseBody + public RestResponse axiosLogin(@RequestBody LoginRequest loginRequest) { + return RestResponse.success(authorizationService.login(loginRequest)); + } + + + + + @GetMapping("/user/info") + @ResponseBody + public RestResponse getBasicUser() { + return RestResponse.success(authorizationService.getBasicUser()); + } + + + @PostMapping("/logout") + @ResponseBody + public ResponseEntity logout(HttpServletRequest request) throws Exception { + String token = JwtUtils.extractTokenFromHeader(request); + authorizationService.logout(JwtUtils.extractJti(token)); + return ResponseEntity.ok("Logged out successfully"); + } + + @GetMapping(value = "/oauth2/consent") + public String consent(Principal principal, Model model, + @RequestParam("client_id") String clientId, + @RequestParam("scope") String scope, + @RequestParam("state") String state, + @RequestParam(name = "user_code", required = false) String userCode) { + + OAuth2ConsentService.ConsentResponse viewModel = oAuth2ConsentService.getConsentDetails(principal, clientId, scope, state, userCode); + +// model.addAttribute("clientId", viewModel.clientId()); +// model.addAttribute("state", viewModel.state()); +// model.addAttribute("scopes", viewModel.scopes()); + model.addAttribute("previouslyApprovedScopes", viewModel.previouslyApprovedScopes()); +// model.addAttribute("principalName", viewModel.principalName()); +// model.addAttribute("userCode", viewModel.userCode()); +// model.addAttribute("requestURI", viewModel.requestURI()); + return "consent"; + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/controller/TestController.java b/src/main/java/com/arrokoth/standalone/authorization/controller/TestController.java new file mode 100644 index 0000000..f5a873c --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/controller/TestController.java @@ -0,0 +1,67 @@ +package com.arrokoth.standalone.authorization.controller; + + +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/test") +public class TestController { + + /** + * GET 请求测试:无参数,直接返回字符串 + */ + @GetMapping("/hello") + public String sayHello() { + return "Hello, Spring Boot!"; + } + + /** + * GET 请求测试:带路径参数 + * 示例 URL: /api/test/user/123 + */ + @GetMapping("/user/{id}") + public String getUserById(@PathVariable Long id) { + return "User ID: " + id; + } + + /** + * GET 请求测试:带查询参数 + * 示例 URL: /api/test/greet?name=John + */ + @GetMapping("/greet") + public String greetUser(@RequestParam String name) { + return "Hello, " + name + "!"; + } + + /** + * POST 请求测试:接收 JSON 数据并返回 Map 响应 + */ + @PostMapping("/echo") + public Map echoData(@RequestBody Map payload) { + Map response = new HashMap<>(); + response.put("received", true); + response.put("data", payload); + return response; + } + + /** + * PUT 请求测试:更新资源 + * 示例 URL: /api/test/update/456 + */ + @PutMapping("/update/{id}") + public String updateResource(@PathVariable String id, @RequestBody Map payload) { + return "Resource ID " + id + " updated with data: " + payload; + } + + /** + * DELETE 请求测试:删除资源 + * 示例 URL: /api/test/delete/789 + */ + @DeleteMapping("/delete/{id}") + public String deleteResource(@PathVariable String id) { + return "Resource ID " + id + " deleted."; + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/domain/request/LoginRequest.java b/src/main/java/com/arrokoth/standalone/authorization/domain/request/LoginRequest.java new file mode 100644 index 0000000..66efa4f --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/domain/request/LoginRequest.java @@ -0,0 +1,11 @@ +package com.arrokoth.standalone.authorization.domain.request; + +import lombok.Data; + +@Data +public class LoginRequest { + + + private String username; + private String password; +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/domain/response/BasicUser.java b/src/main/java/com/arrokoth/standalone/authorization/domain/response/BasicUser.java new file mode 100644 index 0000000..e49fd4c --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/domain/response/BasicUser.java @@ -0,0 +1,14 @@ +package com.arrokoth.standalone.authorization.domain.response; + +import lombok.Data; + +import java.util.List; + +@Data +public class BasicUser { + + private String username; + private String avatar; + private String introduction; + private List roles; +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/domain/response/RestResponse.java b/src/main/java/com/arrokoth/standalone/authorization/domain/response/RestResponse.java new file mode 100644 index 0000000..9752c5f --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/domain/response/RestResponse.java @@ -0,0 +1,37 @@ +package com.arrokoth.standalone.authorization.domain.response; + +import lombok.Data; + +@Data +public class RestResponse { + + private String code; + private String message; + private Long timestamp; + private T data; + + + + // 默认构造函数 + public RestResponse() { + this.timestamp = System.currentTimeMillis(); + } + + // 全参构造函数 + public RestResponse(String code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + this.timestamp = System.currentTimeMillis(); + } + + // 为了方便使用,可以添加一些静态方法来创建实例 + public static RestResponse success(T data) { + return new RestResponse<>("200", "success", data); + } + + public static RestResponse error(String code, String message) { + return new RestResponse<>(code, message, null); + } + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/domain/response/Token.java b/src/main/java/com/arrokoth/standalone/authorization/domain/response/Token.java new file mode 100644 index 0000000..ac15319 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/domain/response/Token.java @@ -0,0 +1,13 @@ +package com.arrokoth.standalone.authorization.domain.response; + +import lombok.Data; + +@Data +public class Token { + + private String access_token; + private String token_type; + private Long expires_in; + private String refresh_token; + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/filter/JwtRequestFilter.java b/src/main/java/com/arrokoth/standalone/authorization/filter/JwtRequestFilter.java new file mode 100644 index 0000000..4b385f9 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/filter/JwtRequestFilter.java @@ -0,0 +1,63 @@ +package com.arrokoth.standalone.authorization.filter; + +import com.arrokoth.standalone.authorization.store.redis.RedisTokenService; +import com.arrokoth.standalone.authorization.util.JwtUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + + +@Component +@RequiredArgsConstructor +public class JwtRequestFilter extends OncePerRequestFilter { + + private final UserDetailsService userDetailsService; + + private final RedisTokenService redisTokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + final String authorizationHeader = request.getHeader("Authorization"); + + String jwt = null; + String username = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + try { + username = JwtUtils.extractUsername(jwt); + } catch (Exception e) { + logger.warn("Failed to extract username from token", e); + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + if (redisTokenService.isBlacklisted(jwt)) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token is blacklisted"); + return; + } + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (JwtUtils.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/properties/AuthorizationServerProperties.java b/src/main/java/com/arrokoth/standalone/authorization/properties/AuthorizationServerProperties.java new file mode 100644 index 0000000..69d7485 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/properties/AuthorizationServerProperties.java @@ -0,0 +1,60 @@ +package com.arrokoth.standalone.authorization.properties; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 授权服务器相关配置属性 + */ +@Data +@ConfigurationProperties(prefix = "arrokoth.authorization.server") +public class AuthorizationServerProperties { + + private static final String DEFAULT_LOGIN_PAGE = "/login"; + private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout"; + private static final List DEFAULT_PERMIT_URLS = Arrays.asList( + "/login", "/logout", "/connect/logout", + "/assets/**", "/static/**", "/webjars/**", "/error", "/oauth2/**" + ); + /** + * 授权确认页面路径,默认为 "/oauth2/consent" + */ + private String consentPage = "/oauth2/consent"; + /** + * OAuth2 授权端点路径,默认为 "/oauth2/authorize" + */ + private String authorizationEndpoint = "/oauth2/authorize"; + /** + * JWT Issuer 值,默认为 "https://www.arrokoth-info.com" + */ + private String issuer = "https://www.arrokoth-info.com"; + /** + * 登出成功后的跳转 URL。 + * 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。 + * 如果在 application.yml 中配置了 app.security.logout-success-url,则使用配置值; + * 否则使用该默认值。 + */ + private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL; + /** + * 需要放行(无需认证即可访问)的 URL 列表。 + * 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。 + */ + private List permitUrls = new ArrayList<>(); + + + /** + * 合并默认值与自定义配置,并去重 + */ + public List getMergedPermittedUrls() { + return Stream.concat(DEFAULT_PERMIT_URLS.stream(), permitUrls.stream()) + .distinct() + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/properties/SecurityWebProperties.java b/src/main/java/com/arrokoth/standalone/authorization/properties/SecurityWebProperties.java new file mode 100644 index 0000000..819d1ca --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/properties/SecurityWebProperties.java @@ -0,0 +1,89 @@ +package com.arrokoth.standalone.authorization.properties; + + +import cn.hutool.json.JSONUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 授权服务器相关配置属性 + */ +@Slf4j +@Data +@Component +@ConfigurationProperties(prefix = "arrokoth.security.web") +public class SecurityWebProperties { + + public static final String AXIOS_LOGIN_PROCESSING_URL = "/home/login"; + + + public static final String DEFAULT_LOGIN_PROCESSING_URL = "/web/login"; + + private static final String DEFAULT_LOGOUT_SUCCESS_URL = "/login?logout"; + + private static final String DEFAULT_LOGOUT_PAGE = "/login"; + + + private static final List DEFAULT_PERMIT_URLS = Arrays.asList( + DEFAULT_LOGIN_PROCESSING_URL, + "/login", "/logout", "/connect/logout", "/home/login", + "/assets/**", "/static/**", "/webjars/**", + "/actuator/**", + "/error", "/oauth2/**" + ); + + + private static final List SWAGGER_URLS = Arrays.asList( + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**", + "/doc.html", + "/swagger-ui.html" + ); + + + /** + * 登出成功后的跳转 URL。 + * 默认值为 {@link #DEFAULT_LOGOUT_SUCCESS_URL},即 "/login?logout"。 + * 如果在 application.yml 中配置了 app.security.logout-success-url,则使用配置值; + * 否则使用该默认值。 + */ + private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL; + + + + private String loginPage = DEFAULT_LOGOUT_PAGE; + + /** + * 处理登录请求 + */ + private String loginProcessingUrl = DEFAULT_LOGIN_PROCESSING_URL; + /** + * 需要放行(无需认证即可访问)的 URL 列表。 + * 默认为空列表,由 {@link #getMergedPermittedUrls()} 方法结合默认值进行合并处理。 + */ + private List permitUrls = new ArrayList<>(); + + + /** + * 合并默认值与自定义配置,并去重 + */ + public List getMergedPermittedUrls() { + List merged = Stream.of(DEFAULT_PERMIT_URLS, SWAGGER_URLS, permitUrls) + .flatMap(List::stream) + .distinct() + .collect(Collectors.toList()); + + log.info("Merged permitted urls: {}", JSONUtil.toJsonStr(merged)); + return merged; + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/service/AuthorizationService.java b/src/main/java/com/arrokoth/standalone/authorization/service/AuthorizationService.java new file mode 100644 index 0000000..b55d394 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/service/AuthorizationService.java @@ -0,0 +1,15 @@ +package com.arrokoth.standalone.authorization.service; + +import com.arrokoth.standalone.authorization.domain.request.LoginRequest; +import com.arrokoth.standalone.authorization.domain.response.BasicUser; +import com.arrokoth.standalone.authorization.domain.response.Token; + +public interface AuthorizationService { + + + Token login(LoginRequest loginRequest); + + void logout(String jti); + + BasicUser getBasicUser(); +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/service/OAuth2ConsentService.java b/src/main/java/com/arrokoth/standalone/authorization/service/OAuth2ConsentService.java new file mode 100644 index 0000000..1d8e7a2 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/service/OAuth2ConsentService.java @@ -0,0 +1,30 @@ +package com.arrokoth.standalone.authorization.service; + +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; + +import java.security.Principal; +import java.util.Set; + +public interface OAuth2ConsentService { + + OAuth2ConsentService.ConsentResponse getConsentDetails(Principal principal, + String clientId, + String scope, + String state, + String principalName); + + + /** + * 记录返回结果,包含待批准、已批准 scopes 和客户端信息 + * + * @param scopesToApprove 需要用户授权的 scopes + * @param previouslyApprovedScopes 已经授权过的 scopes + * @param registeredClient 客户端注册信息 + */ + record ConsentResponse( + Set scopesToApprove, + Set previouslyApprovedScopes, + RegisteredClient registeredClient + ) { + } +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/service/impl/AuthorizationServiceImpl.java b/src/main/java/com/arrokoth/standalone/authorization/service/impl/AuthorizationServiceImpl.java new file mode 100644 index 0000000..ce626f4 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/service/impl/AuthorizationServiceImpl.java @@ -0,0 +1,41 @@ +package com.arrokoth.standalone.authorization.service.impl; + +import com.arrokoth.standalone.authorization.domain.request.LoginRequest; +import com.arrokoth.standalone.authorization.domain.response.BasicUser; +import com.arrokoth.standalone.authorization.domain.response.Token; +import com.arrokoth.standalone.authorization.service.AuthorizationService; +import com.arrokoth.standalone.authorization.util.JwtUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Service +public class AuthorizationServiceImpl implements AuthorizationService { + + @Override + public Token login(LoginRequest loginRequest) { + String admin = JwtUtils.createAccessToken("admin"); + + Token token = new Token(); + token.setAccess_token(admin); + token.setExpires_in(JwtUtils.DEFAULT_EXPIRATION); + return token; + } + + @Override + public void logout(String jti) { + + } + + @Override + public BasicUser getBasicUser() { + BasicUser basicUser = new BasicUser(); + basicUser.setUsername("admin"); + basicUser.setAvatar("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"); + basicUser.setIntroduction("I am a super administrator..."); + basicUser.setRoles(List.of("admin")); + return basicUser; + } +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/service/impl/OAuth2ConsentServiceImpl.java b/src/main/java/com/arrokoth/standalone/authorization/service/impl/OAuth2ConsentServiceImpl.java new file mode 100644 index 0000000..ca436c5 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/service/impl/OAuth2ConsentServiceImpl.java @@ -0,0 +1,71 @@ +package com.arrokoth.standalone.authorization.service.impl; + +import com.arrokoth.standalone.authorization.service.OAuth2ConsentService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.security.Principal; +import java.util.*; + +/** + * OAuth2 授权确认服务实现类 + */ +@Service +@RequiredArgsConstructor +public class OAuth2ConsentServiceImpl implements OAuth2ConsentService { + + private final RegisteredClientRepository registeredClientRepository; + private final OAuth2AuthorizationConsentService authorizationConsentService; + + /** + * 获取用户授权的详细信息,包括需要审批和已审批的 scopes + * + * @param clientId 客户端 ID + * @param scope 请求的 scope 列表(空格分隔) + * @param principalName 用户名 + * @return 包含待批准和已批准 scopes 的 ConsentResult 对象 + */ + public ConsentResponse getConsentDetails(Principal principal, + String clientId, + String scope, + String state, + String principalName) { + Set scopesToApprove = new HashSet<>(); + Set previouslyApprovedScopes = new HashSet<>(); + + RegisteredClient registeredClient = Optional.ofNullable( + registeredClientRepository.findByClientId(clientId)) + .orElseThrow(() -> new IllegalArgumentException("客户端不存在")); + + OAuth2AuthorizationConsent currentAuthorizationConsent = + this.authorizationConsentService.findById(registeredClient.getId(), principalName); + + Set authorizedScopes = Optional.ofNullable(currentAuthorizationConsent) + .map(OAuth2AuthorizationConsent::getScopes) + .orElse(Set.of()); + + Arrays.stream(StringUtils.delimitedListToStringArray(scope, " ")) + .filter(requestedScope -> !OidcScopes.OPENID.equals(requestedScope)) + .forEach(requestedScope -> { + if (authorizedScopes.contains(requestedScope)) { + previouslyApprovedScopes.add(requestedScope); + } else { + scopesToApprove.add(requestedScope); + } + }); + + return new ConsentResponse( + Collections.unmodifiableSet(scopesToApprove), + Collections.unmodifiableSet(previouslyApprovedScopes), + registeredClient + ); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationConsentServiceStore.java b/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationConsentServiceStore.java new file mode 100644 index 0000000..f80ee84 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationConsentServiceStore.java @@ -0,0 +1,17 @@ +package com.arrokoth.standalone.authorization.store; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; + +@Configuration +public class OAuth2AuthorizationConsentServiceStore { + + + @Bean + public OAuth2AuthorizationConsentService authorizationConsentService() { + return new InMemoryOAuth2AuthorizationConsentService(); + } + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationServiceStore.java b/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationServiceStore.java new file mode 100644 index 0000000..d26241b --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/store/OAuth2AuthorizationServiceStore.java @@ -0,0 +1,18 @@ +package com.arrokoth.standalone.authorization.store; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; + +@Configuration +public class OAuth2AuthorizationServiceStore { + + + @Bean + public OAuth2AuthorizationService authorizationService() { + // 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService + return new InMemoryOAuth2AuthorizationService(); + } + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/store/RegisteredClientRepositoryStore.java b/src/main/java/com/arrokoth/standalone/authorization/store/RegisteredClientRepositoryStore.java new file mode 100644 index 0000000..8887d44 --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/store/RegisteredClientRepositoryStore.java @@ -0,0 +1,47 @@ +package com.arrokoth.standalone.authorization.store; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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.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.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +import java.time.Duration; +import java.util.UUID; + +@Configuration +public class RegisteredClientRepositoryStore { + + @Bean + public RegisteredClientRepository registeredClientRepository() { + + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) + + .clientId("messaging-client") + .clientSecret(bCryptPasswordEncoder.encode("secret")) + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .redirectUri("http://127.0.0.1:8091/login/oauth2/code/messaging-client-oidc") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope("message.read") + .scope("message.write") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) + .tokenSettings(TokenSettings.builder() + .accessTokenTimeToLive(Duration.ofHours(1)) + .refreshTokenTimeToLive(Duration.ofHours(10)) + .build()) + .build(); + + return new InMemoryRegisteredClientRepository(oidcClient); + } +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/store/UserDetailsServiceStore.java b/src/main/java/com/arrokoth/standalone/authorization/store/UserDetailsServiceStore.java new file mode 100644 index 0000000..953c73d --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/store/UserDetailsServiceStore.java @@ -0,0 +1,25 @@ +package com.arrokoth.standalone.authorization.store; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +@Configuration +public class UserDetailsServiceStore { + + + @Bean + public UserDetailsService users(PasswordEncoder passwordEncoder) { + UserDetails user = User.withUsername("admin") + .password(passwordEncoder.encode("password")) + .roles("admin", "normal") + .authorities("app", "web") + .build(); + return new InMemoryUserDetailsManager(user); + } + +} diff --git a/src/main/java/com/arrokoth/standalone/authorization/store/redis/RedisTokenService.java b/src/main/java/com/arrokoth/standalone/authorization/store/redis/RedisTokenService.java new file mode 100644 index 0000000..0cf919d --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/store/redis/RedisTokenService.java @@ -0,0 +1,72 @@ +package com.arrokoth.standalone.authorization.store.redis; + +import com.arrokoth.standalone.authorization.util.JwtUtils; +import com.nimbusds.jose.JOSEException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis 的 Token 黑名单服务 + */ +@Slf4j +@Service +public class RedisTokenService { + + private static final String BLACKLIST_PREFIX = "blacklist:token:"; + private final StringRedisTemplate redisTemplate; + + public RedisTokenService(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token 字符串 + */ + public void blacklistToken(String token) { + try { + String jti = JwtUtils.extractJti(token); // 需要你在 JwtUtils 中添加 extractJti 方法 + long ttl = calculateTtlFromToken(token); + String key = BLACKLIST_PREFIX + jti; + // 设置黑名单项,并指定过期时间为 Token 剩余有效时间 + redisTemplate.opsForValue().set(key, "true", ttl, TimeUnit.SECONDS); + log.debug("Token [{}] added to blacklist with TTL {} seconds", jti, ttl); + } catch (Exception e) { + log.warn("Failed to add token to blacklist", e); + throw new RuntimeException("Failed to add token to blacklist", e); + } + } + + /** + * 检查 Token 是否在黑名单中 + * + * @param token JWT Token 字符串 + * @return 是否在黑名单中 + */ + public boolean isBlacklisted(String token) { + try { + String jti = JwtUtils.extractJti(token); + String key = BLACKLIST_PREFIX + jti; + return redisTemplate.hasKey(key); + } catch (Exception e) { + log.warn("Failed to check token in blacklist", e); + return true; // 出错时保守策略:拒绝访问 + } + } + + /** + * 获取 Token 剩余有效期(秒) + */ + private long calculateTtlFromToken(String token) throws ParseException, JOSEException { + Date expiration = JwtUtils.extractExpiration(token); + long remainingSeconds = (expiration.getTime() - System.currentTimeMillis()) / 1000; + return Math.max(1, remainingSeconds); // 至少保留 1 秒 + } +} \ No newline at end of file diff --git a/src/main/java/com/arrokoth/standalone/authorization/util/JwtUtils.java b/src/main/java/com/arrokoth/standalone/authorization/util/JwtUtils.java new file mode 100644 index 0000000..45c022f --- /dev/null +++ b/src/main/java/com/arrokoth/standalone/authorization/util/JwtUtils.java @@ -0,0 +1,183 @@ +package com.arrokoth.standalone.authorization.util; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.MACVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.userdetails.UserDetails; + +import java.security.SecureRandom; +import java.text.ParseException; +import java.util.Base64; +import java.util.Date; +import java.util.UUID; + +public class JwtUtils { + + // ================== 配置常量 ================== + private static final String ISSUER = "your-issuer"; + public static final Long DEFAULT_EXPIRATION = 3600000L; // 默认有效期 1小时 (ms) + private static final String SECRET_KEY = generateSecureSecret(); + + // ================== 公共方法 ================== + + /** + * 创建一个新的访问 Token + * + * @param username 用户标识(如 username 或 user id) + * @return JWT Token 字符串 + */ + public static String createAccessToken(String username) { + try { + return createAccessToken(username, username, DEFAULT_EXPIRATION); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + /** + * 创建一个新的访问 Token,并指定过期时间 + * + * @param subject 用户标识 + * @param expiration 过期时间(毫秒) + * @return JWT Token 字符串 + */ + public static String createAccessToken(String subject, String username, Long expiration) throws JOSEException { + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .subject(subject) + .issuer(ISSUER) + .issueTime(new Date()) + .expirationTime(new Date(System.currentTimeMillis() + expiration)) + .jwtID(UUID.randomUUID().toString()) + .claim("username", username) // 自定义字段存 username + .build(); + + JWSHeader header = new JWSHeader(JWSAlgorithm.HS256); + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + + MACSigner signer = new MACSigner(SECRET_KEY.getBytes()); + signedJWT.sign(signer); + + return signedJWT.serialize(); + } + + /** + * 解析并验证 Token + * + * @param token 要解析的 JWT 字符串 + * @return JWT 的 Claims + */ + public static JWTClaimsSet parseToken(String token) throws ParseException, JOSEException { + SignedJWT signedJWT = SignedJWT.parse(token); + + MACVerifier verifier = new MACVerifier(SECRET_KEY.getBytes()); + if (!signedJWT.verify(verifier)) { + throw new JOSEException("Invalid signature"); + } + + return signedJWT.getJWTClaimsSet(); + } + + /** + * 提取用户名 + * + * @param token JWT Token 字符串 + * @return 用户名 + */ + public static String extractUsername(String token) throws ParseException, JOSEException { + return parseToken(token).getSubject(); + } + + /** + * 获取 Token 的过期时间 + * + * @param token JWT Token 字符串 + * @return 过期时间 + */ + public static Date extractExpiration(String token) throws ParseException, JOSEException { + return parseToken(token).getExpirationTime(); + } + + /** + * 获取 Token 的 jti (JWT ID) + * + * @param token JWT Token 字符串 + * @return jti 值 + */ + public static String extractJti(String token) throws ParseException, JOSEException { + return parseToken(token).getJWTID(); + } + + + /** + * 判断 Token 是否已过期 + * + * @param token JWT Token 字符串 + * @return 是否有效 + */ + public static boolean isTokenExpired(String token) { + try { + return extractExpiration(token).before(new Date()); + } catch (Exception e) { + return true; // 出错也视为过期 + } + } + + /** + * 验证 Token 是否属于当前用户且未过期 + * + * @param token JWT Token 字符串 + * @param userDetails 用户信息 + * @return 是否有效 + */ + public static boolean validateToken(String token, UserDetails userDetails) { + try { + String username = extractUsername(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } catch (Exception e) { + return false; + } + } + + // ================== 私有工具方法 ================== + + /** + * 生成安全的密钥(Base64 编码的 256 位密钥) + */ + private static String generateSecureSecret() { + byte[] key = new byte[32]; // 256 bits + new SecureRandom().nextBytes(key); + return Base64.getEncoder().encodeToString(key); + } + + + // 提取 Token 的辅助方法 + public static String extractTokenFromHeader(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + return header.substring(7); + } + return null; + } + + // ================== 测试入口 ================== + public static void main(String[] args) { + try { + String token = createAccessToken("user@example.com"); + System.out.println("Generated Token: " + token); + + JWTClaimsSet claims = parseToken(token); + System.out.println("Subject: " + claims.getSubject()); + System.out.println("Issuer: " + claims.getIssuer()); + System.out.println("Expiration Time: " + claims.getExpirationTime()); + + System.out.println("Is Valid? " + validateToken(token, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f5d26c3 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,36 @@ +server: + port: 8080 + + + + +spring: + application: + name: authorization-server-standalone + data: + redis: + host: localhost + port: 6379 + password: yyds@8848 + lettuce: + pool: + max-active: 8 # 最大连接数 + max-idle: 4 # 最大空闲连接 + min-idle: 1 # 最小空闲连接 + max-wait: 2000ms # 获取连接最大等待时间 + + +arrokoth: + authorization: + server: + consent-page: /oauth2/consent + authorization-endpoint: /oauth2/authorize + issuer: https://www.arrokoth-info.com + security: + web: + login-page: /login + logout-success-url: /login?logout + permit-urls: + - /home/login + + diff --git a/src/main/resources/templates/consent.html b/src/main/resources/templates/consent.html new file mode 100644 index 0000000..b6d3b34 --- /dev/null +++ b/src/main/resources/templates/consent.html @@ -0,0 +1,87 @@ + + + + + + 授权 |统一身份认证服务平台 + + + + + + + + +
+ OAuth 授权请求 + +
+ + +
+
+ +
+ +

此第三方应用请求获得以下权限:

+
+
+
+ + + + +
    +
  • + +
  • +
+ +

你已授予以下权限:

+
    +
  • + +
  • +
+ + +
+ + +
+
+ +

+ 你可以随时在 帐户设置 > 第三方应用 中取消你的授权。 +

+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..128beee --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,410 @@ + + + + + + 登录 | 统一身份认证服务平台 + + + + + + + + + + + + + + +
+

© 北京阿罗科斯信息技术有限责任公司 版权所有 | 联系我们 | 帮助中心 |

+
+ + + + + \ No newline at end of file diff --git a/src/test/java/com/arrokoth/standalone/authorization/StandaloneServerApplicationTests.java b/src/test/java/com/arrokoth/standalone/authorization/StandaloneServerApplicationTests.java new file mode 100644 index 0000000..ff3ba32 --- /dev/null +++ b/src/test/java/com/arrokoth/standalone/authorization/StandaloneServerApplicationTests.java @@ -0,0 +1,13 @@ +package com.arrokoth.standalone.authorization; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StandaloneServerApplicationTests { + + @Test + void contextLoads() { + } + +}