From 0f38d49b664812bda14cb423ce7ed16b32b44ce0 Mon Sep 17 00:00:00 2001 From: lingxiao865 <1060369102@qq.com> Date: Tue, 10 Feb 2026 08:22:09 +0800 Subject: [PATCH] 1 --- .gitattributes | 2 + .gitignore | 33 ++ .mvn/wrapper/maven-wrapper.properties | 3 + a-service/.gitattributes | 2 + a-service/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.properties | 3 + a-service/Dockerfile | 37 ++ a-service/mvnw | 295 ++++++++++ a-service/mvnw.cmd | 189 ++++++ a-service/pom.xml | 63 ++ .../example/demo001/Demo001Application.java | 17 + .../config/OAuth2ClientProperties.java | 14 + .../demo001/entity/CodeExchangeRequest.java | 12 + .../demo001/service/OAuth2TokenClient.java | 34 ++ .../com/example/demo001/web/AController.java | 29 + .../example/demo001/web/TestController.java | 158 +++++ a-service/src/main/resources/application.yml | 11 + auth/.gitattributes | 2 + auth/.gitignore | 33 ++ auth/.mvn/wrapper/maven-wrapper.properties | 3 + auth/Dockerfile | 17 + auth/mvnw | 295 ++++++++++ auth/mvnw.cmd | 189 ++++++ auth/pom.xml | 125 ++++ .../springboot4/Springboot4Application.java | 17 + .../springboot4/config/FaviconConfig.java | 15 + .../springboot4/config/RedisConfig.java | 37 ++ .../springboot4/config/SecurityConfig.java | 304 ++++++++++ .../com/example/springboot4/config/Users.java | 55 ++ .../controller/CaptchaController.java | 30 + .../controller/LoginController.java | 71 +++ .../springboot4/controller/SmsController.java | 91 +++ .../filter/CaptchaAuthenticationFilter.java | 65 +++ .../CustomAuthenticationEntryPoint.java | 61 ++ .../springboot4/handler/ErrorResponse.java | 36 ++ ...edisOAuth2AuthorizationConsentService.java | 59 ++ .../RedisOAuth2AuthorizationService.java | 143 +++++ .../service/SmsAuthenticationProvider.java | 47 ++ .../service/SmsAuthenticationToken.java | 43 ++ .../springboot4/service/SmsCodeService.java | 67 +++ .../service/SmsUserDetailsService.java | 88 +++ .../example/springboot4/util/CaptchaUtil.java | 112 ++++ .../springboot4/util/RsaKeyLoader.java | 48 ++ auth/src/main/resources/application.yml | 41 ++ auth/src/main/resources/static/favicon.ico | Bin 0 -> 269374 bytes .../src/main/resources/templates/consent.html | 312 ++++++++++ auth/src/main/resources/templates/home.html | 80 +++ auth/src/main/resources/templates/login.html | 547 ++++++++++++++++++ .../Springboot4ApplicationTests.java | 13 + geteway/.gitattributes | 2 + geteway/.gitignore | 33 ++ geteway/.mvn/wrapper/maven-wrapper.properties | 3 + geteway/Dockerfile | 9 + geteway/mvnw | 295 ++++++++++ geteway/mvnw.cmd | 189 ++++++ geteway/pom.xml | 51 ++ .../example/geteway/GatewayApplication.java | 13 + .../config/CookieBearerTokenResolver.java | 37 ++ .../geteway/config/SecurityConfig.java | 73 +++ .../geteway/config/UserIdMappingFilter.java | 41 ++ geteway/src/main/resources/application.yml | 50 ++ .../geteway/GatewayApplicationTests.java | 13 + k8s/a-service-deployment.yaml | 38 ++ k8s/auth-deployment.yaml | 44 ++ k8s/gateway-deployment.yaml | 35 ++ k8s/secrets.yaml | 11 + keys/oauth2-private.key | Bin 0 -> 1478 bytes keys/oauth2-public.key | Bin 0 -> 551 bytes mvnw | 295 ++++++++++ mvnw.cmd | 189 ++++++ pom.xml | 92 +++ 71 files changed, 5494 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 a-service/.gitattributes create mode 100644 a-service/.gitignore create mode 100644 a-service/.mvn/wrapper/maven-wrapper.properties create mode 100644 a-service/Dockerfile create mode 100644 a-service/mvnw create mode 100644 a-service/mvnw.cmd create mode 100644 a-service/pom.xml create mode 100644 a-service/src/main/java/com/example/demo001/Demo001Application.java create mode 100644 a-service/src/main/java/com/example/demo001/config/OAuth2ClientProperties.java create mode 100644 a-service/src/main/java/com/example/demo001/entity/CodeExchangeRequest.java create mode 100644 a-service/src/main/java/com/example/demo001/service/OAuth2TokenClient.java create mode 100644 a-service/src/main/java/com/example/demo001/web/AController.java create mode 100644 a-service/src/main/java/com/example/demo001/web/TestController.java create mode 100644 a-service/src/main/resources/application.yml create mode 100644 auth/.gitattributes create mode 100644 auth/.gitignore create mode 100644 auth/.mvn/wrapper/maven-wrapper.properties create mode 100644 auth/Dockerfile create mode 100644 auth/mvnw create mode 100644 auth/mvnw.cmd create mode 100644 auth/pom.xml create mode 100644 auth/src/main/java/com/example/springboot4/Springboot4Application.java create mode 100644 auth/src/main/java/com/example/springboot4/config/FaviconConfig.java create mode 100644 auth/src/main/java/com/example/springboot4/config/RedisConfig.java create mode 100644 auth/src/main/java/com/example/springboot4/config/SecurityConfig.java create mode 100644 auth/src/main/java/com/example/springboot4/config/Users.java create mode 100644 auth/src/main/java/com/example/springboot4/controller/CaptchaController.java create mode 100644 auth/src/main/java/com/example/springboot4/controller/LoginController.java create mode 100644 auth/src/main/java/com/example/springboot4/controller/SmsController.java create mode 100644 auth/src/main/java/com/example/springboot4/filter/CaptchaAuthenticationFilter.java create mode 100644 auth/src/main/java/com/example/springboot4/filter/CustomAuthenticationEntryPoint.java create mode 100644 auth/src/main/java/com/example/springboot4/handler/ErrorResponse.java create mode 100644 auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationConsentService.java create mode 100644 auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationService.java create mode 100644 auth/src/main/java/com/example/springboot4/service/SmsAuthenticationProvider.java create mode 100644 auth/src/main/java/com/example/springboot4/service/SmsAuthenticationToken.java create mode 100644 auth/src/main/java/com/example/springboot4/service/SmsCodeService.java create mode 100644 auth/src/main/java/com/example/springboot4/service/SmsUserDetailsService.java create mode 100644 auth/src/main/java/com/example/springboot4/util/CaptchaUtil.java create mode 100644 auth/src/main/java/com/example/springboot4/util/RsaKeyLoader.java create mode 100644 auth/src/main/resources/application.yml create mode 100644 auth/src/main/resources/static/favicon.ico create mode 100644 auth/src/main/resources/templates/consent.html create mode 100644 auth/src/main/resources/templates/home.html create mode 100644 auth/src/main/resources/templates/login.html create mode 100644 auth/src/test/java/com/example/springboot4/Springboot4ApplicationTests.java create mode 100644 geteway/.gitattributes create mode 100644 geteway/.gitignore create mode 100644 geteway/.mvn/wrapper/maven-wrapper.properties create mode 100644 geteway/Dockerfile create mode 100644 geteway/mvnw create mode 100644 geteway/mvnw.cmd create mode 100644 geteway/pom.xml create mode 100644 geteway/src/main/java/com/example/geteway/GatewayApplication.java create mode 100644 geteway/src/main/java/com/example/geteway/config/CookieBearerTokenResolver.java create mode 100644 geteway/src/main/java/com/example/geteway/config/SecurityConfig.java create mode 100644 geteway/src/main/java/com/example/geteway/config/UserIdMappingFilter.java create mode 100644 geteway/src/main/resources/application.yml create mode 100644 geteway/src/test/java/com/example/geteway/GatewayApplicationTests.java create mode 100644 k8s/a-service-deployment.yaml create mode 100644 k8s/auth-deployment.yaml create mode 100644 k8s/gateway-deployment.yaml create mode 100644 k8s/secrets.yaml create mode 100644 keys/oauth2-private.key create mode 100644 keys/oauth2-public.key create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml 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/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/a-service/.gitattributes b/a-service/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/a-service/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/a-service/.gitignore b/a-service/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/a-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/a-service/.mvn/wrapper/maven-wrapper.properties b/a-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/a-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/a-service/Dockerfile b/a-service/Dockerfile new file mode 100644 index 0000000..4e17874 --- /dev/null +++ b/a-service/Dockerfile @@ -0,0 +1,37 @@ +FROM openjdk:25-jdk-slim + +WORKDIR /app + +COPY target/*.jar app.jar + +EXPOSE 8091 + +ENTRYPOINT ["java","-jar", "app.jar"] + +## Perform the extraction in a separate builder container +#FROM bellsoft/liberica-openjre-debian:25-cds AS builder +#WORKDIR /builder +## This points to the built jar file in the target folder +## Adjust this to 'build/libs/*.jar' if you're using Gradle +#ARG JAR_FILE=target/*.jar +## Copy the jar file to the working directory and rename it to application.jar +#COPY ${JAR_FILE} application.jar +## Extract the jar file using an efficient layout +#RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted +# +## This is the runtime container +#FROM bellsoft/liberica-openjre-debian:25-cds +#WORKDIR /application +## Copy the extracted jar contents from the builder container into the working directory in the runtime container +## Every copy step creates a new docker layer +## This allows docker to only pull the changes it really needs +#COPY --from=builder /builder/extracted/dependencies/ ./ +#COPY --from=builder /builder/extracted/spring-boot-loader/ ./ +#COPY --from=builder /builder/extracted/snapshot-dependencies/ ./ +#COPY --from=builder /builder/extracted/application/ ./ +## Execute the AOT cache training run +#RUN java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar +## Start the application jar with AOT cache enabled - this is not the uber jar used by the builder +## This jar only contains application code and references to the extracted jar files +## This layout is efficient to start up and AOT cache friendly +#ENTRYPOINT ["java", "-XX:AOTCache=app.aot", "-jar", "application.jar"] \ No newline at end of file diff --git a/a-service/mvnw b/a-service/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/a-service/mvnw @@ -0,0 +1,295 @@ +#!/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.4 +# +# 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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/a-service/mvnw.cmd b/a-service/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/a-service/mvnw.cmd @@ -0,0 +1,189 @@ +<# : 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.4 +@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/a-service/pom.xml b/a-service/pom.xml new file mode 100644 index 0000000..ae7cfff --- /dev/null +++ b/a-service/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + com + lingxiao + 0.0.1-SNAPSHOT + ../pom.xml + + + a-service + a-service + a-service + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.projectlombok + lombok + provided + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + + + + org.projectlombok + lombok + + + + + + + + diff --git a/a-service/src/main/java/com/example/demo001/Demo001Application.java b/a-service/src/main/java/com/example/demo001/Demo001Application.java new file mode 100644 index 0000000..7d7ff29 --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/Demo001Application.java @@ -0,0 +1,17 @@ +package com.example.demo001; + +import com.example.demo001.service.OAuth2TokenClient; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.service.registry.HttpServiceGroup; +import org.springframework.web.service.registry.ImportHttpServices; + +@SpringBootApplication +@ImportHttpServices(group = "echo", types = OAuth2TokenClient.class, clientType = HttpServiceGroup.ClientType.WEB_CLIENT) +public class Demo001Application { + + public static void main(String[] args) { + SpringApplication.run(Demo001Application.class, args); + } + +} diff --git a/a-service/src/main/java/com/example/demo001/config/OAuth2ClientProperties.java b/a-service/src/main/java/com/example/demo001/config/OAuth2ClientProperties.java new file mode 100644 index 0000000..280aca4 --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/config/OAuth2ClientProperties.java @@ -0,0 +1,14 @@ +package com.example.demo001.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "oauth2.client") +@Data +public class OAuth2ClientProperties { + private String clientId; + private String clientSecret; + +} \ No newline at end of file diff --git a/a-service/src/main/java/com/example/demo001/entity/CodeExchangeRequest.java b/a-service/src/main/java/com/example/demo001/entity/CodeExchangeRequest.java new file mode 100644 index 0000000..1eb98ed --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/entity/CodeExchangeRequest.java @@ -0,0 +1,12 @@ +package com.example.demo001.entity; + +import lombok.Data; + +@Data +public class CodeExchangeRequest { + + private String code; + private String redirect_uri; + private String code_verifier; + +} diff --git a/a-service/src/main/java/com/example/demo001/service/OAuth2TokenClient.java b/a-service/src/main/java/com/example/demo001/service/OAuth2TokenClient.java new file mode 100644 index 0000000..f3b86b3 --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/service/OAuth2TokenClient.java @@ -0,0 +1,34 @@ +package com.example.demo001.service; + +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@HttpExchange("http://auth-service:9000") +//@HttpExchange("http://localhost:9000") +public interface OAuth2TokenClient { + + @PostExchange("/oauth2/token") + Mono> refreshToken( + @RequestHeader("Authorization") String authHeader, + @RequestBody MultiValueMap formData + ); + + @GetExchange("/userinfo") + Mono> userinfo(@RequestHeader("Authorization") String authHeader); + + @PostExchange("/oauth2/revoke") + Mono logout( + @RequestHeader("Authorization") String authHeader, + @RequestBody MultiValueMap formData + ); + +} \ No newline at end of file diff --git a/a-service/src/main/java/com/example/demo001/web/AController.java b/a-service/src/main/java/com/example/demo001/web/AController.java new file mode 100644 index 0000000..821e388 --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/web/AController.java @@ -0,0 +1,29 @@ +package com.example.demo001.web; + + +import com.example.demo001.service.OAuth2TokenClient; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +public class AController { + + private final OAuth2TokenClient tokenClient; + + @GetMapping("/userinfo") + public Mono>> userinfo(@CookieValue("access_token") String accessToken) { + return tokenClient.userinfo("Bearer " + accessToken) + .map(ResponseEntity::ok) + .onErrorReturn(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } + + +} diff --git a/a-service/src/main/java/com/example/demo001/web/TestController.java b/a-service/src/main/java/com/example/demo001/web/TestController.java new file mode 100644 index 0000000..69d9c5b --- /dev/null +++ b/a-service/src/main/java/com/example/demo001/web/TestController.java @@ -0,0 +1,158 @@ +package com.example.demo001.web; + +import com.example.demo001.config.OAuth2ClientProperties; +import com.example.demo001.entity.CodeExchangeRequest; +import com.example.demo001.service.OAuth2TokenClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.Objects; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class TestController { + + + private final OAuth2TokenClient tokenClient; + private final OAuth2ClientProperties oauth2Props; // 包含 clientId/clientSecret + + @PostMapping("/logout") + public Mono> logout(ServerHttpRequest request, ServerHttpResponse response) { + // 1. 安全获取 Cookie,防止 NullPointerException + HttpCookie refreshCookie = request.getCookies().getFirst("refresh_token"); + + // 定义统一的清理动作 + Runnable clearAction = () -> { + response.addCookie(ResponseCookie.from("access_token", "").path("/").maxAge(0).build()); + // 关键:这里的路径必须与上面获取令牌时设定的 /api/auth 完全一致 + response.addCookie(ResponseCookie.from("refresh_token", "").path("/api/auth").maxAge(0).build()); + }; + if (refreshCookie == null) { + clearAction.run(); + return Mono.just(ResponseEntity.ok().build()); + } + + // 2. 构造认证头和表单数据 + String credentials = oauth2Props.getClientId() + ":" + oauth2Props.getClientSecret(); + String authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + // 撤销授权服务器端的令牌状态 + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("token", refreshCookie.getValue()); + formData.add("token_type_hint", "refresh_token"); + + // 3. 执行撤销并正确处理 Mono 链 + return tokenClient.logout(authHeader, formData) + .then(Mono.fromRunnable(() -> clearCookies(response))) // 执行清理 + .then(Mono.just(ResponseEntity.ok().build())); // 明确指定 Void 泛型 + } + + /** + * 抽取统一的 Cookie 清理逻辑 + */ + private void clearCookies(ServerHttpResponse response) { + // 注意:path 必须与登录时设置的完全一致,且不能带通配符 * + ResponseCookie clearAccess = ResponseCookie.from("access_token", "").httpOnly(true).secure(true).maxAge(0).path("/").build(); + + // 建议将 path 从 "/api/auth/*" 改为授权中心的基础路径,如 "/api/auth" + ResponseCookie clearRefresh = ResponseCookie.from("refresh_token", "").httpOnly(true).secure(true).maxAge(0).path("/api/auth").build(); + + response.addCookie(clearAccess); + response.addCookie(clearRefresh); + } + + // 前端调用此接口,传入 code 和 redirect_uri + @PostMapping("/token") + public Mono> exchangeCodeForToken(CodeExchangeRequest request, ServerHttpResponse response) { + + // 构造 Basic Auth + String credentials = oauth2Props.getClientId() + ":" + oauth2Props.getClientSecret(); + String authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + // 构造表单 + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "authorization_code"); + formData.add("code", request.getCode()); + formData.add("redirect_uri", request.getRedirect_uri()); + if (request.getRedirect_uri() != null) { + formData.add("code_verifier", request.getCode_verifier()); + } + + return tokenClient.refreshToken(authHeader, formData).doOnNext(tokens -> { + String accessToken = (String) tokens.get("access_token"); + String refreshToken = (String) tokens.get("refresh_token"); + Integer expiresIn = (Integer) tokens.get("expires_in"); + + // 创建 access_token Cookie + ResponseCookie accessTokenCookie = ResponseCookie.from("access_token", accessToken).httpOnly(true).secure(true).sameSite("Strict").path("/").maxAge(expiresIn != null ? expiresIn : 900).build(); + + // 创建 refresh_token Cookie + ResponseCookie refreshTokenCookie = ResponseCookie.from("refresh_token", refreshToken).httpOnly(true).secure(true).sameSite("Strict").path("/api/auth") +// .path("/api/auth/logout") + .maxAge(60 * 60 * 24 * 7) // 7 天 + .build(); + + // 添加到响应 + response.addCookie(accessTokenCookie); + response.addCookie(refreshTokenCookie); + }).then(Mono.just(ResponseEntity.ok().build())).onErrorResume(throwable -> { + log.error(throwable.getMessage()); + // 记录错误日志 + return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); +// return Mono.just(ResponseEntity.status(401).build()); + }); + } + + @PostMapping("/refresh") + public Mono> refreshToken(ServerHttpRequest request, ServerHttpResponse response) { + try { + String refreshToken = Objects.requireNonNull(request.getCookies().getFirst("refresh_token")).getValue(); + + String credentials = oauth2Props.getClientId() + ":" + oauth2Props.getClientSecret(); + String authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "refresh_token"); + formData.add("refresh_token", refreshToken); + return tokenClient.refreshToken(authHeader, formData).doOnNext(newTokens -> { + String newAccessToken = (String) newTokens.get("access_token"); + Integer expiresIn = (Integer) newTokens.get("expires_in"); + + ResponseCookie newAccessTokenCookie = ResponseCookie.from("access_token", newAccessToken).httpOnly(true).secure(true).sameSite("Strict").path("/").maxAge(expiresIn != null ? expiresIn : 900).build(); + + response.addCookie(newAccessTokenCookie); + + // 如果返回了新的 refresh_token,也更新它 + if (newTokens.containsKey("refresh_token")) { + String newRefreshToken = (String) newTokens.get("refresh_token"); + ResponseCookie newRefreshTokenCookie = ResponseCookie.from("refresh_token", newRefreshToken).httpOnly(true).secure(true).sameSite("Strict").path("/api/auth") +// .path("/api/auth/logout") + .maxAge(60 * 60 * 24 * 7).build(); + response.addCookie(newRefreshTokenCookie); + } + }).then(Mono.just(ResponseEntity.ok().build())).onErrorResume(throwable -> { + clearCookies(response); + return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + }); + } catch (Exception e) { + return Mono.just(ResponseEntity.status(401).build()); + } + + } + + +} diff --git a/a-service/src/main/resources/application.yml b/a-service/src/main/resources/application.yml new file mode 100644 index 0000000..ad4eed5 --- /dev/null +++ b/a-service/src/main/resources/application.yml @@ -0,0 +1,11 @@ +server: + port: 8091 +spring: + application: + name: test-service + +oauth2: + client: + client-id: oidc-client + client-secret: secret + diff --git a/auth/.gitattributes b/auth/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/auth/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/auth/.mvn/wrapper/maven-wrapper.properties b/auth/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/auth/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/auth/Dockerfile b/auth/Dockerfile new file mode 100644 index 0000000..85b586e --- /dev/null +++ b/auth/Dockerfile @@ -0,0 +1,17 @@ +FROM openjdk:25-jdk-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libfreetype6 \ + fontconfig \ + fonts-dejavu-core \ + fonts-wqy-microhei && \ + rm -rf /var/lib/apt/lists/* \ + +WORKDIR /app + +COPY target/*.jar app.jar + +EXPOSE 9000 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/auth/mvnw b/auth/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/auth/mvnw @@ -0,0 +1,295 @@ +#!/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.4 +# +# 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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/auth/mvnw.cmd b/auth/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/auth/mvnw.cmd @@ -0,0 +1,189 @@ +<# : 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.4 +@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/auth/pom.xml b/auth/pom.xml new file mode 100644 index 0000000..094f9f7 --- /dev/null +++ b/auth/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + com + lingxiao + 0.0.1-SNAPSHOT + ../pom.xml + + auth + auth + auth + + + + + + + + + + + + + + + 25 + 25 + 25 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-jetty + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-session-data-redis + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 25 + 25 + + + org.projectlombok + lombok + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/Springboot4Application.java b/auth/src/main/java/com/example/springboot4/Springboot4Application.java new file mode 100644 index 0000000..bb96d15 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/Springboot4Application.java @@ -0,0 +1,17 @@ +package com.example.springboot4; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@EnableDiscoveryClient // 启用 Kubernetes 服务发现 +public class Springboot4Application { + + public static void main(String[] args) { + SpringApplication.run(Springboot4Application.class, args); + } + +} diff --git a/auth/src/main/java/com/example/springboot4/config/FaviconConfig.java b/auth/src/main/java/com/example/springboot4/config/FaviconConfig.java new file mode 100644 index 0000000..59f29c6 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/config/FaviconConfig.java @@ -0,0 +1,15 @@ +package com.example.springboot4.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class FaviconConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/favicon.ico") + .addResourceLocations("classpath:/static/"); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/config/RedisConfig.java b/auth/src/main/java/com/example/springboot4/config/RedisConfig.java new file mode 100644 index 0000000..1fba8f8 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/config/RedisConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2024 the original author or authors. + * + * Licensed 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 + * + * https://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. + */ +package com.example.springboot4.config; + +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.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration(proxyBeanMethods = false) +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + // 设置key的序列化方式为String + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + // 设置value的序列化方式为JSON + return redisTemplate; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/config/SecurityConfig.java b/auth/src/main/java/com/example/springboot4/config/SecurityConfig.java new file mode 100644 index 0000000..06220b0 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/config/SecurityConfig.java @@ -0,0 +1,304 @@ +package com.example.springboot4.config; + +import com.example.springboot4.filter.CaptchaAuthenticationFilter; +import com.example.springboot4.service.RedisOAuth2AuthorizationConsentService; +import com.example.springboot4.service.RedisOAuth2AuthorizationService; +import com.example.springboot4.service.SmsAuthenticationProvider; +import com.example.springboot4.service.SmsUserDetailsService; +import com.example.springboot4.util.RsaKeyLoader; +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 jakarta.annotation.Resource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +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.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +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.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext; +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.TokenSettings; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +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.List; +import java.util.function.Function; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Resource + private SmsAuthenticationProvider smsAuthenticationProvider; + + @Resource + private SmsUserDetailsService smsUserDetailsService; + + private static KeyPair generateRsaKey() { + // 尝试从本地文件加载密钥对 + try { + java.security.PrivateKey privateKey = RsaKeyLoader.loadPrivateKey("keys/oauth2-private.key"); + java.security.PublicKey publicKey = RsaKeyLoader.loadPublicKey("keys/oauth2-public.key"); + System.out.println("成功从文件加载RSA密钥对"); + return new KeyPair(publicKey, privateKey); + } catch (Exception ex) { + // 如果加载失败,则生成新的密钥对 + System.err.println("警告:无法从文件加载密钥对,将生成临时密钥对: " + ex.getMessage()); + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + System.out.println("已生成新的临时RSA密钥对"); + } catch (Exception ex2) { + throw new IllegalStateException(ex2); + } + return keyPair; + } + } + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) { + OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); + authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint.consentPage("/oauth2/consent")); // 自定义授权页面 + http + .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) + .with(authorizationServerConfigurer, (authorizationServer) -> + authorizationServer + .oidc(oidc -> oidc + .userInfoEndpoint(userInfo -> userInfo + .userInfoMapper(userInfoMapper()))) + ) + .authorizeHttpRequests((authorize) -> + authorize.anyRequest().authenticated() + ) + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ) + // 配置CORS + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.ignoringRequestMatchers(authorizationServerConfigurer.getEndpointsMatcher())); + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) { + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/login", "/favicon.ico", "/css/**", "/js/**", "/images/**", "/captcha/**", "/sms/**", "/login/mobile").permitAll() + .anyRequest().authenticated() + ) + .authenticationProvider(smsAuthenticationProvider) + .userDetailsService(smsUserDetailsService) + .addFilterBefore(new CaptchaAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 添加验证码过滤器,不用可以注释 + .formLogin(form -> form + .loginPage("/login") + .permitAll() + ) + .sessionManagement(session -> session + .sessionFixation().migrateSession() + .maximumSessions(1) + .maxSessionsPreventsLogin(false) + ) + .logout(LogoutConfigurer::permitAll) + .cors(cors -> cors.configurationSource(corsConfigurationSource()))// 配置CORS + .csrf(csrf -> csrf.ignoringRequestMatchers("/login/mobile", "/sms/**")); + + return http.build(); + } + +// @Bean +// public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { +// RegisteredClient oidcClient = RegisteredClient.withId("7e2d5d5e-0077-4853-97a8-4e49be099956") +// .clientId("oidc-client") +// .clientSecret("{noop}secret") +// .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) +// .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) +// .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) +// .redirectUri("http://localhost:3000/callback") +// .redirectUri("https://c.makesong.cn/callback") +// .redirectUri("https://baidu.com") +// .postLogoutRedirectUri("https://c.makesong.cn/callback") +// .scope(OidcScopes.OPENID) +// .scope(OidcScopes.PROFILE) +// .scope(OidcScopes.EMAIL) +// .scope(OidcScopes.ADDRESS) +// .scope(OidcScopes.PHONE) +// .tokenSettings(TokenSettings.builder() +// .accessTokenTimeToLive(Duration.ofSeconds(300)) +// .refreshTokenTimeToLive(Duration.ofDays(30)) +// .build()) +// .clientSettings(ClientSettings.builder() +// .requireAuthorizationConsent(true) // 启用授权同意页面 +// .requireProofKey(true) // 禁用PKCE要求,如需要可以开启 +// .build()) +// .build(); +// JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); +// +// registeredClientRepository.save(oidcClient); //添加一条client,也可以在数据库中手动添加 +// return registeredClientRepository; +// } + + @Bean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + // CORS配置源 + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedMethods(List.of("*")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + //使用JDBC存储Client信息,支持动态存储 +// @Bean +// public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { +// return new JdbcRegisteredClientRepository(jdbcTemplate); +// } + + // 不再需要 @Bean 注解,因为 smsUserDetailsService 已经是 UserDetailsService bean + public UserDetailsService userDetailsService() { + return smsUserDetailsService; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public OAuth2AuthorizationConsentService authorizationConsentService( + RedisTemplate redisTemplate, RegisteredClientRepository registeredClientRepository) { + return new RedisOAuth2AuthorizationConsentService(redisTemplate, registeredClientRepository); + } + + + // 使用Redis存储授权后的信息 + @Bean + public OAuth2AuthorizationService authorizationService( + RedisTemplate redisTemplate, + RegisteredClientRepository registeredClientRepository) { + return new RedisOAuth2AuthorizationService(redisTemplate, registeredClientRepository); + } + + //使用默认地址映射 + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + + @Bean + // 自定义ID_TOKEN信息 + public OAuth2TokenCustomizer tokenCustomizer( + UserDetailsService userInfoService) { + return (context) -> { + if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { + UserDetails userDetails = userInfoService.loadUserByUsername(context.getPrincipal().getName()); + context.getClaims().claims(claims -> { +// claims.clear(); + if (userDetails instanceof Users customUser) { + claims.put("nickname", customUser.getNickname()); + claims.put("avatar", customUser.getAvatar()); + claims.put("phone", customUser.getPhone()); + claims.put("email", customUser.getEmail()); + } + }); + } + }; + } + + // 自定义用户信息映射 + @Bean + public Function userInfoMapper() { + return (context -> { + // 获取认证信息 + Authentication authentication = context.getAuthentication(); + try { + String username = authentication.getName(); + + UserDetails userDetails = userDetailsService().loadUserByUsername(username); + + // 如果是自定义的Users类型,返回自定义的用户信息,否则返回默认的用户信息 + if (userDetails instanceof Users customUser) { + return new OidcUserInfo(customUser.getOidcUserInfo()); + } + } catch (Exception _) { + + } + JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal(); + return new OidcUserInfo(principal.getToken().getClaims()); + + }); + } + + @Bean + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID("oauth2-jwk-key") + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/config/Users.java b/auth/src/main/java/com/example/springboot4/config/Users.java new file mode 100644 index 0000000..7649f18 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/config/Users.java @@ -0,0 +1,55 @@ +package com.example.springboot4.config; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@Data +// 自定义用户信息 +public class Users extends User { + public String nickname; + private String email; + private String phone; + private String avatar; + + public Users(String username, @Nullable String password, Collection authorities, String nickname, String avatar, String phone) { + super(username, password, authorities); + this.nickname = nickname; + this.avatar = avatar; + this.phone = phone; + } + + public Map getOidcUserInfo() { + return OidcUserInfo.builder() + .subject(getUsername()) + .name("First Last") + .givenName("First") + .familyName("Last") + .middleName("Middle") + .nickname(nickname) + .preferredUsername(getUsername()) + .profile("https://example.com/" + nickname) + .picture(avatar) + .website("https://example.com") + .email(nickname + "@example.com") + .emailVerified(true) + .gender("female") + .birthdate("1970-01-01") + .zoneinfo("Europe/Paris") + .locale("en-US") + .phoneNumber(phone) + .phoneNumberVerified(false) + .claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance")) + .updatedAt("1970-01-01T00:00:00Z") + .build() + .getClaims(); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/controller/CaptchaController.java b/auth/src/main/java/com/example/springboot4/controller/CaptchaController.java new file mode 100644 index 0000000..efdc519 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/controller/CaptchaController.java @@ -0,0 +1,30 @@ +package com.example.springboot4.controller; + +import com.example.springboot4.util.CaptchaUtil; +import jakarta.servlet.http.HttpSession; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/captcha") +public class CaptchaController { + /** + * 生成验证码 + */ + @GetMapping("/generate") + public Map generateCaptcha(HttpSession session) { + + CaptchaUtil.Captcha captcha = CaptchaUtil.generateCaptcha(); + // 将验证码文本存储在session中,也可以自定义Redis存储验证码 + session.setAttribute("captcha", captcha.getText()); + + // 返回验证码图片Base64编码 + Map response = new HashMap<>(); + response.put("image", "data:image/png;base64," + captcha.getImageBase64()); + return response; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/controller/LoginController.java b/auth/src/main/java/com/example/springboot4/controller/LoginController.java new file mode 100644 index 0000000..86b790f --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/controller/LoginController.java @@ -0,0 +1,71 @@ +package com.example.springboot4.controller; + +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.io.IOException; +import java.security.Principal; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Controller +public class LoginController { + + private static final Logger logger = LoggerFactory.getLogger(LoginController.class); + + @GetMapping("/") + public String home() { + return "home"; + } + + @GetMapping("/login") + public String login(@RequestParam(defaultValue = "false") boolean error, + @RequestParam(defaultValue = "false") boolean logout, + @RequestParam(defaultValue = "false") boolean captcha) { + return "login"; + } + + @GetMapping("/oauth2/consent") + public String consent(HttpServletRequest request, Model model) { + // 虽然不能直接拿到 code_challenge,但你可以相信框架已保存 + // 只需确保表单正确提交即可 + String clientId = request.getParameter("client_id"); + String scopeStr = request.getParameter("scope"); + String state = request.getParameter("state"); + + // 参数验证 + if (clientId == null || clientId.trim().isEmpty()) { + logger.warn("consent页面请求缺少client_id参数"); + return "error"; // 或重定向到错误页面 + } + + // 处理scope参数 + List scopes = Collections.emptyList(); + if (scopeStr != null && !scopeStr.trim().isEmpty()) { + scopes = Arrays.asList(scopeStr.trim().split("\\s+")); + } + model.addAttribute("clientId", request.getParameter("client_id")); + model.addAttribute("scopes", scopes); + model.addAttribute("state", request.getParameter("state")); + + // 注意:不要试图在这里构造 redirect_uri 或 code_challenge! + return "consent"; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/controller/SmsController.java b/auth/src/main/java/com/example/springboot4/controller/SmsController.java new file mode 100644 index 0000000..be0f0f4 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/controller/SmsController.java @@ -0,0 +1,91 @@ +package com.example.springboot4.controller; + +import com.example.springboot4.service.SmsAuthenticationToken; +import com.example.springboot4.service.SmsCodeService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +@Controller +public class SmsController { + + private final SmsCodeService smsCodeService; + private final AuthenticationManager authenticationManager; + + @Autowired + public SmsController(SmsCodeService smsCodeService, AuthenticationManager authenticationManager) { + this.smsCodeService = smsCodeService; + this.authenticationManager = authenticationManager; + } + + /** + * 发送短信验证码 + */ + @PostMapping("/sms/code") + @ResponseBody + public String sendCode(@RequestParam("phone") String phone, RedirectAttributes redirectAttributes) { + // 生成验证码 + String code = smsCodeService.generateCode(); + // 发送验证码(模拟) + smsCodeService.sendCode(phone, code); + // 添加提示信息 + redirectAttributes.addFlashAttribute("message", "验证码已发送至 " + phone); + return "成功"; + } + + /** + * 短信验证码登录 + */ + @PostMapping("/login/mobile") + public String loginWithSms(@RequestParam("phone") String phone, + @RequestParam("code") String code, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + // 创建未认证的Token + SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, code); + try { + // 调用AuthenticationManager进行认证 + Authentication authentication = authenticationManager.authenticate(authRequest); + // 设置认证信息到SecurityContext + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(authentication); + + // 保存SecurityContext到HttpSession,确保认证状态持久化 + HttpSession session = request.getSession(true); + session.setAttribute("SPRING_SECURITY_CONTEXT", context); + + // 获取保存的请求(如授权URL),如果存在则重定向到保存的请求 + HttpSessionRequestCache requestCache = new HttpSessionRequestCache(); + SavedRequest savedRequest = requestCache.getRequest(request, response); + if (savedRequest != null) { + String targetUrl = savedRequest.getRedirectUrl(); + // 清除保存的请求,避免重复使用 + requestCache.removeRequest(request, response); + // 确保重定向URL是安全的(只允许重定向到授权端点) + if (targetUrl.contains("/oauth2/authorize")) { + return "redirect:" + targetUrl; + } + } + + // 如果没有保存的请求或URL不安全,重定向到首页 + return "redirect:/"; + } catch (Exception e) { + // 认证失败 + redirectAttributes.addFlashAttribute("error", "短信验证码错误或已过期"); + return "redirect:/login"; + } + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/filter/CaptchaAuthenticationFilter.java b/auth/src/main/java/com/example/springboot4/filter/CaptchaAuthenticationFilter.java new file mode 100644 index 0000000..e55ed3b --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/filter/CaptchaAuthenticationFilter.java @@ -0,0 +1,65 @@ +package com.example.springboot4.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * 验证码验证过滤器 + * 在用户认证之前验证验证码是否正确 + */ +public class CaptchaAuthenticationFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(CaptchaAuthenticationFilter.class); + + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + + // 只处理登录POST请求 + if (!shouldProcess(request)) { + filterChain.doFilter(request, response); + return; + } + + + // 获取用户输入的验证码 + String inputCaptcha = request.getParameter("captcha"); + + // 获取Session中的验证码 + HttpSession session = request.getSession(false); + String sessionCaptcha = (session != null) ? (String) session.getAttribute("captcha") : null; + + // 移除Session中的验证码,确保一次有效性 + if (session != null) { + session.removeAttribute("captcha"); + } + + // 验证验证码 + if (inputCaptcha == null || !inputCaptcha.equalsIgnoreCase(sessionCaptcha)) { +// logger.warn("验证码验证失败. 用户输入: {}, Session中: {}", inputCaptcha, sessionCaptcha); + // 验证码错误,重定向到登录页并携带错误参数 + response.sendRedirect("/login?captcha"); + return; + } + + logger.info("验证码验证通过,继续执行认证流程"); + // 验证码正确,继续执行过滤器链 + filterChain.doFilter(request, response); + } + + private boolean shouldProcess(HttpServletRequest request) { + return "/login".equals(request.getRequestURI()) && + "POST".equalsIgnoreCase(request.getMethod()); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/filter/CustomAuthenticationEntryPoint.java b/auth/src/main/java/com/example/springboot4/filter/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..e82c6c6 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/filter/CustomAuthenticationEntryPoint.java @@ -0,0 +1,61 @@ +package com.example.springboot4.filter; + +import com.example.springboot4.handler.ErrorResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * 自定义认证入口点,用于处理认证异常并返回统一格式的JSON响应 + */ +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public CustomAuthenticationEntryPoint() { + // 注册JavaTimeModule以支持LocalDateTime序列化 + objectMapper.registerModule(new JavaTimeModule()); + // 禁用时间戳格式,使用字符串格式 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + + logger.warn("认证失败: {}", authException.getMessage()); + + ErrorResponse errorResponse = new ErrorResponse(); + errorResponse.setTimestamp(LocalDateTime.now()); + + if (authException instanceof InvalidBearerTokenException) { + errorResponse.setMessage("Token无效或已过期: " + authException.getMessage()); + errorResponse.setCode(HttpStatus.UNAUTHORIZED.value()); + } else { +// errorResponse.setMessage("认证失败: " + authException.getMessage()); +// errorResponse.setCode(HttpStatus.UNAUTHORIZED.value()); + response.sendRedirect("/login"); + } + + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding("UTF-8"); + + String jsonResponse = objectMapper.writeValueAsString(errorResponse); + response.getWriter().write(jsonResponse); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/handler/ErrorResponse.java b/auth/src/main/java/com/example/springboot4/handler/ErrorResponse.java new file mode 100644 index 0000000..6bbf697 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/handler/ErrorResponse.java @@ -0,0 +1,36 @@ +package com.example.springboot4.handler; + +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 统一错误响应格式 + */ +@Setter +@Getter +public class ErrorResponse { + // Getter 和 Setter 方法 + private boolean success = false; + private String message; + private int code; + private LocalDateTime timestamp = LocalDateTime.now(); + private Map data; + + // 构造函数 + public ErrorResponse() {} + + public ErrorResponse(String message, int code) { + this.message = message; + this.code = code; + } + + public ErrorResponse(String message, int code, Map data) { + this.message = message; + this.code = code; + this.data = data; + } + +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationConsentService.java b/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationConsentService.java new file mode 100644 index 0000000..873e7b2 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationConsentService.java @@ -0,0 +1,59 @@ +package com.example.springboot4.service; + +import org.springframework.data.redis.core.RedisTemplate; +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 java.util.concurrent.TimeUnit; + +//自定义Redis 授权同意服务 +public class RedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService { + + private static final String CONSENT_KEY_PREFIX = "consent:"; + // 可选:设置授权时间,授权时间内不需要手动点击授权按钮(例如 30 天) + private static final long CONSENT_EXPIRE_DAYS = 60 * 5; + + private final RedisTemplate redisTemplate; + private final RegisteredClientRepository registeredClientRepository; // ← 新增 + + public RedisOAuth2AuthorizationConsentService(RedisTemplate redisTemplate, RegisteredClientRepository registeredClientRepository) { + this.redisTemplate = redisTemplate; + this.registeredClientRepository = registeredClientRepository; + + } + + @Override + public void save(OAuth2AuthorizationConsent authorizationConsent) { + if (authorizationConsent == null) { + return; + } + String key = getConsentKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName()); + redisTemplate.opsForValue().set(key, authorizationConsent, CONSENT_EXPIRE_DAYS, TimeUnit.SECONDS); + } + + @Override + public void remove(OAuth2AuthorizationConsent authorizationConsent) { + if (authorizationConsent == null) { + return; + } + String key = getConsentKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName()); + redisTemplate.delete(key); + } + + @Override + public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) { + RegisteredClient registeredClient = registeredClientRepository.findById(registeredClientId); + if (registeredClient == null) { + // 客户端不存在,即使 Redis 里有 consent 也视为无效 + return null; + } + String key = getConsentKey(registeredClientId, principalName); + return (OAuth2AuthorizationConsent) redisTemplate.opsForValue().get(key); + } + + private static String getConsentKey(String registeredClientId, String principalName) { + return CONSENT_KEY_PREFIX + principalName + ":" + registeredClientId; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationService.java b/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationService.java new file mode 100644 index 0000000..5bcae48 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/RedisOAuth2AuthorizationService.java @@ -0,0 +1,143 @@ +package com.example.springboot4.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; + +import java.time.Duration; + +// RedisOAuth2AuthorizationService.java +// Redis 自定义存储授权信息 +public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService { + + private static final Logger logger = LoggerFactory.getLogger(RedisOAuth2AuthorizationService.class); + + private final RedisTemplate redisTemplate; + private final RegisteredClientRepository registeredClientRepository; + + public RedisOAuth2AuthorizationService(RedisTemplate redisTemplate, + RegisteredClientRepository registeredClientRepository) { + this.redisTemplate = redisTemplate; + this.registeredClientRepository = registeredClientRepository; + } + + @Override + public void save(OAuth2Authorization authorization) { + try { + String key = "oauth2:authorization:" + authorization.getId(); + redisTemplate.opsForValue().set(key, authorization, Duration.ofDays(30)); + // 同时按token值存储索引 + if (authorization.getAccessToken() != null) { + String tokenKey = "oauth2:token:" + authorization.getAccessToken().getToken().getTokenValue(); + redisTemplate.opsForValue().set(tokenKey, authorization.getId(), Duration.ofSeconds(300)); + } + + if (authorization.getRefreshToken() != null) { + String refreshTokenKey = "oauth2:refresh:" + authorization.getRefreshToken().getToken().getTokenValue(); + redisTemplate.opsForValue().set(refreshTokenKey, authorization.getId(), Duration.ofDays(14)); + } + + // 添加对授权码的支持 + if (authorization.getAuthorizationGrantType().getValue().equals(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())) { + OAuth2Authorization.Token authorizationCode = + authorization.getToken(OAuth2AuthorizationCode.class); + if (authorizationCode != null) { + String codeKey = "oauth2:code:" + authorizationCode.getToken().getTokenValue(); + redisTemplate.opsForValue().set(codeKey, authorization.getId(), Duration.ofMinutes(5)); + } else { + //第一次授权,需要用户手动点击授权按钮,根据state存储,用于后续验证 + String stateKey = "oauth2:state:" + authorization.getAttribute("state"); + redisTemplate.opsForValue().set(stateKey, authorization.getId(), Duration.ofSeconds(300)); + } + } + } catch (DataAccessException e) { + logger.error("保存OAuth2授权信息到Redis时发生错误", e); + throw e; + } + } + + @Override + public void remove(OAuth2Authorization authorization) { + try { + String key = "oauth2:authorization:" + authorization.getId(); + redisTemplate.delete(key); + if (authorization.getAccessToken() != null) { + String tokenKey = "oauth2:token:" + authorization.getAccessToken().getToken().getTokenValue(); + redisTemplate.delete(tokenKey); + } + if (authorization.getRefreshToken() != null) { + String refreshTokenKey = "oauth2:refresh:" + authorization.getRefreshToken().getToken().getTokenValue(); + redisTemplate.delete(refreshTokenKey); + } + } catch (DataAccessException e) { + logger.error("从Redis删除OAuth2授权信息时发生错误", e); + throw e; + } + } + + @Override + public OAuth2Authorization findById(String id) { + try { + String key = "oauth2:authorization:" + id; + return (OAuth2Authorization) redisTemplate.opsForValue().get(key); + } catch (DataAccessException e) { + logger.error("从Redis查询OAuth2授权信息时发生错误", e); + throw e; + } + } + + @Override + public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) { + try { + String authId = null; + + if (tokenType != null) { + // 1. 如果类型明确,直接获取 Key + authId = (String) redisTemplate.opsForValue().get(getTokenKey(token, tokenType)); + } else { + // 2. 如果类型为空 (revoke 流程),遍历所有可能的 Redis 前缀 + // 这里的顺序建议:Refresh Token -> Access Token -> Code + String[] possibleKeys = { + "oauth2:refresh:" + token, + "oauth2:token:" + token, + "oauth2:code:" + token + }; + + for (String key : possibleKeys) { + authId = (String) redisTemplate.opsForValue().get(key); + if (authId != null) break; + } + } + + return authId != null ? findById(authId) : null; + } catch (DataAccessException e) { + logger.error("根据Token从Redis查询OAuth2授权信息时发生错误", e); + throw e; + } + } + + private String getTokenKey(String token, OAuth2TokenType tokenType) { + // 增加空指针保护 + if (tokenType == null) { + return "oauth2:token:" + token; + } + + if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) { + return "oauth2:token:" + token; + } else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) { + return "oauth2:refresh:" + token; + } else if ("code".equals(tokenType.getValue())) { + return "oauth2:code:" + token; + } else if ("state".equals(tokenType.getValue())) { + return "oauth2:state:" + token; + } + return "oauth2:token:" + token; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationProvider.java b/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationProvider.java new file mode 100644 index 0000000..51f984e --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationProvider.java @@ -0,0 +1,47 @@ +package com.example.springboot4.service; + +import org.jspecify.annotations.NonNull; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +@Component +public class SmsAuthenticationProvider implements AuthenticationProvider { + + private final SmsCodeService smsCodeService; + private final SmsUserDetailsService userDetailsService; + + public SmsAuthenticationProvider(SmsCodeService smsCodeService, SmsUserDetailsService userDetailsService) { + this.smsCodeService = smsCodeService; + this.userDetailsService = userDetailsService; + } + + @Override + public Authentication authenticate(@NonNull Authentication authentication) throws AuthenticationException { + SmsAuthenticationToken auth = (SmsAuthenticationToken) authentication; + String phone = (String) auth.getPrincipal(); + String code = (String) auth.getCredentials(); + + // 验证短信验证码 + if (!smsCodeService.verifyCode(phone, code)) { + throw new BadCredentialsException("短信验证码错误或已过期"); + } + + // 加载用户 + UserDetails userDetails = userDetailsService.loadUserByPhone(phone); + + // 创建已认证的Token + SmsAuthenticationToken authenticatedToken = new SmsAuthenticationToken( + phone, userDetails.getAuthorities()); + authenticatedToken.setDetails(userDetails); + return authenticatedToken; + } + + @Override + public boolean supports(@NonNull Class authentication) { + return SmsAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationToken.java b/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationToken.java new file mode 100644 index 0000000..b19acca --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/SmsAuthenticationToken.java @@ -0,0 +1,43 @@ +package com.example.springboot4.service; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class SmsAuthenticationToken extends AbstractAuthenticationToken { + + private final Object principal; // 手机号 + private final Object credentials; // 验证码 + + /** + * 用于认证前的构造函数(未认证) + */ + public SmsAuthenticationToken(String phone, String code) { + super((Collection) null); + this.principal = phone; + this.credentials = code; + setAuthenticated(false); + } + + /** + * 用于认证后的构造函数(已认证) + */ + public SmsAuthenticationToken(String phone, Collection authorities) { + super(authorities); + this.principal = phone; + this.credentials = null; + super.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/SmsCodeService.java b/auth/src/main/java/com/example/springboot4/service/SmsCodeService.java new file mode 100644 index 0000000..11f4912 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/SmsCodeService.java @@ -0,0 +1,67 @@ +package com.example.springboot4.service; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +public class SmsCodeService { + + private final StringRedisTemplate redisTemplate; + private static final String SMS_CODE_PREFIX = "sms:code:"; + private static final long EXPIRE_MINUTES = 5; + private static final int CODE_LENGTH = 6; + + public SmsCodeService(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + /** + * 生成随机6位数字验证码 + */ + public String generateCode() { + Random random = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + sb.append(random.nextInt(10)); + } + return sb.toString(); + } + + /** + * 保存验证码到Redis,5分钟有效 + */ + public void saveCode(String phone, String code) { + String key = SMS_CODE_PREFIX + phone; + redisTemplate.opsForValue().set(key, code, EXPIRE_MINUTES, TimeUnit.MINUTES); + } + + /** + * 验证手机号和验证码 + */ + public boolean verifyCode(String phone, String code) { + String key = SMS_CODE_PREFIX + phone; + String storedCode = redisTemplate.opsForValue().get(key); + if (storedCode == null) { + return false; + } + // 验证通过后删除验证码,防止重复使用 + if (storedCode.equals(code)) { + redisTemplate.delete(key); + return true; + } + return false; + } + + /** + * 发送短信验证码(模拟) + */ + public void sendCode(String phone, String code) { + // 这里模拟发送短信,实际应调用短信服务商API + System.out.println("发送短信验证码到 " + phone + ": " + code); + // 保存到Redis + saveCode(phone, code); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/service/SmsUserDetailsService.java b/auth/src/main/java/com/example/springboot4/service/SmsUserDetailsService.java new file mode 100644 index 0000000..aa49284 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/service/SmsUserDetailsService.java @@ -0,0 +1,88 @@ +package com.example.springboot4.service; + +import com.example.springboot4.config.Users; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; +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.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.JdbcUserDetailsManager; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.util.Collections; + +@Service +@Primary +public class SmsUserDetailsService implements UserDetailsService { + + private final JdbcTemplate jdbcTemplate; + private final JdbcUserDetailsManager jdbcUserDetailsManager; + private final PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + public SmsUserDetailsService(JdbcTemplate jdbcTemplate, DataSource dataSource) { + this.jdbcTemplate = jdbcTemplate; + this.jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource); + } + + @Override + public @Nullable UserDetails loadUserByUsername(@NonNull String username) throws UsernameNotFoundException { + + // 模拟数据库 + if ("admin".equals(username)) { + return new Users("admin", passwordEncoder.encode("123123"), + Users.withUsername("admin").password(passwordEncoder.encode("123123")).roles("USER").build().getAuthorities(), + "凌萧", "https://img.makesong.cn/10.png", "13777777777"); + } + return null; + } + /** + * 通过手机号加载用户 + */ + public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException { + // 查询users表,假设有phone列 + String sql = "SELECT username, password, enabled FROM users WHERE phone = ?"; + try { + return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> { + String username = rs.getString("username"); + String password = rs.getString("password"); + boolean enabled = rs.getBoolean("enabled"); + // 暂时忽略权限,使用默认权限 + return new Users(username, password, Users.withUsername(username).roles("USER").build().getAuthorities(), "", "", phone); + }, phone); + } catch (org.springframework.dao.EmptyResultDataAccessException e) { + // 用户不存在,自动创建用户 + return createUserByPhone(phone); + } + } + + /** + * 根据手机号创建新用户 + */ + private UserDetails createUserByPhone(String phone) { + // 生成用户名:手机号 + String username = phone; + // 生成随机密码(用户无法用密码登录,只能短信登录) + String password = passwordEncoder.encode(generateRandomPassword()); + // 使用JdbcUserDetailsManager创建用户 + jdbcUserDetailsManager.createUser(org.springframework.security.core.userdetails.User.builder() + .username(username) + .password(password) + .roles("USER") + .build()); + // 更新phone列(需要自定义SQL,因为JdbcUserDetailsManager不处理phone) + String updateSql = "UPDATE users SET phone = ? WHERE username = ?"; + jdbcTemplate.update(updateSql, phone, username); + // 返回用户详情 + return new Users(username, password, Collections.emptyList(), "", "", phone); + } + + private String generateRandomPassword() { + // 生成随机字符串作为密码,用户不会用到 + return java.util.UUID.randomUUID().toString().substring(0, 16); + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/util/CaptchaUtil.java b/auth/src/main/java/com/example/springboot4/util/CaptchaUtil.java new file mode 100644 index 0000000..9d16ef5 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/util/CaptchaUtil.java @@ -0,0 +1,112 @@ +package com.example.springboot4.util; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.Random; + +public class CaptchaUtil { + + private static final char[] CHARS = {'2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', + 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + + private static final int SIZE = 4; + private static final int LINES = 5; + private static final int WIDTH = 120; + private static final int HEIGHT = 40; + + /** + * 生成验证码图片和文本 + */ + public static Captcha generateCaptcha() { + BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics g = image.getGraphics(); + + // 设置背景色 + g.setColor(Color.WHITE); + g.fillRect(0, 0, WIDTH, HEIGHT); + + // 生成随机验证码 + Random random = new Random(); + StringBuilder captchaText = new StringBuilder(); + for (int i = 0; i < SIZE; i++) { + char c = CHARS[random.nextInt(CHARS.length)]; + captchaText.append(c); + } + + // 绘制验证码文本 + // 使用系统默认字体替代指定字体 + // g.setFont(new Font("Arial", Font.BOLD, 24)); + Font font = new Font(null, Font.BOLD, 24); + g.setFont(font); + + for (int i = 0; i < captchaText.length(); i++) { + g.setColor(getRandomColor()); + g.drawString(String.valueOf(captchaText.charAt(i)), 20 + i * 20, 30); + } + + // 绘制干扰线 + for (int i = 0; i < LINES; i++) { + g.setColor(getRandomColor()); + int x1 = random.nextInt(WIDTH); + int y1 = random.nextInt(HEIGHT); + int x2 = random.nextInt(WIDTH); + int y2 = random.nextInt(HEIGHT); + g.drawLine(x1, y1, x2, y2); + } + + g.dispose(); + + // 将图片转换为Base64编码 + String base64Image = imageToBase64(image); + + return new Captcha(captchaText.toString(), base64Image); + } + + /** + * 获取随机颜色 + */ + private static Color getRandomColor() { + Random random = new Random(); + return new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)); + } + + /** + * 将BufferedImage转换为Base64编码 + */ + private static String imageToBase64(BufferedImage image) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "png", outputStream); + byte[] imageBytes = outputStream.toByteArray(); + return Base64.getEncoder().encodeToString(imageBytes); + } catch (IOException e) { + throw new RuntimeException("Failed to convert image to Base64", e); + } + } + + /** + * 验证码数据类 + */ + public static class Captcha { + private final String text; + private final String imageBase64; + + public Captcha(String text, String imageBase64) { + this.text = text; + this.imageBase64 = imageBase64; + } + + public String getText() { + return text; + } + + public String getImageBase64() { + return imageBase64; + } + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/example/springboot4/util/RsaKeyLoader.java b/auth/src/main/java/com/example/springboot4/util/RsaKeyLoader.java new file mode 100644 index 0000000..8d75f38 --- /dev/null +++ b/auth/src/main/java/com/example/springboot4/util/RsaKeyLoader.java @@ -0,0 +1,48 @@ +package com.example.springboot4.util; + + +import org.springframework.util.ResourceUtils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * RSA密钥加载器 + * 用于从本地文件加载RSA密钥对 + */ +public class RsaKeyLoader { + + /** + * 从指定路径加载私钥 + * + * @param privateKeyPath 私钥文件路径 + * @return PrivateKey 私钥对象 + * @throws IOException IO异常 + * @throws ClassNotFoundException 类未找到异常 + */ + public static PrivateKey loadPrivateKey(String privateKeyPath) throws IOException, ClassNotFoundException { + try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Path.of(privateKeyPath)))) { + return (PrivateKey) ois.readObject(); + } + } + + /** + * 从指定路径加载公钥 + * + * @param publicKeyPath 公钥文件路径 + * @return PublicKey 公钥对象 + * @throws IOException IO异常 + * @throws ClassNotFoundException 类未找到异常 + */ + public static PublicKey loadPublicKey(String publicKeyPath) throws IOException, ClassNotFoundException { + try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Path.of(publicKeyPath)))) { + return (PublicKey) ois.readObject(); + } + } +} \ No newline at end of file diff --git a/auth/src/main/resources/application.yml b/auth/src/main/resources/application.yml new file mode 100644 index 0000000..a8ca0c8 --- /dev/null +++ b/auth/src/main/resources/application.yml @@ -0,0 +1,41 @@ +#logging: +# level: +# org.springframework.security: trace +# org.springframework.web: debug + +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://192.168.1.14:3306/test3?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=true + username: root + password: lingxiao + hikari: + minimum-idle: 3 + maximum-pool-size: 10000 + max-lifetime: 300000 + connection-test-query: SELECT 1 + connection-timeout: 300000 + main: + allow-bean-definition-overriding: true + data: + redis: + port: 6379 + database: 1 + host: 192.168.1.14 + password: lingxiao + lettuce: + pool: + max-active: 2000 + max-idle: 2000 + min-idle: 2 + max-wait: 2000ms + timeout: 2000ms + connect-timeout: 2000ms + threads: + virtual: + enabled: true +server: + port: 9000 + jetty: + threads: + max: 1000 \ No newline at end of file diff --git a/auth/src/main/resources/static/favicon.ico b/auth/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bdb06342d4aaed62f58339ba9a00e5192dc304e4 GIT binary patch literal 269374 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

N`ZWp zWz*tEvUSDpQtJDLM(3k1j&iOxKiN0iDUked4Zn`=9PQ0k7DG4*>8>+9m@SD$o}ffG7Ea2+#dkj+#UcMQz`x1s zrQY}Y0A#;<`;iC!6E9)XEZHz0->l_11JH+e7xH+i&kgsMajWw`TS7L@g5K|oIk-JBN@^p(|I(Qhm+hl9o}~u#I=3ICs`7V z$(u!14zGw@XSM@&k$@2+q<6KY$nn_-osY3T%CXOn$pvUoVmjLY%S+Czm1WB41NuF+ z9)LsgX!#_&)Dg?ZKbGZ~b6dx2B>SiXaq0)pL$UMoX#K!8mmK?Hdf)4^1oOJv>w1J(Yo-E?|tB$6)%CVG?X^Umq#s2VRp?XWmH|+I&aCCZJtUolae@oV$2; z?|0h+JlC`BS||J4H$=%;%w>1z5Muyu@q5>0#hll&8|Q^Nzq1Rr(DE5?WO$D&s{bjw zZX@3({OA=KJ!!F2tKCYnx|M7gE&(?X2bz&c6C}aE& z;MaEhWejYVRFV{9KK@*zuhy4-Jyz;CT*Fg%laQ^>k0)>gLLmM!|!Cj!T5tdDx1w!USb{70Z1gb!fie~~f< z{dzoY0FK_T-fmbp$Lva;ICH!~Qx{|pxb)&?Alfyk`&IutOS29+pxdSmP;CIRPrbhk za|=2CKJSm5M=9@y`^ddbm4EZB&NJiwW3;q_-CPRoC`oi|Js;l;=r3ss$J|MhJS}8E za`8=;Sn8I|B-;-#mJ>SJX z;|h}pp2gVgqh#N~yYl2u@^0JF%#!TWrayD}y#$3mmmRyFNf0>RAMr-cT>Pl|d<$&q zq1`Tk^(c=WPcBy@emQl-H8~GofAY{PG9U6x`3942&O>ldEXuee6FjUt{JOWT%;0Y< zoArlG8EJlF7Hs^P(DT9D(C-txSee1!+OYT?bn&v3RzMteX5>*`M0; zh74+YT$YS|sO>MxIORAC-uwS5pUVP%YtF@U{SxLR#W$!_u_@v$DbV(iRR;gMQMRpm zE+xMES;vL4I^N5FR?Q*LnEpHI(qM+w`xn*UH{2ul2D}^Y$^R#n2LaUoN5KC9wgI61pX_Ts z|ByXkKcR23EYSPQGHtv3$F)G%532jA|H(R+_rbju?(@oqU62#XzUqDC^zK1y0{7}+ zd@ndB_j^#dPMLtE+5q65alV(kTS>hNISk*(!Dl>G=hEa7xzikXhw^}SSAvAeByGy1 z(yB&Y**&`<@}Gvt_?GjK2aLYJ;J+i;M;<`OikqZZu9g@#H@W1`Q&U#XyRLP@T?dqD z)C+$9P}u+L=e|PjdE9v#m=n(P=p$Ge;9ipdMWY|c48+erAM=mD<<|SWMlj-SLpz^> zZ+u$rAXaABH{)iRwsg~WZpPp+a)52}Y&&PUcI%^z?t4NOAbv<)YxOyobH+Tvd{Osg z2jcp}VRLVzY#_^)XL~;G6of|JUOmfbmH~?#iZBk7V3yO+c0RdZJ?tLFq?|1u_hOv+gUyi#(^BpPG?VZ-wb3W7w*0x<;T-#^>pzlt)t}_W zZ5!}BZiD@xv550uRGa?*m^Ivc*=OvJx}S1k>$O;!b`)(=2Tja>oU%Vu+4o7BCEken zYqM;-^72Lyt`nrZ8_okA)_2-{SO(rhj7Q}He3=92zYq3HUIna=you zNGw%`VHu76(O^Fo?Rgj1HIlqpQzD*%ep2K-Wd_4O=h6lHDV)OfJ00Ha{1YM z2=qVO30rWrfNM+!OiMW8~G@?>X1u*DW)s^8?m>l(A?Z*}C?zjQQ;F*@FL} z|6$puY??OmlH7d^9SZ;D;E}hoc`I^HS3QxHYo5sFZNJN&Loemp!)UqZJm;AU@6eWT zLi)FlK>x%b9n&^p;!HAk`f-ypfp_QmZdl(9-+w9Mj&lb-kg**v$>0_z)pp}IaeoK< zz3sj@OM6XxFMHl`^o3!Jf93LUNsNo)IO3klaH(zS{^)O zJgdF1naPgTpI9nGx^(1#>QL(av54~<_Klry^*+bbH?qL6@9$eW{S##3_AAiH%U^#4oE29eVa=BT2_~# zO%MaDvL?1aT5AIaPS_hn?8vofshDcO$xyK1AkzNux}{%@QmuETAfqXo9j*<%=^4`cK8 z?>ilO1M!G2zQKGNd8J{ke#q6%hyEe-^PO0}!+uJ%*A&awT*mxzUu}KH_2>7Gk!_P8 z|F%w$djZX5?Wppw39?J3biU|Mo)Xu<*xlKSLeFJEJS(d_+CiO2exUQM{v_vM#>2nE z4{d0}{srj$XBy)(dOy}<_j8*vfqI{FYtRnZhWww|Ct_sce)QpOGuShBN8dUA+2$Yl zTc)B~hJD9k2F2822)deW_prq(VM5Yy#z9ZG#Mw3^BoPkPDwIabhW& zBd=8d4tZAS2gjTc?$@{>a`_X%hh$!FkB*DMzKInp_x$z_$A76KwqEeKWuJA**{^j5 z>)V#UlW`a?kbU~Byx=j7BlBFRDF3()>n8jD4s3T2ZRKZl9!#U-$%oPVmVep;+((=E z*fGRa+8mUPi_u3FoRVL1%=W#_%ihY6PUmDe@{ATB4zGOUn4ZJ)oT7|7`x%?FB^<{! zR;R2xHUZbkGxT}LcG!$V@1alL1sUD@8gc}TY@z(2{7@Of-{Ak?`jZCTkkyNCYF?lx zh6x#_a{)pYFc*mOjP}g26R%`guL#&PC(#ebVHfXA`%!@wi^*4X!G-+LhP6c!DC zh+MflGPL3FV)-lMSS%SALRCwkJoojK-O6iFK^5{4c`~` z0rh_f{x1mU2cmGPg+75(g0XKdY`1>u?=O&d4*Q04$In;xxo%`wv_403wfX-HZzavD zpwA6rd9>%PeNVpK?599qT+X%a-Dhk7BL^~SJn$>D4}Jj|{V(K$e2xBb$JO^(d5q_MUbx?__sM)X${t*Y>*Uxj^*ron8^Xx$S7iY5W|vIGm=!z^ zZDEuR9_}6Xg9FfKw9iEe*mPg-+uJX|VP9o{@neo2e<_2z?U#`~PU~EV`i|}U=Cfg) z#qPJ;~nor`)znEA~;rVTtU2V z5Bl!qEP;NQ7y~#(dif@auly(eH}d7zNtE9-$lXt)ZTeXybB1Elsa}BU`?-A)|M82G zWy1qyWT%Ewwg~!>rbr3@Acy42T|x>Kt|Uc^RzsR~5NK6;SWf%J^SOf_boS zx#!Z&KGtbRTeeQ_BKwH-?J?{h28(v?EVu8^*dKiWa?d$*ITqLUs~@CZhXIEYe3{ud zH*$LW&)c4DX2i>zGf&3K+|$vr5&Q=~fXoN~!7Nx0LLu+L^b4>I!FA>$ug}AN7~&Mr z`}Y1c#-}=;dLQ@g$S(RcF=(TFj&}V@#WQM~z7PF+>T_K>cD}A>Hq0yk9E;0x@AU;3 zC-^!6#>4*imypj1(I%5xlH+~)q)4aiQw}6ao(8$4KImUpPc|-j6elLc@9!>JN1Qrm z+1Gl<^RVA@F4pPiPety|dhlo2_8hm@<+V_k^|-TS`@-<9Cy?)d0CQ(Yd)oM{&bRuK z?B9l8Ji6aenKAsf!=2Hu%04(;GWn_WM{dgu#NtAsBmb6r%QWQ%3i(xr$uYTi*eCb4 z=y}+`e&HLmSzVN|;B4onDDdvY_SwdW^Nn3$)_1}88HzE}LxZ2Fy<*~-81wQE^cB!z z^!?G}uVhe{eVX^M6E>5PtzP}k_hngk+g*Hzd-05$=D&~yL+;7cp4Vki!5Zzsm&HFG+#Zuz2KCM5q~euH^dlj)cc%->6! zHa~I?=7alaZO|1|Y*=L;J!@k$g_p)zdf7bt3`2c43-FYufJN|ko z{uf!4eZNJ4S!W%3b%X5BI061oMau@&|Av2K1Gw0SFAxI$=PCQhFGD{6`!HkkM}~v* zaPsch`?_vr7Gwit0NX783~eFJt7ONRItfkNKJ+xl+VlALc<*Wp(C2s9SN)#~HUbL2 z371I_C#1grH}b<63shU5+`HIMoD_Lw@e`xJY$j>bXa?fM(N2AfTR&J=v@SSxkLwyE zz|z)5PkkcODD%-5W94MTve&h(8;4h7WWC!AY zw0}*WJnekbM~3}oxc}to*YaE2eX<7U8=XtNYYOf4dB{&1*y1$i5dTBm#j@Sg0qP~LIRTy9$aP+f(`=9!sG9U!{ zKLolz)Y1EWk|E`&R6d3GK1>&FXA&d>?{3WAK3UlKI!q@Oi2!#jWz)N z3ib=_(yW72FWEz;jX0%zGJn^@vgO`gKb(E`9JL8t^@??EE#`I@(LGEW)fpsZOE;5- zbqAtP4918CBc@)NdP`fM?6bXr^Y`vUJ)Z#kcv$bf81LeP#=RawH+uTYVV(T!+;$IR zfgg|^h_x%LxQ^;}EXnbVL3c1G@kNXo9cg%W_LFs6*vEl=3@!h~Kc5 zFE_CQeaE~PdrjwC+3&~w_M6yqxV;Z@?D84J9gmnfcoHDLl=i+a^^2o>vggFO@Mwo- z?osCSKgghtCooogs1(oN26c}8^>ay{yrrdR5$bj7bi?*{u=BZ1&c7>0Tc0w(*~T(& zzOs@UzQO9@l_YjQGN&xjc{T1#O6w#hCwb*?dFm; z0Ob&ivd(A3I&FIO>9LF>Qz#Qq7y}607%5|WT$AO~VRP_2{IBxQC47D>2Plhp{)(B; z#P62~xqS13iSOe%X!kSLf9pY%OdoSXdNtc4n;|2Vd;Fi}(K~jU_xn~dz zhJJ;aVcRE%pP1TV-`M?`GUrovDHjqXNGG|o*M>izA2It}>dzNef1Y-|;X5wlZrDU*e5F*BUA3lzL5nd!9H_)W+1OmeR+p(ue~1&-dV^# z$J;UfKF8gs-f!BWr?&5>NRBqXrJ>noAASRWA9^3`Q}@q29xF4D+pP9K+6U+vV7=rXes2d!^F>vEM#EYWU9=G*J8F%WAwL!Vh)>p=( z=T1B(-+xydaho4e%1eImU%O@x*|q77)>-SXYJGzqU5|O+$Mw1-qq|>_g<~Gd7%cm> z50T@iUTZr(=hd_2{uAV54LB&XhTrhYB=X`E=BrNr?S?Fyj5c%J3mM_P>pgP)v%>#V z{{wZOER%m_nM)kg_ zRCZB*aSVWazsJaux#y+x&pTx$>?Gcoz85a-{W|a2ZtFAfm~$>=X^SSqp(o48w|T&R zmLg~;D<<9BFTgc0rasQKww3)~tM7S?ae^(&AIrEQyQF{5<+23xDf0jLKS%eV&+gSv z(y7C6^qclozdm7NUq|;F_No7Avll8{0pmcSPXc1g88YUPf*2#bFyi(_ieij1k1tQY z-Hca(&u?|T-EWr0=g+SFS6&37@5<*opT53_ed9Y@_NODx&vrbw&2M>K`-eQ=0I&aV?0>ZZ!2W!U+e7vPx1tX( zY<@TU#^!JAvHSHH`G;&E`z+Cj3B;&uL;MfRSZ({q+&#$myRo6MY(@M3uT2Zd4+Ya< z{!Pwt@eSr+NUZk#R|(iJ}ivO$`a$xBWX9=+h!`<8jDk2n`&%f`c{K!NgF2MQJ{uj@U# z%!Pc2@ziVO4Rwt5jN`loqkUi@`VkLqcT)N^J}lkqhRgWACuP;b8?w{?fgC*e0)6rR zlv5{O!hhZkwjW6-a^*uY?mYcr%LC_*nlbP;=95M40R$17U2y5#=0gk;HD2);7L`zqf` zHm`V~x?HcrXHeG7b@_eQEVv;9daabI6+2;G%_^`RD&YUx!Z*97vi{6r*!zcnmm-C# z$yZ?iYv_H`{sg-dcE1+rk&K!0NtMbiB|G{g{QHXpkPR875ZGs%9T~RltL~2@2N>_O z>{m6hKaXsvBIz^cmEw8R$lcA2KV#q8&yLQg%?~+nawqiustcm*Q>VN1{^#t|&p-SK z#bH0<-bZQIW8hxo^d?H>lOx`Q&Bs&EIri>6*;k*Ry5G_NvC829hI^savp{>UN3?2pR8Hph_v zdt65Ra!h(P+=E!#NzC7R4r98Xk;M~COq=|$U2EZ}hbAA{sq@MW)~#(|m!5lZ&QsRG zmp+qgpWZ9^$9*my_gJ>VFCo{~F0j|L^1yEM|3>vXDT`;H(;Qvq={|Xb`5lfw!?;|V zWh{=d{}k_A?F#2NXmhcxbnTqyh%4ULbr1WzFI?9?Q_!Zj(y-nD$&m|Ve`P6zzJA5= z-6~*Q@Kt!HMu%H_p64!@c1pS;@3VZ_mSFxTDTMD+2=bY{vTy$2zMJF#e3c6)@T~K~ zFt2(}*z>UI5g+;s`|A5=l<$kzkm}Xi!5__te#r@O9|bj*Z`rrp+og%=SN8h+kO4*x zR8~7NdyZn-PFS?z#YdWpAVYah@h`9i)B@-1u4=p&k}M8*QP&6;Zdqet`yK^a$zKX&+*HkOQrbAQphQSc_vaxb<1& zL0yz-18&NeC9jQ+!~c=}b+ezNt&wpuY&$IT{ye{l-=VH1ziy7~xo-AVU*deqr`@nK zmZDEk2*yMV$Me%4XCGJP(7AW}8)3MQZ5XG>uYCmi62bR+Ac3$e=mc221xft zJ7ha#NXUj5?b959*xUy6KUxf%Vrst|G8+ApCL?cfcSF{4nSZ-<0q(HMflZW_rxalV57r>MA8m z)<&B$eA2`zH3mWUZ9AUof9Q47mQQ_8`@XvI8E`Lb?@tGRf!9x)%2#TX&LW}Hs;f>n zu`)0YCTKf*VrOJT_aW9tz3+U?G2*!$G|haQ0a-b0+RI*Y}dy!Aw?dB@(J zHF_WHgLAd_>G!+J49EiV&-OpI@6X5h{LJT9n}0XjC-nXd`%z#%5`|0J0F3#6L~$=| zJAknP_?#?`jH92!vKMg#6!QP+KubBbytZUdmk8t9e2q4NH2VJN)3Z%a?R_q32jDyD ze#nKyh#P*LAd$4GS5Iz)?2@pBJEeKqSu%gr4UB7ZL-hjvb=3>tm-Q(>@~;YFy-LM) zGJN1B@m~W!6}q0>l8p%?g79rIPPW!3>ir7xL#c+cY_=H_&|PP(-ty>LM+R8d$m)y{ z7cd@8D;YIzzsz6wP^QngDNX9ll9sg=%9cfcq75-hx#MqJ=IF07e(268vbG7Y={eV7 z$iox29a*6=fZI+{xdg`8?!Fdz3;Zo}-{k%?_VGXDapxMu?=e40@A`Yt&*FfL=y^iM z^*<|P`W(mjxm%@QvjZ~rmy3{J=ftnoDH+iGgbYCcn;~t^K%U)z9C)cd4e!CeBabZO zJ}cL-9Cq#R%|y1zla-q864 zFdhl=elf>h(V~@C6U$9EXtD;HvkX%W}QoT2$MU@qB0vUOY) zdFYRN4?EXu3p+9W&)8?18P7SfbF>rdGyeYP>_4``F$` zd62}1<6S0_dX=lng&qF#^uTVpx+6?}{ds}(Z@C+GXbi^sI4X5&^h7=RNx3gn2;4Jv zhq}W$R0M6cwX65Q9FMy&cEn39KiB$Ibqc>N3+q;wwo^5Jpd67=SG{%DY0IbOj-2jT z87U2`OqIIzN64Bje`0Kp_fo&!Sn1q!x$)z{n8#k=by2t-=Qk`PSn@aQah_w>^*5d4 z^sU*}q4&#c!u}@fJU3v)JM>#cI~;#Q&v&tpb5#!T7|+M{x;f9JMw#)_uHFXe)nc!7 zsOz`{b)cQp#|I|uzM&l#5EKlAN z%70<#g8cc*A~&$Oq|E@`j{i%ZJcVRP<10TDO0Vr%o5xg^YwH^!#|3?OQKxOr?nl;r z6lFlyjXq?oOm#k%A5q9Y=i;29u|Cs|XMO(vOZRhrJ+=9J4}s19P4cW+N|Al)eXt*^ zZG6hU^6zDT7Pe<0{>K;~kI#kwKOSRC^j&dH_5S-%wDF;@QWW@)0pqmuRp)zn$G%X+ zGPJ-p;5km=`T9TjUp}Yi2|)jA|A27Q<`IK;bZKo9^!rN=UmxSzCu9Bv*tZ4W#GB7! zYyirHZ_uuvBXc$hUA9D?><^X4dxPZRo-kQBV7;{cX{Bsk{7QA#CfJJoe_bUN%C*!w zrS*lrA@w@=F9>~63~{DL^$-*6Fb#6OhN)W)`z4BD47u^gjGvD>`gcrfn<81W>^Vzr zv&~`Jkn>U?Pa_#S^{A}%edn82)wvw?EkILj}=Ps55Qv2v^ zN4`GiFbspNCF`^oRIcEfmVJAlZaL|GCd!!2bKhcofSo$`C1q|=#@U?0m|Ni@Mi!HM z%I!js=annAmEU?S!hEdP&{i64zLE2sL3qwib=IRV@B+CN7Am89_kwOtAqkNW#`?p! zk#Y|^l6v2VJ^*xo$r81tS(5?ib686Ij3$L&nF9TLH1>qJa{AQ1QY=pzX<0S5OzVMp z*k;s}J6oEd&o27xK*t;Z-7sxzdDM4h+%RDEB7I~VCpR{;!#?KVHte%q-^BY2|C9wr z?;D*D-ygBQBe2ssr&ff^=FgC}P`oJ~o8Qeo;{1>WGr&5F&H3}#0Ei2Y2nPEruY>)C z@;=0{9}WIvlwsNcUcN2oW@%(ZB$%g7U}6K52aFSb`l@545J6y^uSN8VtUCQao2-ViWP?t{R8h(s*kDII>AC({RAP-bX1 zha$GO67{1~n<<#by{__3o6*>ftVfIo82gd=*+q)r7z+LUmJLVAs+mu{dKA~Q_0}%k z`2=>Kd$}FsXV&;}uvD$pSC*}Nh`8Y|GPHY`>Q&2~vd3#%4xQgoZtOaiWZTp4-8olf z4%eOIe_8UF!-VzmwcQ=IzVe9UmP`FTo{N3-E#{271R2>><}AJ{bsGrSs z{{)ZQLVn%<%XO6n&b9Oyp93s6PLf@iEt8p^VbKA?E4rRSN!lRk?*W>o>uuzpznGxfA9g+22l1nUf*?$>1f!GCi5Qd zd9TLC_i*oQ8$aJX3)ju_Q3ia*KDduzLEQhzw&qf~Sb811HHqVk89Sf-cF4Yo^BbSv zu%9>?#s)~76ytJ@fvr#W1GPLtjc}&Sua91f54bON2Gb9 zVHi)kmdXHfZ{z@UOCj2srf^@W5_M$mq{Hab`(A=Kdi(Nwx%S55v<(Zt%UoQRqFxHYdEr(ZR!=#rj7z#uWfA5_gt31w`kRD1jcL&Lyqw?{FZ6^=euyf zbKW!C9sJtulYz|^%JskydAu)Jo*oR9gX>mGo*cO~=BK%$;NB;t6Zb>B-v@>BaaXL+ zOjx~Ss zTzHo}PF_yz7^5~n+xV1ya*smRJ?z8B_plHCY44N$LoW6+q_yn7J45CT_uw3ivrLEI z&!VwE#{Hdh=pT#-!Z^KPKahESA+XJ%^J&+6SZAC+lm+@7>ru|O`umU(#>aP_S?z!4 zUGTl&J<;!B>^~MZQs9(o7>oNW@&tkd%*bq%+E-ZI<17Q zSS(NRU9H~d^3mR4@t^E3t;;Wzr4#RJopaX>*8f1vyEJK3sMM+66Rabz13I7FYjM~w zg6-naF`ZgZk$Dp@f_?aro_gkD-tPO%=2!iVW46w3LEo7liguAq*-A;SY&B)#K#X5O z7G3Aq`?UY#-nQ%R^F6Hn%{AQT>;I5Ha$)b+wY0zISvu$Lfz0aNW~1cJRaxic$)C42 z#{Yimky+M8@!W^~KWF(l_l!HNTYLv|Z!eIFKekr8ojDYYr_$ci7>n9mjNg-a+Ww5W zRFVeu`pAwA7`qxe-P#QHUAUi#?{q!d6+2X2BO4}cl1KYOQNaIx)Dy&ndbVkUToQ~e z;gd$WPtLlg{G0ZE*0cP`F>Ky!5Ze4Yz)tXyLfL)g@*3pjvt3SEHhMgUI*m51Q!MxD zBZCdb!Bqe0(y@c}|LHfIZ5uCF_C5YS^7@o{!@joTGp>g|__X(FpeqPL3^IybPkf#ItUW>BM zb%%W{eYvi-Kl)#y-$jNDXxH1kUS;|t)%&#j1HpcvT=Wl?zV#Q1Uz>2mnd9nf)^FBH z)*=5@zsqpwo=O#3oAx5ck74J>Dex3Qu~G1<~o+zCW3d)yn@l)j!TcaOXS?v zVCDWH{X*!7-y_0h%j_9w?@xvKRGBks^uGG~?Axa?fb^0seQvb(bpii=h&_IX@uI$v znLSIXFK=vF)M3@mvssBu|wlUAMaWAldb?QpPKJ_EzfTQ=pKIi3`fw?(!d=A?7 zwt)9T7^|7>d7Lxz2=?pT*;wcHVdPx4>**H}FP^Ut=i(eNe|wJE5r5pC4P8EqTwB{8 z+w}R_=BILj>p0G0*k`QoI`a7q`_cag`;lOt#o7V15u(UC^}q8Ty!Ha@B+5$o{jsq3 zZ*OQM1+yi`n9g6JuM~58jeqaa^C}Ar|CW97p9p;~Q>95Khr`3<*{LJ)5Q*v!!;Rgq z_J4SYtQxjK+ErSPI5ozIaq5S=9$G&&d|RZnYdKc)_Naf<{#X7TJ;b;G+XRp+P^D6P z)^xuG-edPKe&&ReI8TP67>H9MVh?pPR`CGLd2K&D`#`;Mpm5Qd9$Ntb+WFB?c z=zJIdY8zu)>%V2daA4(v@=jeyJDKtT*U}goS%C~VxpS1x%QFq^hoP^{0owW)BTapK zvQK>v_T2^Dl;kMN;fImZwx{2rfBz%BZ?11E$MajTIp@M7&+p6u)3Yu6(D!QpLk>{? z8(ZJW_oa-0-X96QZ`gNieh>SW@rKGe^}S`E>xTb^U>`-F(aJs}?F?7A@alfzLQQZ!M3zzOEXHMN_eO$ZfhU#_dqQYwXQ}?h9fO5ds_N=>(@jS}5jq{&N!JG{cgH0>L zehZ8vm+b%jujS|W&FigObW48xp}Dq|JQ*MFcw?k$5yg_*&UD1p}gZc>^<-|r}V!dU1}_qqic7;&S(4=ejvEFKL3@y zp=e7eine6rk5RX1o#K)V4++wCy<44qo)tGF82wt0m=c| zqO=9*i$VqzM86*P*U9|K7T`5<35Koj{Uy~{bH&V9VHWf!#pGI z^PM_uuk$%OcAG3y$J=7^1D--rIul zHzh-=Qc}D~6A4&_IYamy_W#`1`XB514){MP#s-L$i6cU!X4PNNcdwIlY&}Va^bA59 z=w-b>jSoOZ&>v%dWxYDTseeEj%x}OpfRTg!wH;A;r(AP>o7dlt9K^23KbZa7dbz(R z6m~wjx9o?=v*U;4`nj``uRtOBnz^G|&&WIKnZvyt%T=vvd+7e*QoMLg`48%8 zkNO4V9op`#f3N!7Vc*n!)N|H#6fgU<4afnO(0w)+#_C4n15ocbQT7Kr><8RJZpdwX zXN;#8p|O7Ot?YZbC;vxT)aHl%+S3p1`(LAdzht}>!4YW>JbZUC6t=^P3(?U1(Q#~l zw(Hxx9~0wu^nUOw>9_I*@;=?{LvDe6mMC(M5*^HKhkf@_Z2;Hv<6XqE;2p$L_andO z{+6avEO$z@p&EPN+m>hJe2y$;yB@v`^DeC~kOnyz`F^M?X)#M&U~USpX|{1x`KQ%95`)DzaP0E`(~wPFY3d$P?RwkKmuV}=G` z?yC1PciK_(+a0Ctg7ia+g8!oG1JEu&-#+A@Rjbt5j2DbLt6X3ihbi~(Vtps_UIw*3 zB$Ix-XjsI(xEbNP;RO3}{^y1B$(}e6E2odd$~nYpFThT_cm{pwQ7)gulI5%^+|T1@ zkA0HUhd;^j{g4YgjSb}aP3QOZo}GK;I)8UepM#Rx=R5KHHNeA|&GUDkufDeZU)R#} z8`!7Mq`!svpw_>{*rR_S7851&hCP-Im^aXKobQqRBiC}w&>i5Nxjn`gqa1Gdb3ept zUz>N$-@-NZejx{@_P;1S>a3AtYXgzHa^YkOP{u2ybb$L zrHXj;;T%$QwV&?t3sLa-q5mDb-yPd$Oh60j$|#Jx-v58GPp&;OfVKb&<$-s94A(tA zfL%Af4cQm?`XN)QOY%hVk&mCy*!DiO?M=R}we3yZ&&XtDU*rF2C0&-n(rnOb@yT3R zDpal}_a4AGa{fMj`XrHW-^j~r7vTr&mIeLRN#|;tWa}d2`Z#r(OZM5BFgytL%Jv0; z--(?6>Qy?xmjB)O^N$Y5Wt=oNE zUN!q~aCr&3BLwW@9^BVdn*er#+lOYHuV=H3Qm$BM@n88$&$oJ?&t~kGUe{rtIYRzxo=P?J?Ps38wf(;@R$JyxJ)-}w z_kr_yJ^v+tqTl3BSu`X-9v|8#Pmk`Erza1|i>nvp?XxEm6BVg7WZr^B;5ZrlM)Vb< zy^lI)bPDH@Kpjl!*Z`0PS+fOi%HKV#m?0=Aba`)e=Bq_F48J=x!S3$oJk4;^o8ePjDu_K!jDA3?0IeXk*i z^M9Qz8|LKN6md4?!N(uZ9m2c`U`>b`T>juXaWDHV&ky?fjJD+?@m$9 zgQ54a9Tg1jQH(8M_&58#a=;!l&*;Q9!dlAc_QjFI`yaKvly!asAH%=Z?;aU|@8GL* zd{928ku=D+?lk>~)akQI5~j>1jheTV+jlYHlKU?vTArUdp}N0Etz{Ui;yC)tm*3kb3;q`R_jPPN8FDJh^IrKZw4?eo*(Nv7 z+?S6ppGzcke9XrWum?YRYQ>8`{*X#lYs%M&Q%h1`=8U7xVacLn_@!{OpF!>Z>ahD; zw;C>G$~HhuI=*zNok!jU)9wcs(D%wexM#bbwe^h+KnCJ+&;8afw#-vT&`)MLwS)ft zWtj+nU%AKlyIXI?N&xuZ>F{skeV^<7BZ&2N_;pwW$Lz|Tr%JrdhtH=x_~heR46`PO z!G6RGjMG8;|DvP+m3yP_t-nw9M^f*vx+x(XqdOQe4LAE(k964g><@Cu279ix1Gug0 zd=GqAjpfzO7E%{|oc@76dx?^;t;LbQ&Ug4*_QAjDyPH^)B|~@Ndt0g*YU^! zhkxD1dA#R1|L6QKpOIts&mDSS82@DMKm7(zyP@;!v<=oTy@NhkEwmlRj-ybnObg8o z(zX_yyJgX9=~{P}Y*=(g;QIaTUrY?cxhTYHh>=?Fbqis79Ixt3UV8{XWf4%iyZDQr# z9P?~DHnY`_r*@1~_9r>)`-6S|TQL%F8}kBy{hgR6oAy8beyj7D*F&41dcRBGksh0W z^PzJd_Eq;&|D)`7{D0d29M^v~WPsP^cl5rSeX<<{U5`TU4f~G254NLlJPP|M3(OMh z;N9&9SUDF9S$TbRBgvBq`TPHkuVd|c^*i-C3+*txdo9rUx@~fMIeve-Yz3v;yra^8 z%WLVg?zuD`5h&S8VGifCxsf}QM~Zw`N^;~zf3Um-rG3MOvSs=T>DOSTjOvP*KXSh4 zOa9tz5#r3Ior%6Xb4+@5or`)--Dj9nU()hNedH9@-7tUD*#Ml2b(1USJ#NqQaO?cv zKGbirPn}BkX{U0V{>OgwH#dA6?s<>>lqj2wpSpzXkR1!G4z&wzVX`G@;;#|5k$;M`k_Mn6WrB<3aX=etC~|7hF*|D*4X z?_Eo*^y)GX{rQ=9jqilQwt;z5j_Z5lwMKV8Ed$yhu7B^%-*W%Q%a<}?(qu`MA-fbt z->r6&_et8EC5^9XbwBEwr(aJ>#+OWAvOE||sCm=D(z3-6$%3&3l6>=}Y#Ub)2=4l@WUm;%BF}k5GT#+WY~q`L*ENY;(#k@Xz)? z@^86k8{bi|-?7h7&drf1b&6c^wj4R1GUCa{Gqm@~JoW#<=dkmX|5(-kv&lc~eYOLd zwm|s%i1k^2|3eVu8kmm>Y9KMncLO|I13UwlmVYkk2bdUOB#%3KKL*bfgEl|w|Hs1L z-;G?p^2LW92;ZG#Kln zUcU}Ke&Y(|0px(IaQotEa0-5RgVjT!F>>?TC%KI}bsKf;4$5tZecI!*huo7n!|tn` z(EbW&M>u{UN^V}qxZKb^_a9)aK8$~RcsIrY0K0M6=R4s2I!mrww)q_Xi{D|6P0T-t zm;&XH7F@$^FW7fU-M?nhb!~@b|7-eLWq)X@?SVYDa@rH=Tq^)$A6=D?A3n;ZOII*A z$Vs*Rp8Wp1Y~Qs@>eO#2Nia57!75#)^W4kQdGm*g zzsugzw)IGD^Z(DkzmP%Aiy*c~_QAR8c(9N9ZumF6tB$1pbJ*rF?{UKl*^hSO=8T=6 z_OSl~x*ps+h1_q4-q!+I&GtXb{}JeXvQL}8ea}HUZcj>|tnoG0K(PNq5j$9M(Z>Jn67BmM=ZDR|WuANpf?Nap(O_ST!+lh6eTRK=Pg&r!`#I0T zcR~KizP1ZEvA1tyo>#oB0uiK6+)^*dLm-8hSF4sUN?Rz3XRpwqm zeD_$ij12oAy;fb5lsSqcj*|vsEn=P}%(ajTC0+V#;@4vs`k1eifvv(}XGTh|&hw%7 z%V8Y8vN{eY`}uIpSj)UFS$AD>z}lo_U0LLk#h%0SSx~>(-gXRe^-~xl`siLW_KfA1 z%NYZ%$^4PXId-mLx#yB`u80t0e=|16Yq^iBjqFdq9o&J*Js3x9-}V@^i^s^3FvtY> z?dK5B<-F6^C=0;-t?MG^FgF$dmt%rZcHG9e^=|gb@{#Zu3E7A_o_b%GwX-nxD41jn zkh=a3bj|~`H$6cAGO~XkW4fO@%xjuww)e>AS|5|9*^EqNMN9qg2_uk53$RElM z&Xu`;7oG*jCqk3YIa60N)q|0Xgc{lfIdE%kITKaeLd~q$^qGk%f3ffI{MPG&-Q)V z`~l#dc7DL^STN7Rb;tmG0~XGiy$AZA?fu78@4MO07H{po6KM`U{%}5mzCQFlxwrbC zHUMQp82Dd#iEaOE|6_r_Pxfi^$6=rR+hXMaxi{?V5}Z?x8Cho7H?j@#&0#+h`2yYR z=0Sg5`1%}gk6iP8E8pH&s~uCt+WkD9E`2V^o41ryZ#@LI%6pl25#x-wil6@#NuD8} z&ik$NC@cF}B?Z1;hi0v0$Gl+aTzv(YyD6KOK9UxVMxak!V~oE&069Ll4d<3cGR$>w zZHsQZj$7_6`?~Gad)C>5;P)Weh5q-{XR?iJEFJ$yreMBLZJ+0FIsd0iuwwFazzWyh z$DDO?WaIKi7T`DhodSl~-cR42`uzmBy@>C6Y%ly2<_6%r6Nnq!Ci{-gr{2E~d*LAD z$M)qPFfY+n^utHrT>SpQ;AqGWa8Kqv1>2~5XCY(s_y6V|^}cx@xaKk3!#Ui;l?#Y5 zK;PU$U4DSvl?Rvy?mp@<{f$d!W8@I+2$d1$x%p0w&4hX5HvEb5*2F@vf7-yy(xX8D zWb19|*Ut~K9&_sH{2JM`K7E6EA&}+pL>AC!=bcYPc^?hBF)35c; z>V4Klwkex&q>Znc4*iH~!RH?#O`8lr-d`5Ulro{5Tu@sW=ZjMIjV({^S&U8Z=sfUX zvUC%-4 z@25+hFW$QSC(=Yb`EZ_$yV*B(0QEm)0r^L6kT?Dp1DpRPVtvT%*$llO2)!Q&*0tcC z16_r(K-qUKt$e^U#xVYY5{qYMzJDy_g#i!UdCFJJM0^O3%Wife0--c z*Mj3Dq|fjz^mG|1U8S`QS$|p&_h&qY&G3i?`iA8pbk;*>{W0nw%N?A1B;4d6@LlkklzluC zWh|eKZ6)jHypT??_17;rEk}-=M7~aONu4eS+U|2=e$AX1b0?c50{e}JZVpJNulB#gzR^X-=l?cONomn+u(UzEKW8q?;T``AS>^X5 z@>S5z@7Vlo*QQVG_2owF9i)x6fzUXME6+gIx06#3IIjXMF?2N|R$ePG@W`AIMx^ z7RX$d6b}0;kIi;^_*`szuTZzE4B7DiNV^I+uZpg{uf8fEC=Jq$gc8yqAWBJhh=77E z3P?+Xhzimvpwh5G?9$!cg3{gHTX4STIWu$rcbB!^Z-2kpxp(f|x%YqPJg4T&%x;+g ztYcb-rU?fi`;o>YO@vIT(|)id!um{%)qW{s4%OMx1N-0f>?K$C?UFqUH%R+N)3BEA z23dh|W%`OqU#gcP_poP=Noy`Syn8w8;5zT23{J3K{C03oT3(K|y1xBE@Q!x>>iMocgMIq*Vi5NOJswy)4f;9(+kXqNO1~#x`Xsi!1l$wOdAJN~&Z&$e?oA@D)$fJe8xK?5{~7klA57X4PCviV`6GaPrf}$E#J|x24ECKa zK;1tAeXs~=#mQ)C{YB5Mi4*5cm%q^4LG!j9Aollge9!s$J^TavY6IAuHo!{s0rCIM za_j>Mo!?*|_RL~~{b<_!OJL_~!ZFY7d#law;U2%!_j?uh!2V;zL+qGXSF%87dM?R} zCWb!^G0*SM@67~Wn;y1368j`ew)|2Eu^rW0bd^Dij;f767CtRvJ&>^90I?5UVCcGw zQmoDgk~n21tkIHQ_hP73yMY`za#S8gMahF3w;=ELN}C$fWK{3pHP%LH3-smuQ4S}X zgVq#Nj{d+a9Ny#9!*xIQcf16&~WH5{g>R5 zip_h0{;aivYM03_*B4@p&y#OH-Phut z*r(i6xzA(T!&=|f4d92efA>P@{5GaHAomRqV0@3^2gqRqkRL=qH=y0G_&*UXE!y?k zktSp5jIWk%5)`_5Z~D!b9`4<8!;J4c9zdKkZB*G09>8<*2l)S1!FCw6k=Qpn|AWP_ zWq^I7^E=!(#5vMZoX1J{O%DJ1yWMYut^n){upcp~D#l5${yyeVGk)KEU;CY@ZvpRa zot`Y{$HG{DwN9J9^8Lzl+K0sWxC2;Q?0~~RbF4LrbLs=M|9^xc^WNZ@lDkAD`1-QR z+I1%GR|y6Gq9S8u($MwN7GwUi$H3PP8u!z09L!nF#K)X>&U^jI;?D60XGaj*ew?b0 zde|iy)AzXNBloxSm%P9|gKavj$7`R@=QZ~pE+^*Ay8~PB3#hLU<9=>EAFeMx344oh z?2Pqx!Iwwe)6S=!Ps|$~0I|Zrk&f{JkCf5m14sYBo@3D6jVwo9E87kB1NXBHZOifn z%6udDjowdv!|t>JEu(8|X3m-yb8qrsUzbdh zuUu^zvgU%)gwh6RXS|gg-Dm8Q0U%Q8-V`ouSJPV6&xvfuFm@&(`@ zJYe*WyHe=Q2J%*w>T>heEeCrqzkL3#^!{w2bZ)r|S8Z$@`*9fOn7I4X z>$soSwT{J@udb8FyiQ+W#Iy&9LH`45T*Kz~*TZrAbN5V^d+oSmxp~X`3CzOJ9vb3uxTUU_X78{?+@;ch0YG# zUxM6M%v!`{)>7rk7p`3f+`2_C5O_@}9WgqB(tk zM*jb7vA-;2tlW=)e{vCc*&^T{iFpiofhJo<-Vg)bfH;rzd;|Dh)<>ZJL42FohpE|yg{YL&1`wsu; zSIGX}!FwbnY`#H*zxQY){=tDA7o>gjP#N5DH^!Qc?&{$I`1rHq__*|+^B(7ecNlZB z-`}3%61-+yzmpo{qq>KEW}Nmw^X{CvAKPqC&)t2tuD_ngi*U`nn-BXe<2^_kKje;W z&fl>aKEcz_F%|!i{};|bGXA(V*u!(a(aVX^EvsWN=0|@WZFZmBSKQNI5a1iHX1iId$}A{H~NEM~_K?Ld7IQ-eS_A%Lo~; z=&(!z9gr3#0QVEX1Dxzv?1QF$0!@9h=!jH=ABk)4>X?3d%6iBj<|>P_sBOyqI9tBk z3;zDTx|UaB$gf|zy$Sy{_L#6fZPn+U-m7?bHb3P*@Zt0UhkYmWRqi9U$0zr7EYHKg z#XN8yjxy>5#6R_Y+WN%)B4EE&hrYTWUxu_V2Ze1>oqwN``9|)epL{w1`G7k&*F}Eb z!FhZ~WEtfC17JTo0@x+zfT`<*pKp$1Ne?(;rokP+Cthlcop+@8OzJ@y}G{qpqZmo8aL%9Lv$)thvbcRG)gj$>EIcZ-k9IE?L0M8DB?8;3p}r+R?l+&lU1_nzr{hF6An zP`d#+V;S5r=l-(JTG-vc!B>BlHb1bC;~<{LHdom8SBGMKiLZ}J$EHg#7ZJ)s0)L##-S(qTC3ndx zG6HMNbKHlxci5i@?2~SY`w5U66AmKboY%iwdQz&t+g)9tKCl-Z?I2YnD}=(zWU01b%}lY z`Kj}BT#vdweSA*mXC8jiV&D4s79(GbIE%&b^|fr@mvMb5vS%q7G%o?{$H#db>{}gx zIzO?$Bm{o`aM&}zK4pF&LEm2l?Av6H$s6#yTv`m>2PuY$JOJP2{-SrJWtBYglw#kk zmF#SOj`gK~DJSA=%1dc{+cKCl&3%$emE?X1HKk5n_rq}&&ptK*eT!yc|s(hXH zz3Ot*2UPa6Uqza9A1fKN=aD`8_9s9ofg;-evnK46PwUM?9P4>+ZjI{xplc7az*K@5 zv*oUyC-Z?DV#ndxU02KzpB87!gJYm$-&I^|9o*xv?cvk5<=t!X&2v|mJV@WqU1M&q zyYWZw9c*3y^WwxZ#d{w8Zk*?{Yhx_N`Z@kad5q^lCR66Gfs8)(N32{@J3laPXK-(E zu2{G7p4eADU}Qe+1LYUE|2*uA9qXdCUGf(4)}Xh<2(EV`-_16cfahr>AC-8N|P^t)u8#?_B~nvqGZD0jnb~+VjVYD z9H~4rc=BT=fTt&~v7VFr4!3_M_Z{}NJ_K9^Q>itgkGgqI0xz2m& zBRmFue_aIj2S(d#kY-}v(65@V#6G{f<-xs}zwF7~d-h7EocX2ops61AClcqB^}v3d zL_Gj>L)`y_I+OQB%hwZENGYtkLH&8bwAf}zS?y@L_GS7QlnH> zxjrAheqtYZk99KKVINrM*mwZ*&JG5SP;PLK>(mEm?=M8`>5SjxN9@6^IzO z#A%m|o*!ZT`-`FPQ|>$LKOK}hb>4h$2F>5P&-nR$dc9)YVx41r^!KT~k9@Na`(3AF zjqVW4*r7h;I*1khIQa~(LK^or+Tw4^7XHqWc=P}<>j>d*{5y3nhR=sKim+YxbP-n0R+N@#*j# z?;a0}KCF6e@_v7Pd#+_H|C4q-JB4-ZJ+wL8d;{-(drjX#pYO-M`7M8Sc2X<{F?qToadBNB_tm`{#WlaAq7{`BTyUO_X zUBCJBS+4rH+=+p1yO^oA} zhFl>X7AR0w^-#B_ALGMn)%-x(ygxu1HU1KF`qD}10_o)()|^y%uXv}tx3bO~8)KQl zyyD#+YnjvkE%tY+&i|VX1NIf$PS&fur+@!Zz{YnnpSHfmek-H%C(oF^0CaxEzR%t_ zzI-S98SAIKz{-Dz{b{?Y^Bd}36GbwM)iTU{4!?ZNkXQkXH)-`$fx8=$gpaYN(0RN@1u1X24pI4?rQ)$z0 znvC9hTk%hwzuU}hk^;ViapNcY$>T4NPanS_eLhmkUAX8!3}9pOG?vn5U1QInO8Vjgi3-$K;bibEM`w-K6Xr z&7~yhj(ane20a;DvrYj2(EmB+O!-u;S}WN517P=e1>I(sd|6Y4(+M~(xMM!z`mi?#*Xq=9d&+y*_|CUSAAtJy6!%8{JGrm+{+E619Nwao z`!8XPZ-2nXH`tGZcOUK*`_Kud@4XG|ACrZ_lVI-~UtctIe53C>y+6PMV%6?9@?U-a zjvuJZ2Y*of`?1ftyXn=A%EbK+iC?UT!8I|M%=dS4-{mZ?tGt1J0Qv%%N^u`l?2%Xk ze4^3kKgh7Pm$Z+E!=TAsqKedO&{*z2c;Lw6Z}Ve^u1fDu7E70RR}0sCr96y-;{<3w zLF~t?7r%~&ch+;s;>lr<#~p6S}v zZ@_D=o_&V-U5_>H*ZJvHmuLKr=i4XlE%u$>Z^!dZU5@D^26!drQo(<~y$rgwTrY## zua>jNZvAbaxeCN16Ja7kdd5+2a;9u0&7ehNg+$0`r!nu|!PcXK>C+q## zcfTL}@W#9*(D^f~&Yv86qosu(o<99DrGaaHbHy|zH+9dzcHO-R`{_Y{STOmn#_RU~e3hK| z9fbQY{zi=%FKKhUDt*KD%OvzavCnb5QSb?U3OQe?<|k5;@W`M@Gzae0wshs@ zT1n`a`Dy=Szn)5!ny9`1Uh6)HAuc58QYMuRBWfjpeJ2;e3rPQp2XpLfXDzd1DIOHQ>rncwS0V%0 zSIk>IAGZExFIirI>x==MbMPK~r6(jLc&6M7$NYVZ{TSjs9P%H@99y2?_(6iX{r&iD zx8dt+@VfE!ac_h?SOblCC$3FDfCr-=%sG!?-|M->KKjkG2RIghD^gKv)@vY~%l&Wu4(+)tJ&6Bz*Xg=2l>Z+615*yeUK!}$$!muv z=FUInaN_RMYrM{O{cYpe-Twsk6Q~#GUO(33x9h*=Keo?{kNv-E*ZmIue*SL8`5edG zp!4+A6XOZVd}r^geUCcO_lbMP6>nUGHEJf^#yX~(WKf5-a{OQ7{>IH)QuXb+QnXqN z8N2ho;+{JGfUw`B7UXpC()FO*TU^toq%3i^C1nqBPkPeCW5*Xb)>KpSVGd8_$}M2; z50v*oUtGU0)r(1Fvz)vHA`^sGe8H_|jo6 zWO3|=<@9@ldH21Uo8!3HXCImVQ<+b>k3`(Fe`x=gg8p9|dR&t)hspRI_vMq}Ve%q; zwrxNDRIXfwvh#O;j~`1+Of+g?9gKt5r2l8jq*K%7f!JRkZgiZ>ol}=4{(ab`%y!r( zM&jVbVpGd_%)yGqj;rgQ<=&a){$sBm%iaC9 zUH>&6b3O0_SnthX->>hJ51`!R1(fwh-gEvSeS4Jmq1e~A+q>)KTj=>G4}%Ep-=BN- zTn&>?_~~a!jaXjx_axZ*9K-KAd7Zq0`1=yYYe3c;T5;G``!NpoE&fSUihDT{ZGrT!Y9rA0`Rlh)>Zxr{$zT%%g0Qdl*Bed;1O~wNM4Z9APWa+bF{g$S( zYxkZw>o@u-X#4h^(zowG`Si0d<(qE?%YLl=cjm+``L6Ry`MmBz%(=(DudqiQ-J3CP z;LhTn9|8?)!Va?{QeRCbYK$OQVv(I2T@8L7;y-e70365dD&i1v@ zm`fEaGZ3TSso7c?)_s#)ICV$61tlP^m zK4AF~QY_^jXx-xhq2$igccKT&C>2Bb4cjFs^5k z!@EC+yoV031ocd8Pn5jZq0e%}_9RW0DmT{Y-3{zV`mi7H^*LWZWBBR&Q$2t-0kFU1 zbhPviJtO01WB<|x*niZ@`m6YYlg{)9wz=?Z2*gTj|cd$Kn$s$4*5!~5x4;RMlg;i zK2DunPN>{n^RP?IF?r81d?^0Cf$RRdagTYA8`tA}9K}4!{tWwUld%ilyLj&qXm=gP z`nWcSlk3DfaqqCtIeVu;&x{k?yc{vqp)pvy>kRa&)!4ISr(C^oUk?0+IjloRNc)am zy9lCa55jb zRywoTk56-DO2h6%-SV$Dmgdd6W6fTJ{oHv=NQxJd$kM^p6#EYEiha<0EED+vbbMkx z(vFFHJi)uh>#&C--j$hiW=mJt+SB&kH<*U3=lpyVyUW~-@tJbsUK8Rimq6$5GiJs& z!2W+yCeIA)?*aCsp1{8I{SoITK^LH1V6ngAJmxtrI48qrt(SB2KQ%Ze?jh%+kSB!w zD76Kw{jd4}@o(;NV+7=j=VmsLN`+s-p4*AxKgq3oVW_P|Umuh9m$CJU`G7BgePf#y#{6sDFSM^gZj(j^p4M_quxS zc{+w+WAl_x@tsl6+5Z0fdHh$n*EXT|bB!SS{Lbrq9?Z?BtcSi&yPkYuE9L<(R)6{I zXc^x959xq-y;)=S$h})pvTXTEDN(Yb#?_@mj3?vkDp#!`d!c_lh>VmdU_CM_Qm$RU zF6%dLl26;UmzT2TmHe;QlRlyQG1UG11r; zam6X=+9Cq;5?08xA1?vJMrN)??5SeQum4-TD4uY<@K^Yt`k&G@gFO8E>ht~p_5;Tb zvz{LC7ufgkisyO93GfMbjW~D7=@iNn;@@w7i+dO52Jbev=j##w%XoZ_*)bi#xc}a5 zF<6%mc0J<%=obi^d|Uc{yi2;aS^*!&QHg$t^@7iyl{emc8}anmr#MqCU3Z6lkUm3p zDO{|i)T~oq8aHbp^%^#TK8Ly4zPUw@yK-bmF8e3cmB)x( zd*raJxCgG0Yb?CxM%NGEo#%lv@`VcvKa^oJR!fgLrzPZYw81{{?|6V>-^hE7;it^E z*OwuVV%eEkV1HkmV+!7(7DAJ<-x zY=z3}S|42B)8Ib0;-0ac-M{Wb=zD*sj$DUrx<)>Edw~q?^qVZ1`OvHZV6p4RhdU;o zi1+Yc?#rm|M`h{{7iDEIu>hU{{43^}lNYcZJvOpm&l6}nKA&*Ua=F7d&xup7oacD1 zODgkyHb39#3HHA+B!8!h1KalGaW8H+o=dg#yIvLV! zv;4m23f9v%-n`kcxiW&+^7|Xz3%nP)7x&C~A#GN9ty(iFTD3Xy7V_r1o#g#N)8yNw z$1$e^>wUwwH+((z@IkCjN!WkHyYVF%>;~k&#kSHFv5xDSpx3M2AEt0QdA-jiPN6Xq+@G0!QkHs{5Bo+3@L`|4 zTXD`lvpSx}Xt3Oj53=7G!%-cvSi?44v-5uF2gV#A#CK}i7b9=A>?%pHzbEBR#!OiM z0kS`X>i@ZgZsr-O3ys*BMzUv|v6iIDL*bgfNdpSaHqe{jmw znJ_o68v5>vjEBxPes7e<-*eoaYw>Vi_rwF3!`6Kw>;)&={kg@ncg@KZ$`u@g&RkMH zVCkq#>AF(otyVgBkLz={c^_+jVtlWDy)U2-zKV6G6U*SX#o@z)pHFpr&^%>7=v$Ne zyDa7%-jxUNy2CzmbKk!CUqjaJk#84WP`~~X+VEb2KaV~=YyXoMI6HqS>?_6o>FCFk zmu+u@<(O6O&se!B#m4h@x5dZ4o&)2w^|v@(-|+;NBWY|O;(O-Jog(+O+H>kE9#&O686#KScwZF*|VE>n?EA4)q4>~dP{LMOl=9qNUY0?_W zknL4ThP8uf_cMN*sruV>Nm2_SoAm3xCEsvY>mF=N2yx z{|+<6!_ryU8>#y->DPLPOz3q&7Qr6SczR+7_bIlm9^m+ZYsZg!SLSlXvVE?@x3*_7 znIQIYEe?(yo|*HSyBF=Up8FjhkMPxV?Qos(^+;Toa6`L4r8*}d_+Bs3Q< zV(}8mj(Ip-XV>{#$P1DtPbbws>TT!YnRp#<4j%P3$oxLDc1r0A&C&O?_58TEbVAv# zot_RwLil?oP8|j2dU2g8sblry_i9s#r)j{<+91C>5$>j z_syI&gJBo@N*t*C2hM5xx^u%DhDo>kZ?}bGE+2zUunGxhwZ{f0WHT z@<6&`jDO0Ud5Fhrso0MO_Enx!zxQN5bphk=e`w1iQIE;!2o0b&aBRTX1d*(x3Hm?v z2^{l#G45ymu(##&+W91F+GH39d`^-kOCvcDKUEw)+_G3-$uIYnXDimxXCB5~9~1j7 zu?*OUKcQNq_A+8UjbGScKa?CYKndT*#6C9Ed?6u5| zJ>R(*iS>5>knXJ($(Ie5%IKb$0}0tqEK>(|I=HslyKIp&6)rH1ilL(e&Mx;H!IfA{I@ z?c5w2lMl=@R+skLc&yJAOpN#7mh`O>qGG%ZuS8~fhy09-rwNb6AM6Z?yg-;3!n zeDb@iPToIyG4>6S4QkGWfIC z0~=~TTl^|NuozF zcKHK#eQfOCkJpd?X_7quVruNqnnsdhU8ffy^Ixf04|BAz76);zm?!ShUq~F!>oQ@L zlq%QIjPX(ycquL~FfyL<1K9U4Z}CUv+)dVcq2h`wx1Km{N1@*29YZ%5S~#)TWE~XL)k| zZ}9K1Pg`K^l}9pY@kQx7eXr~b?k>^u!RLTk>itIlH`piEqhSxIPOt#K4Jlfa;REKn zsS|~IOssGCgVhJf1Dr0PaY66Mt+3`852!2+%V(7ohzH=@zf@_m>Uv4^;d8$P+5+rr z?N8A0PMbJSGNvW9`GbB7?U{$Ht2=7i}HZyao@LX~y=Pf1em4W;kX?J$d<@7@79{ zCHemIgVMM4Zt34hx>TP?jDC(@0>Vy z_;<&A9_~*N`{X4^s>5JSt(haQ$-qzdNc)D%sqmgK(FlldaV zTqMUFFUEUwueKDaGfJJ$2g+pH^cMHb^%!NN;F#hZZWFf`z_h_Xu+Oo1_=_0B=cfrR z$2Ha~Te_aSU9GLW+oGqmZ8K1sH14G1!>Q6_2j&yWOThelm2=DTZ>!7ouoj5B$9@&G z@2z}ycvt_Ihk0Pchkb{6liO<+|FIbRIvf6({4i^i^g>L{g5xo=oHo2--r#;2LXm&)cB$E3W@3_Tls6xZhaJf#^E@ zFxHYBA@}DK*9Olql=;Lja))Q94;UXG<++jV2J7ZNSJ#jINVG%yfc5#kVv!!m{D$iGAKR)FF4pSDJ#L!a20nCK1Jfh1{8TjFT=?5M#`19Xn>X1v&|1q9OEV_jH z4!4>Ui?+=;<*ets=I-~`c>>?=8S$`BenR=q#P?k?GfJimJ}-UR?tss8g$(Pl5qklh zmb)0Q5B&W-?pc@f@Y24N32W?{JzLTszM{i~#WDppy{@?hJ>OxUKAH|ABXm6{jqy-< zZ|por2Oi#uf7tW!efe)xY^rPVd;pt)^Kwf33px=Oxi}Y;9$Vk@#eE|A1$Gf#|L)ksAH)wL73|@8# zcKjnB_Q?Z`eQ#tw@o&nA{pCLFhabLirPJV1Mf;7LuGq)t3|e{Y(okZ58*oqg|AcLC z^?lmCOvSNxL)C?DXtP5OI`eq+9gmF>j0 z%6r2vjBQ|X4?Q7@x`N7ojs;MUGP)D>sfQR7IDz$$hksT|Uc&!L*C6}h^Kag9gs$~5j+kenOaU!%oaoJ3i2nxmb7L)$j9IeFj-5LL zB>X=dQ4ctEX=&i@aeO4h7f2YWvQu%7`sUKYvmN*UncsLsdL^)?jy zz(4wG%+^~{vjxZWjE}dJ%5m4{rmu;8GloZBKCW4R9_{)XZ?}bh-y7?1_K>pW>Os$d zoX46nnNlZ}w$<{=GVHZ`BMkG{7D2y{a9B5ZR~;X|D$=OYR~`Oo69+KOYmSZsxOY4N zdNlmw4`DZiV|}bnn8Pz3PWI*00LHowvDRnPZu&pJr7^&$5$94`@vuOI0+a9HNA z28~ec!wd*oef)CgZ8xxYE3xm#w-58q$LFp)AK>CskCj{?_PPE|rVYJhe1qTv z^o?^LUgDpLc=L33hezVs9ut>(j65OP?&|3pK4?p^kNmId68p-J&?d1?9fkh-V9d7} z+vlY8Zo6IA%Ko|G2O^J^DdwV{?kK!xjdV9%|r`B=3l&LdI$r|s<*xmOu zCR4GGeMKiCF@CSdFB_qwHBvj@`1YaSTRWaS-r4m=2jDg8_-4GmQN#Aq0(_%l#U@xQ zE)V2x*0Ihr)|XylaosUY)I_FH+Va#1mF`2lh8 zr>Oz<^n6do%vuB)i@9^O^?_-l&(oJ@^?bxdYZ3~O*xzw2Dkx&_v6fq}#XR1IvAk{n1p7`mK)xRFecvs;AYV^EBwIqhkx1zMwB?<> zZ}5-bXtD1wuIKm`dJaDUeFZ!=`u~&1@#{Gsf#C^=2f#DUxB+beti`dQsjL}NU7A5=puz29fD6f0g0 za}6pB_w#Mm{2h7tFz#5ik6i!J;bRO2-o1BEZr{EoQHV*60!~i+aYGg&e&X8>>#!c( za`_5;VBjZvWYRZhut)W6i~(St3*z_43!HvUJjRjpz^KDMbLA0^C-9m*R#|TEO$hrc z=k0YqV=cxOxF+^O=o?dqT$CZ54`D5hjq+8q6*A!SwK8MWUfH|(qTIRpzsvZ9K3tb> zJrFaKT*p{bK$DUvX2*4@?>pR6)_Xc1ct2zT<2yM{ zRITcJ(y(C%srW`S#B1k+>_{T%QzVx6s=%HfR7I`_Q;r*)Q*O~09jSB-{6cSKnTL1P zW2wuM&P{T43@=PhUQ5ed+;v zoG|uR0{c@p9$b|=O`2p`a^+R*uRnDyX#S2vRkvP^eyG?7o}KPb8SmHgoy>Q3f%6ak zvh${No_<1R&z>na;p>Q2{99amzuzajS*BQb-z0#2^NomobHB$!P(S8R62Gk*Gb9ht zZ%2CoV*;13R@&rF<)uviwDRn;&q?B>sU%0PLeT564v^RXet$3M0(4LuKEFnveTVg% zqCDIa|CA>ku~%;i*nOo+z~2X7fBKA>5of+UAyWNEWf!r*-0+d3r8)KiuGgTceE9L_ z*h4Bz9z1-Y_us=jfnx`+%aS=qW%$>dv0l(pY2SD`u(ko~(eIUUeNVuLc12@jmd=Wl zRiTKL12+9~nX=hSz%J#qmvD_dfjWfp5vNZO>u6_XC};~~5epG-IBOJQpAd8W-IoWX z2Xy(4O;#fAcDek}Z3AqU-=Nc9kQ-MXAQn&L(&bCCe8ozcH9J_AA-{MD8$tg+{?42` zE5%Bd!JaLtF~*9$F%UD`ZuA0~a+sL634KIfFa+^@+YbRoqg~0d+(e)0jXkz z#!{!w$5N$AE6JL@s66xB3zFu=#M11I9I^=W@UDk8gXDQFoS#eS z7PwXYlyr`|4)^XF?Oa}utN)@NW88e$XFcls%Yw(KzCU8^HCd|iop=xU_|(6rHUP^5 z*jFE)mHR6&M{3IIJ<|aCr=LxfNUvq{Fr=`ew1kJA8=?&ed*k!kYq}e6#be6aiH0y(5n@#-f#L}^?s!VGuB@b z^SXarbx!HZ$$i?9{X+Li>GBPwWbx`!1o4IRJ5{b)UCtoh{NMgv#d-m4KKK}W#4%2g za|5tHY?i#3`&~o^V2*J7wZx(xVvYMVcQ7C3oXr2_pp5RbS^9jq536IDGj34|u^dc~O75exMohLAV@*U+9jH6?khx3q$w`4A^6Wgr+)1Wgl zs@F03zTo z!akMxC0E{8rGCR^vUU6RfBT8N`ri#5ia4Isk|Iq;tl^6N*7FpT{__uLye@e%@y~vm zg5xiShGC5c%5x**16U^?pv}*6%KExkZ{r=QUack8-YKhbxXGSREN_?0Dsy_hC6^&P zj4clw0+$Z!q%)U@X&wjU56=yKI~rGc&oc537vtt|PWg^9`o$dfsXGw+R~EdFxw6pr zXP%N7``y?c@a^n+_3y`8%p0Gd!N2kEJNz#{7b_E&Z5x1(8T9mj|Et*lW!Z+HDNEL7 z+n3$8=X z9zS$Lwqgw;+6ce=xJyR%+9HF#To2#j8u-~(NoT~)bZXAD3iIGsLN~VwWgX#rXX^A0 z>b<*Cy0u=3y=~TDExh$IqURR*W!N69dw&@1pOd4%-H>Y+AIQV|F)G^=dbypuc1!89 zl{D_2JdKHSo-$<0DGeJplN&ccH2?CCF@4!`<<&9$)akS8+U@zv)R5sDuc$8OuuuF? z*mWQ4==W0HjeV)OSD&8K?+ib99b^8LDmBCY&6O};JR|(WiKJxSG&1J%GIAPz<4CMS z$=EW3Ysz}cd?V8w-5G3CM>WaFe?zYh_wlcJwm;fa8Yi84$Hc$k1LOt!!n#YJ>AR)x z!t+>*4|5wS-xcpx#;d+hzrWK1%yrfy?iKqW$>nDv9}S%u(H3F9ihKU+_Wg*Uulf#3 zvi8)C6;9qeUEbl_;ojK*u3U3;Bj@+c+=sP0XC9YdX3xV~yR8-XQN+JbKC8~o-^emx z+sXD=#ku1F^-x|9--6fu$=*lYqYX_Szli&-&3E+k0FI$FlE>hqk6;HJL%dbb7KJ5S zdZPnyeV`(RD|xcOtn*(}*YZCjhi+}Y;5dbQg;;ghg-d7tyHaEQ&ObPpt4@~hXwA5eeUzX{f9br=^_7S zjS4=L`ZoP)_ijha^~(?C;_16`627*>d#}mComb?*w#%|_%O$nPX_N2Yc1aHIx*|vR zU)Or)Pu`Jhmma`YkCMm-vGU07XZe>u$X=x1qIcf`;AOcK^I3p}0#4 zHy`c!uBohHeTP}lr|PekPF056_FTK_4@4X8UL&U*?kTq#$lZlaB@FTMou(d7_%bq^ZW6FEv3F02t7~{j({uSqAWWa*6GH}LD*&otfqQdYk=0SE-h9mLV=mEsA zE5kPee~1O1Sr&ylk<3}&CLTv>IljB)BOcFXU0$=-AF?j^h4NclmwdqZUXWAQ!Z+nS zqB~d%oOaxsMKXZjWADEg(#p%&Z-rw4N+Xo}(CKTp?kQs-M^x_rhOvI6aqt1u|8M|& zeGdB$|F0?jv*pYq9XoZw{P7KP`O0PW19AlH?-gLX z=GJ|l6#LvC8YwgOi00Y=ojP~_*Vz9%OkKZmU5*|*YStWhaPRM`|EudU*!SwAPufAo z=fdycUiTLJuwz)pn4Aomb70+#PIBmX?4k20VIE1$A4AMk-vI-l3*|JpM+|S;^jYDv z#awE{_0;;Lzv5o?G3sPUwDmh+?sW<1Z(N@tur`O$|EcSjELjykpCXb7b3&41U7cnX zun+V1)#Sl^>@kC}x`&_-gL#K_qi<3ck+!IJnz1K?aiuYbbJ81p)syqT!1&n)=|20E1RstxK6~JtSYHmm9(jS`2PSvt$TXp*axnOeZTD=N9Nn>#Q#R5pTHox z&NwRb=KLbJLa|R5hU5qJG~J7#FUH zy%vUI{;nVUoa@t~%NWG<0{d9^)5w12)#cS!$^!psw3BSup=R;%{{Re*?@*moH$G+2JcGRHV4>#mqffQ*(b7 z;@)CE73Nd4>N{Dn?$*^;9&jK=nzVy|A9kFIZS_HSVKr*&`L zzKu0(I>7GEjd6*bYKv12bKRR)3Kf@y;fwzC-hX@Hm#Nb*$0;N9@~rp`*)blMMdw>! zZCDvGcoSj`VrA^m?b5JL7s-{YG*%(8C%%5=p@!D9gcqM%GPVYB*K4pJE{u{Y(&Q<0+xsP>`eqOcn zJM?4FvxyRW*yq8JpXUS(nLI1qnzMKIDfVH%6Zf}bM6O3amgDyy$-Y~$vK{e1jN>)< z5AXx>1kUH*fOY-GU=Cc5nMWmj?j*Sr0v{>xPwYnl_lo^6V4pld&slDCD1PSvW)0?5 z-&eh!{DNg@%O>9U5bdg-uh)zo!0U!ro4VTOJbYu@etbV-OgQd?AK)Qut2IAVlWN7_ z^Lh4J$eT3SbFmo4lxj%#pVzBDN%v_a_CaIqew-&op&t}4Zt;&XL-K))nXr#2)*5PF zDVJ;+T}L`LC?FYAzbJ{5q>&1xTgb?P+Ys}HF|~OZKMO?)L&EWj&?uQa>VWj@94tLL z&X!(XX30w#@=2$z-4W&f&*EMG3ZtvBR%OoI1=WW@{AWg@{Y`nC4(r}DY}_2N1$X|c ziGOsB_I}6Coh4(&0#d%rJCZ-&o6v=-NrO7wWx}w%z&&CV!S6WUxF$SOB0^8dEQ}A0 z9~UZLe9=#`3Mnj*~H2N%zk%fuX|$O;Xi%00$4j@qxJ=3dT8r&eU5MDACyvv z$tL!7-KJ7Cq#$Jdizzb6Gl;z}oIQn%`=X4lr@?Xh=!Kv`Vi4ofv5IL!D;^zz&Qw43 z=!*3NJb?9-wk$tzJ}R{hU?Zpv3SFPJW)z=Ay`M4%$UF76wVf=OaZ~0_xgepxUXi7UL*kyftMOacVvU|HD`Mr)ZtTm4 zbz~1>?BKf}eue%~0X#B=6v&ZI@?}e{W3kEMA4ro9`UA)FDf2aDmdv?}O8>~3SK~3Lz(LOyat@R@t9esWa{2~ zigSZ?@Bl0Od9I0efFJwBx=qg3kND%-xh~(0D$sB2uf+Kif8h8*q1z9AviVxO8{HeXO5!2|naQw`1V{!+r_CKW%;P*GnF-4tsfwT6aadV4u#A+0*1k z$UDG4WHxv}EHL^AiMf;eigCrW;h&NC&VhPHKj8fy-@v`b#y7fwcWld@|G?ALQ0{nx z*2S}oE=OL#XOhnwK1@F?=UiT#-Aul3TSBs=O$r&5L@GA=1am~MtL*0-QI02dnX*n^ z(-L{|${DPV{Ud>G@7kd9VO`uR)*S`h!szD>{kwhem!{T1vj0R-R9sOwzxE zJ>ly0m9anUm8Ekdy?Okr!3&5H;=fly{+?CDd<56r#yY+GMNVP* z(;kd13>!8Feup~PvomdBJt$NA6ZP-U z$69s4qji4V;ANEgl=0N{t!%g0cd}k_@2*id&=ipQ^z+T$c63F7!o`x6u2lUA+~<5b ze^BtIy+Px`mKNW5@xfJ0flxdD;zNh}0RA0mL_xPB4>)*-{z0n)Q1;(Itj~#unAd+w z2F=_lyXW?jhoSgR#J_$w<^eoHjSN!8#uhDN{kI5@|k4x+y zw!_@<0zSvoqwT@EXjAzB-^bvewjLA51K9_&dRCHx@CW@DSS^CJ;QEIj^y0^QOx-NS zFh7{MFHx*2u$&X~Ig`lS#j?toX~aC{bE)6Rv>l87kDS*?exF!N2EYHB6wi|uvIwzb zsq#scH$IbrUoU~(f8CoS!x-8nbM8o$ifysp>&voa+5c|$Z`!g&3cOle(xuC*elD(Y z#dXJsrR1qHz;B)#u`=1DGva0c>DZg21AorF3JMl3DHY1SD+^}cl$F5z8tkb-8NY8U z_Ra=2Po03RxcRj7>o*bpxVI%)vhA=_Wus#JQCjydTMoO|d;IgOD4#GBg(-`@Bb!)d$=- zfzt)3Gw|IGhjx&`GZ4Eu=ZwbguTq(gH5pZxH#)ti=TpAh>*@<2U%-AwtAKxEUK7UO z#)q%(gMNAf_pYE_uYo~rzU-WA)rqSsF?=nI^Er0!u_QbK6Xn-7TAJu z!p%sNw_leoGmgp6SX1~y@CQEZ^V=HyE1oT8-7#e}b%H>u3%!BoC|CYr{07Psw4Q0p z;ojw}&+qSz9pF4*KUm)b%h-3W??DHY2aF3Q{-dzh*xI2rF*i5^=;&F=3?F{`af@X# z){*W#a|h;$)Ww)UWysK%aXqoTf<0F^4#isRpk4A1<>ln_+V6(n@SPrnHJ0Bojy(L6 z(r7!Aq_tK5ji)CJx^Z-v2;0M2O57e?;=XTv77BQdpKQ|Mzjjo40P2 zGOt&Zg88b-N3F(5?rfFh)q<7P&z>EztF$Y=@3&Z5HR}gnkX=TO8H+uK;OP06f5(m= zm+H0ZN!F}IW#Wh}hy%htT3ccfi!5^XgvcKU?@Dm+X8GvjK8RT=E6+clMsjCJjWw7G z%Bt_GLqDYL&2L8k5BdQ*d1&v7Iye4V%*9BF7zkoNH4^9T=PU6R=2TwPdHa)reeS0{ ze#dPoUA=`ompD0eD~#P$%q6?W)=|C7;2nK!``h6>fbReuDeVyd+HaO$D8?;*QO;|s z=M(dqY#Gb#y;=vy$|qFc$NgSTUDLEjyE7KPfa~+P{{BqqIQ1y@*uILn_m9+eH#$9z zDc?0gzGHmPll>g)Q`{T5zZyJY4Y7|9>ZQkS-R|+@gtw+F+ZvRwXekHxIQf5{eNM;u z7wkULjdGl_pZ>lFc8%1(A_e-QXbYTvfVGxkD{KYd;CTNx5$C1XjNdW0e>D7lzytJt z>QKa^r^Bl}r(6dfacuPS0QQMj#lFgZb8Ikd`pV-Y#JWu^)BEhX(+fO)V%svj#68pX z9q4a9)>VZq_z3uo!8qW7pBl=0)nAt9lO&d;n8#WD!#>iY$5_e#YB|s&)@%Xpb7y!_ zf_qg`S+BlZ$`hN&&y5~}XQ}@Kx`6VSCK^9Br)OnpS@~tj`BEx*K2a)3lcu1QE!k2& zYco;C4cRS&dPhj!Jmn-u?tJq7kRjO163qT@{unzqY04CgqZO3`c`M1tLEELnM{}fd z*{>w{=kqds;!&A7@i=(QJ?wJ_U2Wzysru%}@U!QT{@)Cet5>1K{ewS_!*d<{if>kh z|FEEp8My%aWeN5w68ZhNyRs-^uk`6NR^F)87`8=jc_C>Ec`bK(>CyBxSwFOfT!%lH zu{4Tzd?#ann{RhBw28Ej#A%mpnjD6B8?3{!hg?|M57f^>c zem-KKm?!>y*vC2j`;_~%@11{-*k1$ehweJLwMv~vX=*orKMvmgCym;C5;SkyZ$Xp7 zSC`my>A_Vj38s4g)u@2p|7WCNYEVbOTr~IuIWEBU0A^!uf4AAEW$>)+vOl<+M23Rq z8S?}BSKQ&78+{(KeO_IAjPHZ;SWQ+wqhDfd()!>B4(D-ms~`CKDDHLBw)P|Y7SCXE zI)~~A+ArkCcu&e~>Hv`Am$1(AK(+qWJ7{0dw%kaKtz%(x;AYITv6@G-oF{Vt|XpDCwKpTS-iQE|GS zF^e~E-I6VM)>mD+!#AG~YbBMFF=H0W#tmm>`n0w3)mKBM-0KZ6|Mg|)G^r(9x-{^$ z<(A19OZ@|O&pqfq{JzAzNp8Hy>ftD749uZP_2l(eUQ*pZ=?kgUH$fgyu*};sV$)Um z8GPp3#eYb#8m;9i%)iO`(u?rpm&4qf=7_rT%zekjxbo^A=+;jyC) zlUw@3wE#UCIp$=%>UNZANGfX->*n0(4|opmN8h{mao8unI|^O@$C+!@zc*#)U0JUF zI)`Usn>v6W|3?20@B!-jko7hx_AkUj-`}XgJkLC9=I{71??3*md!L|IAGdpc&50|E zcL4j0@jZU;ksw6$FaP3m2WpI0U*EGRoizO6jMjXxNSo4)q@G@YL5XfJ*YqnXdqNG{E4 zAgjNxF8y1Tz?frp_`p(05{xxF@_ic+ECTT-}a35}b7 zA$bO@51m?Gd$oc5I{JkA))&u?luBj4l-7-hnehbRoofYq2|R!{#`4hn(xbynd9^?l z;4Fu{0{gvj(-y+HvjYbWmR^1O>D;3?Am=k?$%(O$7r~#h$s5?;>diOb#X4ps<0a`hVVPtB-{H7wnfw_fVnB7o=V4u3@D+n~)0ghp&;_3Z z8U%f)99RBNIy2{5u4Op)uJL^Xv=XcOEBF)IG5u}YWF6zHvUPEP;5v>iT{#+{?rfSf8a$@r zgour*)BCZW&@o_(IlTuz*>)rL5wTCb|K2~)`~A#;ynqR8L-t>dmLC>h(DVDem3XC&1zbCw+{32IcS9py8R|ua(g$GsP5Z<0SAJ*x)=yq@em;Xm+a{mM zcIDhZ@6b3_;tk*0`C*NHM-2LKh-353&3E9pjilcdYqI6ez_nNsVNBo! zNsl=XWoosSWNEU2&n1$^74pdLaq#0~{LcBz4IeRWyJy7z?ymn04Ui5ET`GN9+VIzL zeW~q$9s>Np_NnUiXh-AMhw^!Mp+ZlJN{E9Q=bu76PaFFYsnjziBD+=M(qqG2Rxk<5+0!mtRd%u||W0pdP0zRoe7H6BaBF8a6#7 z$AQP7vh;%Qy+tE$MBfIGqgW( zKkW?m5oyx(sp%_&Thb5dmhp;?u6QqX1VdZ4Ux{s-Twe!h#?gVx%{?9tR7XXb@)_i6 z6X1J6|GfvBf;Q~d5w&C{u+Y0jQOqyTC1ne|B)KxePmlr_OqKyDkF;no1oOUQ75kin zA5QESYb$wjR+Tp^ek#=}caWOZx=5|JyJ=p%N=K>s))!K#!lzi9vW>h}v>D=ZYDtC+ zC17V{0`618=1zrpMa-Q~nObsWOe-bxWs=6_^GJ_ougNc+-+*kdC1>z{cY)PN;GKMu za!nJztM(PYr8zdg+3-F6j<(+nHXu{+EvOF@|M%uMmoUUhaGqTv*#G~5t@ZTN&tQxo znS2e~{>=1dI&S{}zM6;N$4unK)G?Jt(6;dt84Nm}tGRtIXIIkRCyjaZM7pqv?`--q z(2k=OLu2e$+>3e!^9K9QCeYt*>4opOGq|q|nzcjy`jatlgllS2hWqt*gMU9C06(x; zr%g}XE9QxNBn+|3sCRj7k7PcU?F=eisS5eQf5P`2`)2D+`1~OIf4}=! z{(;&51OxE!kFkK|zulBBGf!gtVyo<&+e01z`!O8*^JAYn0P#)yD-YnYN!0VzCbRv* zzVZ48w8Xyg^JnK*bbZTn9K=_W zUx7Cn{fAih%XZ@2VqL$V`99mKM<)F7zmgu+&-^1dHyjZ^{+EUuE&oT1X>FR;!DQ$r!NJD>$ecM;057UmH z225Lq9(>nT4`4r=zSjHreUOg8#vd_jopb^&2Ccd&QxG%05c}%TzqbbXU*pMq%KiZM zS+2<^@5uv*d*%qOmEax6wpVM=G<~Bsp9Vesw3)9TN6;s5@>$RRK{MBG4;mA)sNnhw zcTZ9FGtTGA{{ns=H@J^IDnqwlk*}s7mk~49$icZ^Nu=T*--$fdC1Mgfy}>_@wLjP= z&JRnOZ<4{j!I0vJ_u?BXcD2t9t{j$)4q)Uv{RsiP%sj{8JkWn}`_s`Y`;Tq9c1>>S zfOM<+{&%=jzZlG_u3q2YQL+JP6c%q{lo*f{pM9Ym}Uw+E6MN zLEK=?p0L}E%x7E;vA;6xu{5eRK+3;b5c|T`m3?Dt$*z$#WXFh_vVBAiO*=-`ls#i= z%WvcB%8|+S5U0=pzK4c#GZ=no_-Lt1bDr5Fj7L32dZh2dZw^1LzQ6e{#If<+u+NAk zlN_J-^Jw?m9XM7N%a~ZiUqxa~wkVxzYrYZnFoSpV9ZjErZhYU? zeQ$$X*B2%y_6`1Vzsew#`})lEpP!!C-?q>Be)c(m++No{gr{~vcAiw*PhA^M*kI{AMK>r70Af2Py4V={KeQu$-f zm+~+e-z^06Y_V_TIcN#z`n}O7?%4H@!@lFsD#M9;w!!+8=j=Dzho;{G{X^`qp7AG| zW20Z#I=J_d?RTYV9GMq-_QD*m3HiXd}n@d=18ue z^|#pXNI~ZbE|e;g3@Ko{O4d#;L4BFgAw0*Flgl5 z@WSiP-#tUUe+S0=ulVf#e}?)%E#oJE{%|*1CSu*2PQPM3f*DJI|8~GXXbJd_48iv$ ze*KC4=;%jtcOJ+|Q{FvZ|JpL!w|*Y3eS>mi-#Z#WebS(L_jVx~pUFReIMpSU>O#kNzCJA$QMF{>5_FhRv1k*oNW-cm)j-v(bnl{`URy zl0SD9nKki@uFFf@uS8P(!>2!S*g<(YM|m0g!Rw$w${@DyXglz1*R>7(9yrE3#5s56%*zG%+z z`}wX`zcBA_-bugx6VJf+bML`#K;N?_(?ffq&pzb$N?(9w(HDd1L24 zjQVj_#79700?7XRqW}EoKS33$*JQ<~BK95qp0PfR*Zb}EBf)f@|LM=ke%^eM_{aKB z+*@q?OoRUi!T9c=74`}Hf|xS+GW28mHa;!cYpx$1-LTIbZKBNU9~>K-pM6qSj}3jX zzUeR066-s@pcvFPe9z`{SRe1mWbkk3$Kl-n?%Jk(AHA-)#XHz%DQ%-YC#&>c{SD@K z1jax4Tlic)-_VunFu?IW#KC-0vlRS!EhS>s-2nEj>?i&g&wU`(Ds_-5MPHF)h%0p1 zVw;K=etUO}+#PG|1>WEPoBhWD{zpuhw)~hUFEr=)R`L7FKF9qo=Kkch7r$M6JH4iD z2XN263-A(4N7`5RyP+L--91NTzNI&9lYQYo_r22-@8hny{^PT}WctX=TdyzYLO+r@ zb7vvm^>^tSa$ZJk#X7Kx`vCSS|0e+ZKN@EBf5ks=@9cVP4Jn#{dD{0(*oIg_ zcAi+&^zmN66Am^#`JGsxCdjG&4D{R|%qoa1-sZ?WaXK7Om0>~A;c<~O^32w>9mmFrK+R!=`;+g|&;-?Ys( z6nE~M+V}JGl7x?mf73Un578#Sr^T@A6Z6c#Z`SLmXRuCQ!DDkCR~K;im^M8gq0c}W zb6;}2nPrhkr$RrIsdMMS-+e^7g<)OhEs>tQclti@{*%@HdCYRfKjuJi+)sV_KDqB? z{ran%Kac)BbMOAQKL6Bp19Nz9V;`CS z&!5u)F5iomUupjX|3jv4kS%lj!LNiEPw4*Y7b2~*@6o6J{&qOizQ%Rauf(Z4j+=Y+ z&}DhT-qxcQ?P!{mwv+x?0EO^quf`nZDxtDQ*$9?*n%fPI{T3KnD=!&JfU`7eZxykyXINO=JVrT^*8$e z;0wJEf5f4X&hpb7uC;qizMg+sMsCBN$!Kk33;BaV0+2V3Yz z#jC3Wnhanc^{FdcIyE$5XjYHO8w4F_uDfnu3$6kdSWvO8*eFs2gd`*(p@dXgDAGcc-Vy?YE(l5|F!z0D&UgOF z&!5DC`|Q1&KhKw$Gv%9^Z@zc>nR7^eSP0y^m>0a;eZAO61NUQbHyq~#qtQ-G;P}57 zXL`~9%XpvTd}5z{e$LiIr7yidwnNgQ&%fb*egMGxIdS~3aMuxMSN}Q&QFD)Th-+zW<|f19`(V?J@24 zGi2`hx$T6DEvaY0lJpHW_N8uh{R2mQmhhrSOjqKt?ln$fmgzaLrOR3`TwMYmvyWi% z$2^<8OXKfrkBr{P_*BwnK6IX3)8x}?w`2@M6a5R%0(|tR9xAHY9cs+5P36b@Zoe=1 zpF88C8u0Wq)x1%A^~c@`s(38-&)hZ;Wz}V5Idyr6PYdH-Iq2_mH|0ZKTz?~bgA)%g zy51O%X#nSLe6iCM?3p@deAcOpN#@7pUmlmP3@IyFm!MqSGKk5zCZ6Pr`CvF7+388X z?YwAz&DR%4^n@mLf@@8s>AzJoQ^u&lNvm;2f0r7PdqfRiRc4Lz&6+;f_KEi~+63O+ zzFznF>xq9MrN(3bKON(LzMn_TbF9A*xL>gEblFE4`7Z&wS2n=9{#8G^+7HFw?Xgkw z7EVIQqJAOeO{hnigwiz;eAgguzPO4!!`*80^v3fUop<0ys zraGF2@BFx9fOAu&?onqN@6t|7yWleg_c|QvMC0CVBkcO7-6O5{rcd_jTk9+9*|CcDc*jMaD*Uz&*lyMBZ9%tW&q1nKw605Bn&)w3nVNMW$|8 z!LZFo(y?`c=^4GEY0@(FZDb;PqSHef?B~m+jr{SRfOS)!$NMWYpvPO)3#p~*?fJ*m zs5QVkaBcd0du@-{H|>DN|0USR`^o74IR+q&$M@kfiqKaA|BE=!$JyPCRoiFvdE~MB zPrfkF>Cp4i`#HWF@=dH?{pA_wl@CTczj@|`x_Mhl(|CU;-|yf2^#%Rz?)U5WO>DLS z97mKKJEz8A!SeZ(U3f=+gPM{u1$TcvqfX#_$+2k>wDf9@T-I^kkk_obd2>&{n~ zT36g>4|gucOg)>hX8vI6-qUI7kQG*R($)|16n&zpbwi|s`x2kw zkS~xD;?dqD4x>#>AaCh4PP3}E_uuZ%1wi1ILUHg+vP@R0Vx$ldrctQW^k zN9sZ9QRYhJFw1!X<;(P>PWbHEAs@TQJX5e|>Jus_^+WY$@>0CNR;-5R>{p+x!Ts**&I9xKF3vg-aLzftO|q|Vu#YhS z#`&6nb%T4QP1?tRCZPY%*m*(CN8g{f)Gg&sUxFb#4(Aw=IR6LO7u~1D)ld6b$HY#-L)RkuKC!{lD1Fx zJWRW~E$NgueLM_r$&bG~pM8E>_uM>r`Q`jZw*$xn#_d^y@6&z%FCTnMWlzC3eWl;G z*8RNx-)#p*{&R!6@6LWIBDACG(;*gn09{le-@C)zIj6aPi8MKuz2K4_*TPNrdBmVA zTVqOo#gsD1^QET7GRvuX`Q%J~B6D4K;#RPa@IDzJuHc<~kWU!{Xxzh1x69zCv8>}F z49Z}_x$_;ip&Ykqe_tD8w_(0Cjc%NV{0~k2m0FQDP>oE^)_wheGms;wnfDYwKWVEPk({R%r@X56r~caLHDFF$MK!mw|>`MMKo@&o_utq%1%C^-`Qo~&pw~H7vi)2{w~bSq!a3m zmlvuB?jE38h24(#bXuuLyT_@2Jla`}f2xPd#Qh?3huo#+y?U2g{}JxHM7t~fv&hht z%POCig?XH3DTuU&TC;ZHYDepVb0@8Yl9LsNIUZrpmiq+TVQ zHo+TtA;}tqX>06p57Wok2>cGFKci5*1!Mp10>(jTM()U>M{z~3q&AE9- zaT&(!Jill9e&Sxn`d%9l5c{|vay{=OGyDC5{qgJ1TzqH7%2Dk)b_;&wkIy-s?$qaT zE(_KJF}e2_k2(+i`gioYGIh!4&n`W5<}~~N)xdu#I)8imFF5|T{XgT8zQ9Jp>f`4A zuW8#(sdw;w0?q?o#Ji58(7qO=ys37lJ)utMzQ2#wDb}mTB;zxW%Ry5kJdJK^I#mm!S{pfc*c)_oTrTF=0X{f*0CuPX?D$vg9C zrLX5acsA_~O`Z?nIR$e@nC}<&f24~FZd5k?*sZ#RtLf&4%|KABo}OO~1kW$%*U-k7Fs((8}F z`q^~oSVkXp5@$6^r~XDQNgJ%jr(~#CaL>*&i96N6%zbM7>aTFWMj7s`h2CNfPTylq zQ{Q2WJ9My4KNjb8WxOw#*JFKmOyKqT8267Q=7o4SJZS^&B;9acy_>sn)`L$zf8&4= zQ=M^ns{+CGvb+8ExdG1Khra9F^U#0QnzeSvdn=BdzreXa`~SV@u;r#V!SHv*zV7?6 zm$(#smtlOd@DSz!n@*^qc?VQqted`ob(CRA^Hf&KM`~N@-_)_RN7Nad4Lpyrzuh#?J@d4d((jfCsj;djU*Q($AVyp@aeNZ*Jsjd2R?+)tFWXi@5 z`^cA;i;Z{NuIs~BCzQtp@u#1QU-A(7dh(WV6_~O{Y1#%Of6B$zR!AS22pHG* z)dTzb-fDe^z1(Ae(e3{)VD8TT-}L)>J>RpN^*ov5Yup3t#QQkhdpH)vwZ0Dvb}xOb z-`kNR3X1g{|EjC6x$HO(#E|EGhfmFRMu7Ug`F_3GTS}*_#F=0A|7&pWN8{h^^j+iI z!e57H+{3v0XXTf;2N?M0xx!_jf`b>-l#M6UTeADK|Jeq9-5xsP)7pjP$2!Jo@xD-v^Za3 ze9=9hX|QZ~+ijGDH9F1y9%<{e+_=(*ktg){>$Hc|oT2xs$M0&Z!Wze@RuO&F!}tF~ zz5MJpJ_uklaQCNuWep9qBaAvIBxyig82XYH> z-k0nDYq0;v@&9*y=g)2^cn9z%+yTya2{iU`|MxNw?cCeoT;Ky-Qo_3ppG_*&-voGV z@^UpUDFbVR!_X;Dq)wKt}zQH*BwO+a=?K-S^7ECaVv<1dA?SW|*kT%x; z4krIp%@}--`s;)3RHv4a*w>FyEt+&!vC%(MT@rqWvw%00famAHRlc@L~4C9qC zu7A@ldb}}z#{uV^$?2i9cO1+vJX)p-f&JB>5_kQd{Wrhq@mFt#>b~}|xbq`c-4FBO?(ymmecG$nf7?~1z0ymq8Tk`+a0{f~od#dC zzsh;A^jyx9g^>y6AY|$X z;}Lxx+{jz=*Xg5f!Q&L_katFXiT<2tM|a`=$W3Wa<1G0gm5uYHlktAt2lxiw;KbFs zAL*O4OASchtKOP(SWQ^-mC7zTi*Kl=Y^ zJGp+xJpk$V&7L0bfalttS=S@(FRD??z9=3rF85yIK0L0g(<~Ul;rQeh}M$ zKE&kfADR30*@|Z;@5WfISl<~qaPk`UX5wNsJSk62oRY3m z@$I0QDWlcwl=syFwDpT{r`VFzp=ue%dCOCWfM~voJsg-Wtw z#Pe838i4ofhGZR3A1pko64#vo-p{Fpm}heSPTW$*IH%`2o>|YM9eyC)*Ta3OCLi9t zHo?Wc(cz2e|1YYUCD^aqcwX-T@(ds6`o#V??Bk8q`+4~0`_jWj{XU+l`}@{i?smHW zwBq}=FDJgM;?+1TQsaO2mOai}<0dzmv$b^UilbQn0~UGqf74eN^!Gfg`o>3~wt(0N z^wn0JA;KrI@E##B4pZ(ESuWTo{%ME5Hjy^l=J~kaJPU1g>gJPbT;VY_Z2n>OD&}b~ zW$eYC0oDqV(IzDB0+rx>qGH^oP=xoaituj14$yYhKY6?A$M>#4{Xqj-FmEK>d!bG7XV~p(010(RJE+irfzj@O?tyB%1z7KnH2h@Aei?LAHRP-5HJJ0IzJm-|&cpexP>}%Xw zShq311mQ>q`(nCnf{lCFrT-`2S-TAWHTJo#=dSJX9i9&h_H6I_LE7Db0=})$$%(uD zhuzDo3h(~@5pCLP{O7JOa$b7(!y9nd->}aPojJ7(V}Ig*E&A;}SOeg@|HN}3stEgZ z-*XE0{iD5IPTW(5F2;#-n*{sXjbW^Cj6->IzhExTT(FM0UXX%y!^vyEQWI7kS7Qrt z_HyYl_4eGO>h*lk?4#;6m_l@WEzdmD@AcVVYCoDoK{^h?3}svr!jgvO!=HpOA1pei zMlJbLjl@UGNuykIBtB|55*-R@giUZC;nK*(M?Jf^4Y&MtJU+YeeSSyPaKzPw^hSV& zTcn>6Z@7+U{OKk%5)J7n65MqDbehZyd7At&Z!C}GOZ>#&=k6;H-Mli>uAdDbk&iLq zCwYKLyGP8E`>gW_`^S*)7z_%>Vo*2^m}cFwj;E|Yq0%>>Qkgr>sN7xn_66qb3-JCM zk~MufOpSNo5m*0fyqod7-8ZfOz*-1qT^-oM3jy#@_6{zqkGJ9pmq)0%0E*Zg(yzON2) z{x5rgTX9~8-}IJ!z(9BxH?|M5M!>y(t|jVz9~dr&aX?GM;-a7EQpn;55N)f<+VIPWlwzixQezgzsYDtcRq< z^q4o>g!-B}rI^f@n>T60E(wFMfPtop{Iomx02+qBYyS+UM}9oe9F)(Ccc-?7fZz5J86Lci9Xy)Z0qTl%AYhlb}JGVkKw z+BD+d0_PQFBDhXnXJmS|^E~$d@w;MD-ooAsb{<~EHGq}Ju?B##IKS<^^Q8HHPgU~( z;-9}`I5fojLIu}Vny6n7>(J|R;axx7Ho%XVqHbtx!A?w)^6~wHq%GKs0scP$=J^hl zA)co;Sf7YB7()i*f^{)JCGJ6!fPEj~-t--~7yls6Em-MlJTfmBC;9ArHss}xaRQJo zkF8r02Js~>O(~yUe(?{45!=n=h5182N4`vhX~NXuVAuU7O}fDp441RJembmwTs)V< zE6eZiCj8MK@Y|2tpX+VH%QZD0J5R3N#;$Fbyc4e^+pjXamL<~!e>+#Uz_4|LEsi9Wy#&37$UPbu)q1}UmL!2j`dEUAI;YS@{+j)EJ z7uv&(DG7-8c)(wLIzHWiG&Hv2&g9D`1~{ha5i;$27(6 zk83^7hb${IMdlJlKZ7T6Ve{iX>B-y5^x5f&*kk#WgnoY_Y4b(&<%TA~6l_l}hjSbA zt{vZwWB9b~Ww?LQ;9uuM(w4m2&teZ?GVc;!%H^2|?_Vand}5a{5>MP|N?Rdm*nWZR z!b5oKFp@`@+Mj%3_t$@&mV}{crLWr=JZsYp!|^iLel#!F6dpS73}<7Ec96!Pq~p4I z?ykSx=lf$;o^@FyKKMy_gumd~zfJJsTjk@uc=Jn+em-%|(ubQhZG27NcPBadhm}*S zPQ4!*vX`BICv(rgleTD;^UL4=rB?c~b-!7%@A$Tr==WCu|6H5rTEOR63*fsuybFly zdgf;=^2mOl`-Xz|fY^uZ1F?hLXgph(qx=}2So7k+uIrxR=x2rJbGbaYVWpkW*!-6LzRm;E5M1l@kauaTwCVcm z{J7=P!dx3kF zw&N}q;=hveNnZT(_+6g4|MFepUCuZxk6=Og3T~yWwyFJGtVleKEvYw{x;|Y~+5)eQ zfO|PiN_b$=?hD9+`O(-UPThDqzy7!;54cGj!M=_w{Ka(hE@8=!d(!Od7wkU1Htn;| zfBndB|5z(|!AcFy_HMuVufpjMna+d1dfaJnD!VON(-hFDMEi8O+amnF@g-6eq zu?^7r=#B$87GOKTy8((Z9^e@P_W4>~keTR8r6y#q+x!Yl)`2FMpKTAsmpU*X_@Aor zZ;kUc{(<95=Jc2L{bpWp5pBXnmAHkbf=RllQh@)t`*AJ;>3DrJ^DgDJ?cydT{VE~} zW0`{eK;sK9JuAF+1Ao!mOTtLrBoF>^nESx_#!9qmuL2OT>oCQ@2h>- z4;%V!Vn!48_5I#@-^pCH#RWXGvp_-G;=x&2U2g9nW%$uc#yD`yMLWX-Be~4U_U6ms0@8J=Ka91?*BOk zAh~_H9{;o7=QD6kKjK|XgLi}bB>0mI{>$*bw>ihavX{$;g7kyos%)Zx2-Sf&vZOnFYLMzJrIBRUDE&AVeGiDvo48$zKcC+i@`t5$-u9V4DN}2 zpPRuqF^(fF9^v?oA|+y9Cuu9*ncH?j<&{D|hzU3COE4jM4`lk~S9nxuk{8nyvUyj= z(++3b{o~kvRoNMrVf=ZBdsXqpU*cQ5{P7$x4T&eFKR>ujn`L9$pBKmU+!vwA`P`BN zClBUs+BI%s{^HItaS7Mtt}Alx?DJFS&jSWI{1(=K%8%~?-uH`N>f(;c$kJ`V8y`=& z8S8=nJ+JuC^rZ(+A1%bbpxzT;dkGm{f>Eg(F|~{>ow4fNt{0n+@U`9Su;uI?f1Jg{ znXl=<|77(490MexU)TLPFpO7=4M}_8J`>*>@H9HnRbt4-s9P{-;1bU^(x&?OAt~Uz|_#7CAq* z;(cG2&fb1tR>q2rPmM^=Zguwq57j)4zU=+PX->OtJ)M`|{}^{?f3t64Yvun>{D=9# z?_YS?Y0@Ix88Y${2bkA$f&oMSS#MnC+>X;%ZvHFw2`A4l`C`rD11G;&%6o$_E|7Lt z<_Ts!)wB;%x6-ED^=iA7t7AX?5;$Fm~K>aa^p?U)&`u zpS9gj{^BpD#PdvxC&TFez~!aG(BF?VFF!Y1e{^`_+v;byFSq3IxwDJ*pDdkUa-?9^ z`kn7jS-A3-AEnHQ{K`;ZF|Jv;lu-9PYfK_$$`RdMk~7J|gW)6|bt}IVd-cFG#{fP|1HTx*mm~D& z?9-y=Z=Xq89e z)0DWn{J?UC&w|+RiaC1uL>TOwA zXUWM~zcVv;L&@~qjU|~_f1R1PY4?m=L$h)>?#{wdkMcIn9-hB>j-gF+htJ+TZ{(aU^GD3yvf$(S zJMzXXD9#yOP&At~rl4r<==nS5eLQdb{896^8JfFo-l#d-=E806$McKkjhPEc`X&iaxWIU9D($YK6B?#cq?g7We+y4^5B<;aQRuE7tcnR{Ot9` zxtVKs%$vD-djZ`ct1M6s!se3){ASNsy>0HyHQQ&y&ainiS8vOov1;2K&;s}^nz?2h z;ue7?_)%^fcg?`v52%A(*+?6_igQsnb3k*at=Rlo`tps-W~|z}c;>2Yi@;+M_|3=j zyiBBla%N+%EDL4IMIPoNPYd9;e8$SHg|pUf{|wIsaGyio`CInP)UV`k3l{m!QuZTtUoWaEb(d$JDn z>`M0Q=EAX)ZoTjf^LF6 zG@@0lMoq(OHE4Ki@J%;13clr*Cc!^$)I21#dCQO%k#Qm6(FwJ03yrANxJg)0cuf1? zu$YA4$hgkIQEj`0M8tOq36JYsJ0iYw?eN%+Ae5E2G^L|o^Pu+|B|jhaQ) zs&{?Enm5!3HE3A#y1I31HmF~}W~0W9Yc>lD4~mRwT`LOIB0M~(S&J6h9v&4PbX#bX zpyn-*W>j>o@aUGo&BG#s8#QWN>&6BRf^KZkFzB`>O=~r69tQXDS}h_XK~c4u!$df3 z507G;$Y8oh$F>cQY8@9671O3xSai#vR`Km>MYf3#4vmNm35|#fX%QV0(kvo6xCwaO z)*?Lkw&r0WO~a!?B4gs}M8&qP1G6^x1UIE$lhB}s4eHmt>82ZNghxgPMMSm?Y8Kio zs714|TG3H4wc^@#tQ{NQp-vl^@$EZ@#KyO;9o;H61bGPw4Q&=278YJJEHWx6qGjvg zR`CfT?Yi7vyIt2iuIt$2?(5oi?oqc@+YU%CKIGQMO@eN|<<^>{>+4-tqjt@!uessc zy4T!r{q;3!)wqV2T#{;nendSHTKnY%Cv)|7=cB2)&WpoFJCFR~X{XCw_c=kqx~q4t zyb_vJ{rmTmfIHseSmBzQHBnz7P92OHuB~^21Ds+lA;M`I5$*7s1I?paInASEoTgDN zoo2A}E`pZrIvPso=tRWEJ59r*o!gp)IgLV_JB^!$In5$lI#F#BoanY4o$%Iewb{B; z58S2H#knma&S?_S#%UVa)@k0dy%T}^m7)`RIxXAxa>Cnm)nQ_~-0QUJe7DoOTW=?} z`~6Pq?)N#Zy21_5F;r2V7PIvWo5-`4P4gVNW zZ0FlS-8IF4Pb=h&er-A$F=VKMCFMa60z5%ZX#!$V&p!>fG~Q zr$hH%PRHBta=P5v+vxzaE#fnLOO(GI%4*8o6M5<7bh-O}r&F){oUZr$1b5ZbmQH$uJM+&I*^zTwSI?Yh@#Y*SzUFQ29V z_XA{gj;bY4ErEY031Eza-?2-xG*Ek(SLw#QqGOya=uv;Mi#-4w`=*|_Me}L4< za3BKc^allE?>Kc`|BHdWovU2`z{w+$>t8|gj?>MgUr+&mr?K%LRFQuT?O##$9A*&Z zt1x?%5d6^z!oTjt%ESj1y;zy}9H&MV{#RYBOuP_q75-OT ztW5kE++3_w{EqW|{a>jNTt>)UtMV`LRYo9N*DCxQRpnn0h^x=7Q5F7ms`4M?;}ytO z=X?5JQ$_r$KHN~1e^m!gtNjDjYPDOn1ga%aErDtYR7;>*0@V_zmO!-xswGe@focg< zOQ2c;)e@+dz~z!a_4-rglB`NTRjvPBGpKU@-`D^9u76g!{#sCZ{;OOcE~z|zRqM}H zu5VYd{{2c7>+@ClSGoRQ)&4-``wv%CvHx&Y75f|5{}@!6fUEMy{z^e*@v(nXQiXq| z6@dMrO8o~`y?<4ifVuxwnSi-}R@nv;_uncLpx(c$L_m7~uMz?2{liKGFp%k2Qb0xj ziVJA=KPxVP**~qghOq$*{uS4dyMJ3Dgb + + + + + 应用授权 - 中央授权系统 + + + + +

+ + + + + diff --git a/auth/src/main/resources/templates/home.html b/auth/src/main/resources/templates/home.html new file mode 100644 index 0000000..948c66a --- /dev/null +++ b/auth/src/main/resources/templates/home.html @@ -0,0 +1,80 @@ + + + + + + 主页 + + + +
+
+

欢迎来到系统管理平台

+
+ +
+
+ +
+

主页

+

恭喜!您已成功登录系统。

+

这是系统的主页内容。

+
+
+ + \ No newline at end of file diff --git a/auth/src/main/resources/templates/login.html b/auth/src/main/resources/templates/login.html new file mode 100644 index 0000000..6a3d56e --- /dev/null +++ b/auth/src/main/resources/templates/login.html @@ -0,0 +1,547 @@ + + + + + + 中央授权系统 + + + + + + + + + + \ No newline at end of file diff --git a/auth/src/test/java/com/example/springboot4/Springboot4ApplicationTests.java b/auth/src/test/java/com/example/springboot4/Springboot4ApplicationTests.java new file mode 100644 index 0000000..dbe7615 --- /dev/null +++ b/auth/src/test/java/com/example/springboot4/Springboot4ApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.springboot4; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Springboot4ApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/geteway/.gitattributes b/geteway/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/geteway/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/geteway/.gitignore b/geteway/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/geteway/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/geteway/.mvn/wrapper/maven-wrapper.properties b/geteway/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/geteway/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/geteway/Dockerfile b/geteway/Dockerfile new file mode 100644 index 0000000..d2d86e6 --- /dev/null +++ b/geteway/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:25-jdk-slim + +WORKDIR /app + +COPY target/*.jar app.jar + +EXPOSE 8083 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/geteway/mvnw b/geteway/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/geteway/mvnw @@ -0,0 +1,295 @@ +#!/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.4 +# +# 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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/geteway/mvnw.cmd b/geteway/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/geteway/mvnw.cmd @@ -0,0 +1,189 @@ +<# : 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.4 +@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/geteway/pom.xml b/geteway/pom.xml new file mode 100644 index 0000000..8cf607b --- /dev/null +++ b/geteway/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com + lingxiao + 0.0.1-SNAPSHOT + ../pom.xml + + + geteway + + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webflux + + + + org.springframework.boot + spring-boot-starter-security-oauth2-resource-server + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/geteway/src/main/java/com/example/geteway/GatewayApplication.java b/geteway/src/main/java/com/example/geteway/GatewayApplication.java new file mode 100644 index 0000000..80ac119 --- /dev/null +++ b/geteway/src/main/java/com/example/geteway/GatewayApplication.java @@ -0,0 +1,13 @@ +package com.example.geteway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } + +} diff --git a/geteway/src/main/java/com/example/geteway/config/CookieBearerTokenResolver.java b/geteway/src/main/java/com/example/geteway/config/CookieBearerTokenResolver.java new file mode 100644 index 0000000..0f7d484 --- /dev/null +++ b/geteway/src/main/java/com/example/geteway/config/CookieBearerTokenResolver.java @@ -0,0 +1,37 @@ +package com.example.geteway.config; + +import org.jspecify.annotations.NonNull; +import org.springframework.http.HttpCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public class CookieBearerTokenResolver extends ServerBearerTokenAuthenticationConverter { + + + private final String cookieName; + + public CookieBearerTokenResolver(String cookieName) { + this.cookieName = cookieName; + } + + @Override + public @NonNull Mono convert(ServerWebExchange exchange) { + HttpCookie cookie = exchange.getRequest().getCookies().getFirst(cookieName); + if (cookie == null || !StringUtils.hasText(cookie.getValue())) { + // No token → return empty → triggers 401 Unauthorized + return Mono.empty(); + } + + String token = cookie.getValue().trim(); + if (token.isEmpty()) { + return Mono.empty(); + } + + // Create authentication token with the extracted JWT + return Mono.just(new BearerTokenAuthenticationToken(token)); + } +} \ No newline at end of file diff --git a/geteway/src/main/java/com/example/geteway/config/SecurityConfig.java b/geteway/src/main/java/com/example/geteway/config/SecurityConfig.java new file mode 100644 index 0000000..53047ec --- /dev/null +++ b/geteway/src/main/java/com/example/geteway/config/SecurityConfig.java @@ -0,0 +1,73 @@ +package com.example.geteway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +import java.util.List; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig implements WebFluxConfigurer { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .authorizeExchange(exchanges -> + exchanges + .pathMatchers("/api/public/**", "/api/auth/**").permitAll() // 公共API路径 + .pathMatchers("/api/**").authenticated() // 保护你的 API 路由 + .anyExchange().permitAll() + ) + .oauth2ResourceServer(oauth2 -> + oauth2.bearerTokenConverter(new CookieBearerTokenResolver("access_token")) + .jwt(Customizer.withDefaults()) + ) + .cors(cors -> cors.configurationSource(corsConfigurationSource())); // 使用自定义CORS配置 + + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowedMethods(List.of("*")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + CorsWebFilter corsWebFilter() { + return new CorsWebFilter(corsConfigurationSource()); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedHeaders("*") + .allowedMethods("*") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/geteway/src/main/java/com/example/geteway/config/UserIdMappingFilter.java b/geteway/src/main/java/com/example/geteway/config/UserIdMappingFilter.java new file mode 100644 index 0000000..4ddd414 --- /dev/null +++ b/geteway/src/main/java/com/example/geteway/config/UserIdMappingFilter.java @@ -0,0 +1,41 @@ +package com.example.geteway.config; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Component +@Order(1000) +public class UserIdMappingFilter implements GlobalFilter { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + System.out.println(exchange.getRequest().getHeaders()); + + + return ReactiveSecurityContextHolder.getContext() + .mapNotNull(SecurityContext::getAuthentication) + .cast(JwtAuthenticationToken.class) // ✅ 先转为 JwtAuthenticationToken + .map(AbstractOAuth2TokenAuthenticationToken::getToken) // ✅ 再获取 Jwt + .map(jwt -> { + String userId = jwt.getSubject(); // ✅ 现在可以安全调用 + if (userId == null || userId.isBlank()) { + throw new IllegalArgumentException("JWT missing 'sub' claim"); + } + ServerHttpRequest modifiedRequest = exchange.getRequest().mutate() + .header("X-User-Id", userId) + .build(); + return exchange.mutate().request(modifiedRequest).build(); + }) + .switchIfEmpty(Mono.just(exchange)) + .flatMap(chain::filter); + } +} \ No newline at end of file diff --git a/geteway/src/main/resources/application.yml b/geteway/src/main/resources/application.yml new file mode 100644 index 0000000..55b4979 --- /dev/null +++ b/geteway/src/main/resources/application.yml @@ -0,0 +1,50 @@ +server: + port: 8083 +#spring: +# application: +# name: api-gateway +# cloud: +# gateway: +# server: +# webflux: +# default-filters: +# - RewritePath=/api/(?.*)/(?.*), /$\{path} +# enabled: true +# routes: +# - id: user-service +# uri: http://a-service:8091 +# predicates: +# - Path=/api/auth/** +# security: +# oauth2: +# resourceserver: +# jwt: +# jwk-set-uri: http://auth-service:9000/oauth2/jwks +# client: +# provider: +# spring: +# issuer-uri: http://auth-service:9000 + +spring: + cloud: + gateway: + server: + webflux: + default-filters: + - RewritePath=/api/(?.*)/(?.*), /$\{path} + enabled: true + routes: + - id: user-service + uri: http://localhost:8091 + predicates: + - Path=/api/auth/** + + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: http://localhost:9000/oauth2/jwks + client: + provider: + spring: + issuer-uri: http://localhost:9000 \ No newline at end of file diff --git a/geteway/src/test/java/com/example/geteway/GatewayApplicationTests.java b/geteway/src/test/java/com/example/geteway/GatewayApplicationTests.java new file mode 100644 index 0000000..7ab4993 --- /dev/null +++ b/geteway/src/test/java/com/example/geteway/GatewayApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.geteway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GatewayApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/k8s/a-service-deployment.yaml b/k8s/a-service-deployment.yaml new file mode 100644 index 0000000..7d93efa --- /dev/null +++ b/k8s/a-service-deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: a-service-deployment + labels: + app: a-service +spec: + replicas: 1 + selector: + matchLabels: + app: a-service + template: + metadata: + labels: + app: a-service + spec: + containers: + - name: a-service + image: 192.168.1.14:5000/a-service:v10 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8091 + env: + - name: SPRING_APPLICATION_NAME + value: "re-config" # ← 可选,但推荐;也可在 ConfigMap 中设置 +--- +apiVersion: v1 +kind: Service +metadata: + name: a-service +spec: + selector: + app: a-service + ports: + - protocol: TCP + port: 8091 + targetPort: 8091 + type: LoadBalancer \ No newline at end of file diff --git a/k8s/auth-deployment.yaml b/k8s/auth-deployment.yaml new file mode 100644 index 0000000..bf0c91f --- /dev/null +++ b/k8s/auth-deployment.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-deployment + labels: + app: auth +spec: + replicas: 1 + selector: + matchLabels: + app: auth + template: + metadata: + labels: + app: auth + spec: + containers: + - name: auth + image: 192.168.1.14:5000/auth:v33 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 9000 + volumeMounts: + - name: keys-secret + mountPath: keys + readOnly: true + volumes: + - name: keys-secret + secret: + secretName: app-keys + defaultMode: 0600 +--- +apiVersion: v1 +kind: Service +metadata: + name: auth-service +spec: + selector: + app: auth + ports: + - protocol: TCP + port: 9000 + targetPort: 9000 + type: LoadBalancer \ No newline at end of file diff --git a/k8s/gateway-deployment.yaml b/k8s/gateway-deployment.yaml new file mode 100644 index 0000000..ee22de6 --- /dev/null +++ b/k8s/gateway-deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway-deployment + labels: + app: gateway +spec: + replicas: 1 + selector: + matchLabels: + app: gateway + template: + metadata: + labels: + app: gateway + spec: + containers: + - name: gateway + image: 192.168.1.14:5000/gateway:v19 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8083 +--- +apiVersion: v1 +kind: Service +metadata: + name: gateway-service +spec: + selector: + app: gateway + ports: + - protocol: TCP + port: 8083 + targetPort: 8083 + type: LoadBalancer \ No newline at end of file diff --git a/k8s/secrets.yaml b/k8s/secrets.yaml new file mode 100644 index 0000000..0408fe2 --- /dev/null +++ b/k8s/secrets.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: auth-secret +type: Opaque +data: + # 注意:这些是base64编码的值 + # root (用户名) + db-username: cm9vdA== + # lingxiao (密码) + db-password: bGluZ3hpYW8= \ No newline at end of file diff --git a/keys/oauth2-private.key b/keys/oauth2-private.key new file mode 100644 index 0000000000000000000000000000000000000000..d604b995e179c363480088d8cab97cebe89be763 GIT binary patch literal 1478 zcmZvcc{tPw7{`Az#t^w8lfowBXxs`Dh7g5u-x-M+XBp#XoS~V~DupmBIU+G0XJp*c zSZvBU$hBoHu_ecNayM&RYWI2ekA46ByvO%>|M*m`0KDNKAQ9{n>7yR*hmQb*$T8~H zeld=Ir1o7q=AB%I85DqFfkQsm148#W9*7H$Nru zbf(ALPP6MypNAJQqhiH=43&_p6^TML%wY6pvnVQ}indZk3-xmiP#y>X+5ZFpu^ssM zSfJUa(M_{&M|d!{OfB6fLU$#`MX{9SJzmMNXPRl>(9BwBmG%Xd{Z|v98Syg5sc($v z1r7-};fbn^9I?d77ak8IQ}0gq6vp$qo{)RC&HkilPT)s0rt^~e3o}^FkMm(T`mSfg zwAWc4&@5thN!i1^LPglrQhqVAqS`{$eZ9kD=C1xYsYi2o*{r&bp(CY0!K>x8u|yj@?`2HUl~U&`E}Ka?J(kUSIp#7{QAz@k`otC-MTzPOrT!@~7N*>7o2_uF30e z*(^qz;Zw=hroh~Z+` zFlsa}3mD>mB<-Cl`Sy70@&HSs;K)|WSQ`cwbo%+^C0$oArc%Dy0;O#$iL!6dY2T(7q2NWvve~q;q&zDG@D3 zXVli}JJ#chdj0EX<;?XF%Bl3zGgDWn!Fw1Z3of@)3!KiXRIEOAa=JyI)6RV>kDNlh zx^?U$rP(ECGAc=0+@WT%YvI)&V?r9Q@VZ%0*gA8;v@v6&uey8|3 zBl1=zvejq9irt-KBwEs4w{qdxFOw&t`1poZ4sQ@dw!DIGNNmY{kyWTM9z-{J7Aa^Z zCgYwo&0b4Fg|#H*^8YYMH+g?yck0d2WPv?udRf+HV@*5T%J@Sb(uaZyrdgs#Uvk&h z1Vh8{^I7}{8%M|kCI*Iz0tN;~HG?Kb zB?DeIPOUbNw(q=*jEt-d%uS5^3_x)%rY1&4hLz6YTuZe+biPeePmvK9G|r1rpL1r} z^_>^Ge{@#xPgsroVR9U*Q8~p4G+s_PG|Nh;;vb@ z?6_Cjg`UGdr#G-!SUh>OTj4qr^L&1@-@z(o+eO`)gD*5rJWz7{y$;`G+vK1{0&h3} zQuAxy?6T{rzNP)l4<1n&SGMXcZ8|f1PH)|z7uJeuB}-q%XXG7@*>b+m@9V2J=0g)I z+xz^FOnAb!aC3Q;U~9gJ;s*Y5@vZl5%^yx}&=yehlU(?8%cHGK%!~|-B@C<)dZq@J rbwv!4*rF61)C^D{1Pt3E20pMlP+aP{=9T6mO9EqrEg;m%$I}@A&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:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$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..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : 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.4 +@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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 + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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..c8b2ee2 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + pom + + + auth + geteway + a-service + + + org.springframework.boot + spring-boot-starter-parent + 4.0.2 + + + com + lingxiao + 0.0.1-SNAPSHOT + lingxiao + lingxiao + + + 25 + true + UTF-8 + UTF-8 + 25 + 25 + UTF-8 + 2025.1.1 + + + + org.springframework.boot + spring-boot-starter + + + + com.github.ben-manes.caffeine + caffeine + + + + org.springframework.cloud + spring-cloud-starter-kubernetes-fabric8-all + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud-version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + +