liyan před 4 roky
rodič
revize
e6fe17df80
29 změnil soubory, kde provedl 1464 přidání a 0 odebrání
  1. 22 0
      HELP.md
  2. 82 0
      build.gradle.kts
  3. 5 0
      gradle/wrapper/gradle-wrapper.properties
  4. 185 0
      gradlew
  5. 89 0
      gradlew.bat
  6. 1 0
      settings.gradle.kts
  7. 21 0
      src/main/kotlin/jit/xms/qrscan/QRScanAuthConfigure.kt
  8. 96 0
      src/main/kotlin/jit/xms/qrscan/QRScanConfigure.kt
  9. 46 0
      src/main/kotlin/jit/xms/qrscan/QRScanController.kt
  10. 14 0
      src/main/kotlin/jit/xms/qrscan/QrscanApplication.kt
  11. 18 0
      src/main/kotlin/jit/xms/qrscan/domain/AuthForm.kt
  12. 3 0
      src/main/kotlin/jit/xms/qrscan/domain/QRCodeResult.kt
  13. 3 0
      src/main/kotlin/jit/xms/qrscan/domain/TicketResult.kt
  14. 130 0
      src/main/kotlin/jit/xms/qrscan/service/AuthService.kt
  15. 59 0
      src/main/kotlin/jit/xms/qrscan/support/QRScanWebSocketHandler.kt
  16. 12 0
      src/main/kotlin/jit/xms/qrscan/support/WebSocketSender.kt
  17. 198 0
      src/main/resources/api/api.html
  18. 132 0
      src/main/resources/api/api.raml
  19. 22 0
      src/main/resources/application-auth.yml
  20. 16 0
      src/main/resources/application-dm.yml
  21. 36 0
      src/main/resources/application-routes.yml
  22. 9 0
      src/main/resources/application.yml
  23. 68 0
      src/main/resources/static/app.js
  24. 43 0
      src/main/resources/static/client.html
  25. 53 0
      src/main/resources/static/index.html
  26. 85 0
      src/main/resources/static/index2.html
  27. 2 0
      src/main/resources/static/js/jquery-3.5.1.min.js
  28. 1 0
      src/main/resources/static/js/qrcode.min.js
  29. 13 0
      src/test/kotlin/jit/xms/qrscan/QrscanApplicationTests.kt

+ 22 - 0
HELP.md

@@ -0,0 +1,22 @@
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Gradle documentation](https://docs.gradle.org)
+* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.4.0/gradle-plugin/reference/html/)
+* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.4.0/gradle-plugin/reference/html/#build-image)
+* [Coroutines section of the Spring Framework Documentation](https://docs.spring.io/spring/docs/5.3.1/spring-framework-reference/languages.html#coroutines)
+* [WebSocket](https://docs.spring.io/spring-boot/docs/2.4.0/reference/htmlsingle/#boot-features-websockets)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/)
+* [Using Spring Cloud Gateway](https://github.com/spring-cloud-samples/spring-cloud-gateway-sample)
+
+### Additional Links
+These additional references should also help you:
+
+* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)
+

+ 82 - 0
build.gradle.kts

@@ -0,0 +1,82 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("org.springframework.boot") version "2.3.6.RELEASE"
+    id("io.spring.dependency-management") version "1.0.10.RELEASE"
+    kotlin("jvm") version "1.4.10"
+    kotlin("plugin.spring") version "1.4.10"
+}
+
+group = "jit.xms"
+version = "1.0.1116"
+java.sourceCompatibility = JavaVersion.VERSION_11
+
+val repoConf: String = System.getProperty("repoPath") ?: "/var/repo"
+val repoPath: String = file("$rootDir").toPath().root.resolve(repoConf).toString()
+
+repositories {
+    maven {
+        name = "localRepo"
+        url = uri("file://$repoPath")
+    }
+    maven {
+        name = "cc-lotus"
+        url = uri("https://maven.cc-lotus.info/repository/maven-public/")
+    }
+    // 阿里云镜像
+    maven { url = uri("https://maven.aliyun.com/repository/public") }
+    maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
+}
+
+extra["springCloudVersion"] = "Hoxton.SR5"
+extra["gafVersion"] = "3.0.1127"
+extra["fastjsonVersion"] = "1.2.71"
+extra["xmsVersion"] = "1.0.1125"
+
+dependencies {
+    implementation("org.springframework.boot:spring-boot-starter-webflux")
+    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
+    implementation("org.jetbrains.kotlin:kotlin-reflect")
+    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
+    implementation("org.springframework.cloud:spring-cloud-starter-gateway")
+    implementation("cc-lotus.gaf3:gaf-core-shared:${property("gafVersion")}")
+    implementation("cc-lotus.gaf3:gaf-core-services:${property("gafVersion")}")
+    implementation("cc-lotus.gaf3:gaf-core-gateway:${property("gafVersion")}")
+    implementation("com.alibaba:fastjson:${property("fastjsonVersion")}")
+    implementation("jit.xms:xms-core-shared:${property("xmsVersion")}")
+    implementation("jit.xms:xms-core-services:${property("xmsVersion")}")
+    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+    implementation("io.jsonwebtoken:jjwt-api:0.10.5")
+    implementation("io.jsonwebtoken:jjwt-impl:0.10.5")
+    implementation("io.jsonwebtoken:jjwt-jackson:0.10.5")
+    implementation("org.webjars:webjars-locator-core")
+    implementation("org.webjars:sockjs-client:1.0.2")
+    implementation("org.webjars:stomp-websocket:2.3.3")
+    implementation("org.webjars:bootstrap:3.3.7")
+    implementation("org.webjars:jquery:3.1.1-1")
+    testImplementation("org.springframework.boot:spring-boot-starter-test")
+    testImplementation("io.projectreactor:reactor-test")
+    implementation(fileTree("$rootDir/libs") { include("*.jar") })
+    runtimeOnly("mysql:mysql-connector-java")
+}
+
+dependencyManagement {
+    imports {
+        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
+    }
+}
+
+tasks.withType<Test> {
+    useJUnitPlatform()
+}
+
+tasks.withType<KotlinCompile> {
+    kotlinOptions {
+        freeCompilerArgs = listOf("-Xjsr305=strict")
+        jvmTarget = "11"
+    }
+}

+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 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.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
settings.gradle.kts

@@ -0,0 +1 @@
+rootProject.name = "xms-qrscan"

+ 21 - 0
src/main/kotlin/jit/xms/qrscan/QRScanAuthConfigure.kt

@@ -0,0 +1,21 @@
+package jit.xms.qrscan
+
+import gaf3.core.util.ValidityUtil
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.validation.annotation.Validated
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Pattern
+
+@ConfigurationProperties(prefix = "qrscan.auth")
+@Validated
+class QRScanAuthConfigure {
+    @NotNull
+    var jwtSecret: String? = ""
+
+    var jwtIssuer: String? = "qrscan"
+
+    @Pattern(regexp = ValidityUtil.VALIDITY_REGEXP)
+    var jwtValidity: String? = "1h"
+
+    val jwtValiditySec: Long get() = ValidityUtil.parseValidity(jwtValidity)
+}

+ 96 - 0
src/main/kotlin/jit/xms/qrscan/QRScanConfigure.kt

@@ -0,0 +1,96 @@
+package jit.xms.qrscan
+
+import gaf3.core.gateway.filter.factory.ForwardGatewayFilterFactory
+import gaf3.core.gateway.filter.factory.SetRequestParameterGatewayFilterFactory
+import gaf3.core.gateway.handler.predicate.JwtRoutePredicateFactory
+import io.jsonwebtoken.SignatureAlgorithm
+import io.netty.handler.codec.http.cors.CorsConfig
+import jit.xms.auth.api.support.config.MultiFactorConfigure
+import jit.xms.auth.api.support.web.MultiFactorWebAdviceConfiguration
+import jit.xms.qrscan.support.QRScanWebSocketHandler
+import jit.xms.qrscan.support.WebSocketSender
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.context.annotation.Import
+import org.springframework.core.Ordered
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsWebFilter
+import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
+import org.springframework.web.reactive.HandlerMapping
+import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping
+import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter
+import java.security.Key
+import java.util.concurrent.ConcurrentHashMap
+import javax.crypto.spec.SecretKeySpec
+
+@Configuration
+@EnableConfigurationProperties(QRScanAuthConfigure::class, MultiFactorConfigure::class)
+@EntityScan(basePackages = ["jit.xms.core.services"])
+@EnableJpaRepositories(basePackages = ["jit.xms.core.services"])
+@Import(MultiFactorWebAdviceConfiguration::class)
+@ComponentScan(basePackages = ["jit.xms.core.services", "gaf3.core.services"], basePackageClasses = [QRScanConfigure::class])
+class QRScanConfigure {
+
+    @Bean
+    fun handlerMapping(webSocketHandler: QRScanWebSocketHandler): HandlerMapping {
+        val map = mapOf("/qrscan/auth" to webSocketHandler)
+        val order = Ordered.HIGHEST_PRECEDENCE
+        return SimpleUrlHandlerMapping(map, order)
+    }
+
+    @Bean
+    fun handlerAdapter() = WebSocketHandlerAdapter()
+
+    @Bean
+    fun senderMap(): ConcurrentHashMap<String, WebSocketSender> {
+        return ConcurrentHashMap<String, WebSocketSender>()
+    }
+
+    @Bean
+    @Qualifier("jwtSigningKey")
+    fun jwtSigningKey(config: QRScanAuthConfigure): Key {
+        val secretKeyBytes = config.jwtSecret?.toByteArray()?.copyOf(32)
+        val alg: SignatureAlgorithm = SignatureAlgorithm.HS256
+        return SecretKeySpec(secretKeyBytes, alg.jcaName)
+    }
+
+    @Bean
+    fun jwtRoutePredicateFactory(): JwtRoutePredicateFactory {
+        return JwtRoutePredicateFactory()
+    }
+
+    @Bean
+    fun setRequestParameterGatewayFilterFactory(): SetRequestParameterGatewayFilterFactory {
+        return SetRequestParameterGatewayFilterFactory()
+    }
+
+    @Bean
+    fun forwardGatewayFilterFactory(): ForwardGatewayFilterFactory {
+        return ForwardGatewayFilterFactory()
+    }
+
+    /**
+     * 添加跨域处理
+     */
+    @Bean
+    fun corsWebFilter(): CorsWebFilter {
+        val config = CorsConfiguration()
+        config.addAllowedOrigin("*")
+        config.addAllowedHeader("*")
+        config.addAllowedMethod("*")
+        val source = UrlBasedCorsConfigurationSource()
+        source.registerCorsConfiguration("/**", config)
+        return CorsWebFilter(source)
+    }
+
+    companion object {
+        val logger = LoggerFactory.getLogger(QRScanConfigure::class.java)
+    }
+
+}

+ 46 - 0
src/main/kotlin/jit/xms/qrscan/QRScanController.kt

@@ -0,0 +1,46 @@
+package jit.xms.qrscan
+
+import gaf3.core.data.ErrorResult
+import jit.xms.core.services.agent.domain.AuthToken
+import jit.xms.qrscan.domain.AuthForm
+import jit.xms.qrscan.domain.QRCodeResult
+import jit.xms.qrscan.service.AuthService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.util.Assert
+import org.springframework.web.bind.annotation.*
+import java.util.*
+
+@RestController
+@RequestMapping(path = ["/xms/qrscan"])
+@CrossOrigin(origins = ["*"], maxAge = 3600)
+class QRScanController {
+
+    @Autowired
+    lateinit var authService: AuthService
+
+
+    /**
+     * 生成唯一二维码标识
+     */
+    @GetMapping(path = ["/uuid"])
+    fun getUUID(): QRCodeResult {
+        return QRCodeResult(UUID.randomUUID().toString())
+    }
+
+    /**
+     * 解析扫码客户端发送Token,并向浏览器发送Ticket
+     */
+    @PostMapping(path = ["/ticket"])
+    fun ticket(id: String?, userId: String?, @RequestHeader("Authorization") token: String?): ErrorResult {
+        Assert.hasText(id, "ID不可为空")
+        Assert.hasText(userId, "用户ID不可为空")
+        authService.ticket(id!!, userId!!, token!!)
+        return ErrorResult(0, "ok")
+    }
+
+    @PostMapping(path = ["/auth"])
+    fun auth(@RequestBody form: AuthForm): AuthToken {
+        return authService.auth(form)
+    }
+
+}

+ 14 - 0
src/main/kotlin/jit/xms/qrscan/QrscanApplication.kt

@@ -0,0 +1,14 @@
+package jit.xms.qrscan
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Import
+
+@SpringBootApplication
+@Import(QRScanConfigure::class)
+class QrscanApplication
+
+fun main(args: Array<String>) {
+    runApplication<QrscanApplication>(*args)
+}

+ 18 - 0
src/main/kotlin/jit/xms/qrscan/domain/AuthForm.kt

@@ -0,0 +1,18 @@
+package jit.xms.qrscan.domain
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import javax.validation.constraints.NotEmpty
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+open class AuthForm {
+    /**
+     * 凭证类型
+     */
+    @NotEmpty
+    var type: String? = null
+    /**
+     * 凭证数据
+     */
+    @NotEmpty
+    var data: String? = null
+}

+ 3 - 0
src/main/kotlin/jit/xms/qrscan/domain/QRCodeResult.kt

@@ -0,0 +1,3 @@
+package jit.xms.qrscan.domain
+
+class QRCodeResult(val uuid: String)

+ 3 - 0
src/main/kotlin/jit/xms/qrscan/domain/TicketResult.kt

@@ -0,0 +1,3 @@
+package jit.xms.qrscan.domain
+
+class TicketResult(val errCode: Int, val errMsg: String?, val ticket: String)

+ 130 - 0
src/main/kotlin/jit/xms/qrscan/service/AuthService.kt

@@ -0,0 +1,130 @@
+package jit.xms.qrscan.service
+
+import com.alibaba.fastjson.JSON
+import gaf3.core.exception.BusinessError
+import gaf3.core.exception.BusinessError.ERR_BUSINESS
+import gaf3.core.services.auth.service.GafAuthService
+import io.jsonwebtoken.Claims
+import io.jsonwebtoken.ExpiredJwtException
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+import jit.xms.auth.api.domain.AuthInfo
+import jit.xms.auth.api.support.config.MultiFactorConfigure
+import jit.xms.core.services.agent.domain.AuthToken
+import jit.xms.core.services.app.infos.service.AppInfoService
+import jit.xms.core.services.bff.service.BffAcctRoleService
+import jit.xms.core.services.user.accts.service.UserAcctService
+import jit.xms.core.services.user.infos.entity.XmsUserInfo
+import jit.xms.core.services.user.infos.entity.XmsUserSimple
+import jit.xms.core.services.user.infos.service.UserInfoService
+import jit.xms.qrscan.QRScanAuthConfigure
+import jit.xms.qrscan.domain.AuthForm
+import jit.xms.qrscan.domain.TicketResult
+import jit.xms.qrscan.support.WebSocketSender
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Qualifier
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+import java.security.Key
+import java.time.Instant
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import javax.crypto.spec.SecretKeySpec
+
+@Service
+class AuthService(@Qualifier("jwtSigningKey") val jwtSigningKey: Key, val authConfig: QRScanAuthConfigure) {
+
+    @Autowired
+    lateinit var senderMap: ConcurrentHashMap<String, WebSocketSender>
+
+    @Autowired
+    lateinit var multiFactorConfigure: MultiFactorConfigure
+
+    @Autowired
+    lateinit var gafAuth: GafAuthService
+
+    @Value("\${jwt.secret}")
+    lateinit var gafSecretKey: String
+
+    /**
+     * 生成Ticket并发送WebSocketMessage
+     */
+    fun ticket(id: String, userId: String, token: String) {
+        val webSocketSender = senderMap[id] ?: throw BusinessError(ERR_BUSINESS, "该二维码所属客户端不存在")
+        val realToken = token.removePrefix("Bearer ")
+        val gafKeyBytes = gafSecretKey.toByteArray().copyOf(32)
+        val gafKey = SecretKeySpec(gafKeyBytes, SignatureAlgorithm.HS256.jcaName)
+        val claims = parseJwt(realToken, gafKey)
+        val authResult = TicketResult(0, "认证成功", ticket = createTicket(claims))
+        webSocketSender.send(JSON.toJSONString(authResult))
+    }
+
+    /**
+     * 对本服务签发的Ticket进行认证
+     */
+    fun auth(form: AuthForm): AuthToken {
+        if (form.type.isNullOrEmpty() || !form.type.equals("qrcode", ignoreCase = true)) throw BusinessError(ERR_BUSINESS, "凭证类型不合法")
+        if (form.data.isNullOrEmpty()) throw BusinessError(ERR_BUSINESS, "凭证数据不合法")
+        val provider = runCatching {
+            multiFactorConfigure.getProvider(form.type!!)
+        }.getOrElse {
+            throw BusinessError(ERR_PROV_NOT_SUPPORT, "凭证类型不支持")
+        }
+        val result = kotlin.runCatching {
+            provider.auth(null, form.data, null)
+        }.getOrElse {
+            throw BusinessError(ERR_CRED_NOT_MATCH, "认证接口调用失败")
+        }
+        if (!result) throw BusinessError(ERR_CRED_NOT_MATCH, "凭证数据校验失败")
+        // 解析ticket数据
+        val parse = parseJwt(form.data!!, jwtSigningKey)
+        val userId = parse["userId"] as String? ?: throw BusinessError("解析Ticket中用户信息为空")
+        val roles = parse["roles"] as ArrayList<String>
+        val user = XmsUserSimple().apply {
+            this.userId = userId
+            name = parse["name"] as String? ?: "用户"
+        }
+        val gafToken = gafAuth.createJwt(subject = "admin",
+                userId = user.userId!!,
+                name = user.name!!,
+                roles = roles.toTypedArray())
+        return AuthToken(userinfo = user, token = gafToken, roles = roles.toTypedArray())
+    }
+
+
+    /**
+     * 构建Ticket
+     */
+    fun createTicket(claims: Claims): String {
+        return Jwts.builder()
+                .setSubject("qrscan")
+                .setIssuer(authConfig.jwtIssuer)
+                .setExpiration(Date.from(Instant.now().plusSeconds(authConfig.jwtValiditySec)))
+                .claim("userId", claims["userId"] as String?)
+                .claim("name", claims["name"] as String?)
+                .claim("roles", claims["roles"] as ArrayList<String>)
+                .signWith(jwtSigningKey)
+                .compact()
+    }
+
+    /**
+     * 解析Jwt
+     */
+    fun parseJwt(jwt: String, key: Key): Claims {
+        try {
+            val jws = Jwts.parser().setSigningKey(key).parseClaimsJws(jwt)
+            return jws.body
+        } catch (ex: ExpiredJwtException) {
+            throw BusinessError(ERR_TICKET_EXPIRED, "Ticket已过期")
+        } catch (ex: Throwable) {
+            throw BusinessError(ERR_TICKET_INVALID, "Ticket无效")
+        }
+    }
+
+    companion object {
+        const val ERR_CRED_NOT_MATCH = ERR_BUSINESS - 1
+        const val ERR_TICKET_EXPIRED = ERR_BUSINESS - 6
+        const val ERR_TICKET_INVALID = ERR_BUSINESS - 7
+        const val ERR_PROV_NOT_SUPPORT = ERR_BUSINESS - 8
+    }
+}

+ 59 - 0
src/main/kotlin/jit/xms/qrscan/support/QRScanWebSocketHandler.kt

@@ -0,0 +1,59 @@
+package jit.xms.qrscan.support
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Component
+import org.springframework.util.StringUtils
+import org.springframework.web.cors.CorsConfiguration
+import org.springframework.web.cors.reactive.CorsConfigurationSource
+import org.springframework.web.reactive.socket.WebSocketHandler
+import org.springframework.web.reactive.socket.WebSocketMessage
+import org.springframework.web.reactive.socket.WebSocketSession
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Flux
+import reactor.core.publisher.FluxSink
+import reactor.core.publisher.Mono
+import java.util.concurrent.ConcurrentHashMap
+
+
+@Component
+class QRScanWebSocketHandler : WebSocketHandler, CorsConfigurationSource {
+
+    @Autowired
+    lateinit var senderMap: ConcurrentHashMap<String, WebSocketSender>
+
+    override fun handle(session: WebSocketSession): Mono<Void> {
+
+        val handshakeInfo = session.handshakeInfo
+        val queryMap = getQueryMap(handshakeInfo.uri.query)
+        val id = queryMap.getOrDefault("id", defaultValue = defaultId)
+        val input = session.receive().map(WebSocketMessage::getPayloadAsText).map { msg -> "$id: $msg" }
+                .doOnNext(System.out::println).then()
+        val output = session.send(Flux.create { sink: FluxSink<WebSocketMessage> -> senderMap[id] = WebSocketSender(session, sink) })
+        return Mono.zip(input, output).then()
+    }
+
+    override fun getCorsConfiguration(exchange: ServerWebExchange): CorsConfiguration? {
+        return CorsConfiguration().apply {
+            this.addAllowedOrigin("*")
+            this.maxAge = 3600
+        }
+    }
+
+    private fun getQueryMap(query: String): Map<String, String> {
+        val result = mutableMapOf<String, String>()
+        if (!StringUtils.isEmpty(query)) {
+            val queryParams = query.split("&")
+            queryParams.forEach {
+                val queryParam = it.split("=")
+                if (queryParam.size == 2) {
+                    result[queryParam[0]] = queryParam[1]
+                }
+            }
+        }
+        return result
+    }
+
+    companion object {
+        private val defaultId = "0"
+    }
+}

+ 12 - 0
src/main/kotlin/jit/xms/qrscan/support/WebSocketSender.kt

@@ -0,0 +1,12 @@
+package jit.xms.qrscan.support
+
+import org.springframework.web.reactive.socket.WebSocketMessage
+import org.springframework.web.reactive.socket.WebSocketSession
+import reactor.core.publisher.FluxSink
+
+class WebSocketSender(val session: WebSocketSession, val sink: FluxSink<WebSocketMessage>) {
+
+    fun send(data: String) {
+        sink.next(session.textMessage(data))
+    }
+}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 198 - 0
src/main/resources/api/api.html


+ 132 - 0
src/main/resources/api/api.raml

@@ -0,0 +1,132 @@
+#%RAML 1.0
+---
+title: xms-qrscan 接口API
+version: v1
+baseUri: http://localhost:18090/api
+mediaType:  application/json
+types:
+  ErrorResult:
+    type: object
+    description: 带错误信息的返回数据
+    properties:
+      errcode:
+        description: 错误代码
+        type: number
+      errmsg?:
+        description: 错误描述
+      details?:
+        description: 错误详情
+    example: {"errcode": -1,"errmsg": "系统错误","details": "错误描述"}
+  SuccessResult:
+    type: object
+    description: 请求方法处理成功
+    properties:
+      errcode:
+        type: number
+        description: 错误码
+      errmsg:
+        type: string
+        description: 描述信息
+    example: { "errcode": 0, "errmsg": "ok" }
+  QRCodeResult:
+    type: object
+    description: 获取uuid返回结果
+    properties:
+      uuid:
+        type: string
+        description: 唯一标识
+    example: { "uuid": "5f135f01-33ee-4cdb-8cd6-d95a051cdf27" }
+  AuthForm:
+    type: object
+    description: 认证参数
+    properties:
+      type:
+        type: string
+        description: 凭证类型,固定为qrcode
+      data:
+        type: string
+        description: 登录页面获取到的ticket
+  AuthToken:
+    type: object
+    description: 认证结果
+    properties:
+      roles:
+        type: string[]
+        description: 用户角色列表
+      token:
+        type: string
+        description: 用户认证token
+      userinfo:
+        type: XmsUserSimple
+        description: 用户信息
+    example: {
+                 "userinfo": {
+                     "userId": "00000000-0000-0000-0000-000000000000",
+                     "name": "系统管理员"
+                 },
+                 "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6ImdhZiIsImV4cCI6MTYwODYxMzg2MCwidXNlcklkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwibmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsInJvbGVzIjpbInN1cGVyYWRtaW4iXX0.YdnTX2YrvsXEyzs6zau03OiuD1wHEAdJ4BnPtV1bXCk",
+                 "roles": [
+                     "superadmin"
+                 ]
+             }
+  XmsUserSimple:
+    type: object
+    description: 用户信息
+    properties:
+      userId:
+        type: string
+        description: 用户Id
+      name:
+        type: string
+        description: 用户名
+resourceTypes:
+traits:
+  secured:
+    queryParameters:
+      access_token:
+        description: 接口调用凭据
+/xms/qrscan:
+  description: 二维码扫描认证接口
+  /uuid:
+    get:
+      description: 网页端登录页面获取唯一标识
+      responses:
+        200:
+          body:
+            type: QRCodeResult
+        400:
+          description: 处理失败
+          body:
+            type: ErrorResult
+  /ticket:
+    post:
+      description: 扫码客户端发送登录请求,通知登录页面获取ticket
+      headers:
+        Authorization:
+          type: string
+          description: 扫码客户端账号口令登录获取的token
+      queryParameters:
+        id:
+          type: string
+          description: 登录页面获取的唯一标识
+      responses:
+        200:
+          body:
+            type: SuccessResult
+        400:
+          body:
+            type: ErrorResult
+  /auth:
+    post:
+      description: 登录页面将获取的ticket进行认证
+      body:
+        application/json:
+          type: AuthForm
+      responses:
+        200:
+          body:
+            type: AuthToken
+        400:
+          body:
+           type: ErrorResult
+

+ 22 - 0
src/main/resources/application-auth.yml

@@ -0,0 +1,22 @@
+# 全局配置
+jwt.secret: "GafJwtSecret!@#"
+
+# 二维码认证相关配置
+qrscan:
+  auth:
+    jwt-secret: "QRScanJwtSecret!@#"
+    jwt-issuer: xms-qrscan
+    jwt-validity: 1m
+
+# 认证比对相关配置
+xms:
+  auth:
+    multi-factor:
+      singleton: true
+      providers:
+        qrcode:
+          name: 二维码认证
+          provider-class-name: jit.xms.auth.qrcode.server.QRCodeProvider
+        extra:
+          key: ${qrscan.auth.jwt-secret}
+          subject: qrscan

+ 16 - 0
src/main/resources/application-dm.yml

@@ -0,0 +1,16 @@
+# 数据库配置
+---
+spring:
+  datasource:
+    username: XMS
+    password: XMS1234567890
+    url: jdbc:dm://192.168.65.123:5236/XMS
+    driver-class-name: dm.jdbc.driver.DmDriver
+  jpa:
+    database-platform: org.hibernate.dialect.XmsDmDialect
+    show-sql: true
+    hibernate:
+      naming:
+        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
+      ddl-auto: none

+ 36 - 0
src/main/resources/application-routes.yml

@@ -0,0 +1,36 @@
+# 路由设置
+spring:
+  profiles: routes
+  cloud:
+    gateway:
+      default-filters:
+        - AddResponseHeader=Cache-Control, no-cache
+        - AddResponseHeader=Pragma, no-cache
+        - AddResponseHeader=Expires, -1
+      routes:
+        # == 客户端认证处理 ==
+        - id: Auth_Ticket
+          uri: http://${api.host}:8001
+          predicates:
+            - Path=/api/xms/qrscan/ticket
+            - Jwt=issuer, gaf
+          filters:
+            - RewritePath=/api/(?<segment>.*), /$\{segment}
+            - SetRequestParameter=userId, {jwt:userId}
+            - Forward
+        # == 扫码服务处理 ==
+        - id: QRCode_Scan_Services
+          uri: http://${api.host}:8001
+          predicates:
+            - Path=/api/xms/qrscan/**
+          filters:
+            - RewritePath=/api/xms/(?<segment>.*), /xms/$\{segment}
+            - Forward
+        # == 默认处理 ==
+        - id: api_default
+          uri: forward:///401 # default for unauthorized
+          order: 1000
+          predicates:
+            - Path=/api/**
+          filters:
+            - SetStatus=401

+ 9 - 0
src/main/resources/application.yml

@@ -0,0 +1,9 @@
+server:
+  port: 18090
+logging.level.jit.xms.*: debug
+spring:
+  profiles:
+    include: routes,auth,dm
+  main:
+    allow-bean-definition-overriding: true
+api.host: 127.0.0.1

+ 68 - 0
src/main/resources/static/app.js

@@ -0,0 +1,68 @@
+var stompClient = null;
+
+function setConnected(connected) {
+    $("#connect").prop("disabled", connected);
+    $("#disconnect").prop("disabled", !connected);
+    if (connected) {
+        $("#conversation").show();
+    } else {
+        $("#conversation").hide();
+    }
+    $("#greetings").html("");
+}
+
+function connect() {
+    var socket = new SockJS('/qrscan/auth');
+    stompClient = Stomp.over(socket);
+    stompClient.connect({}, function (frame) {
+        setConnected(true);
+        console.log('Connected: ' + frame);
+        stompClient.subscribe('/auth/ticket', function (greeting) {
+            showGreeting(JSON.parse(greeting.body).ticket);
+        });
+    });
+}
+
+function disconnect() {
+    if (stompClient !== null) {
+        stompClient.disconnect();
+    }
+    setConnected(false);
+    console.log("Disconnected");
+}
+
+function sendName() {
+    // stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
+    stompClient.send("/app/login", {}, JSON.stringify({'name': $("#name").val(), 'userId': getQueryVariable("code")}));
+}
+
+function showGreeting(message) {
+    $("#greetings").append("<tr><td>" + message + "</td></tr>");
+}
+
+$(function () {
+    $("form").on('submit', function (e) {
+        e.preventDefault();
+    });
+    $("#connect").click(function () {
+        connect();
+    });
+    $("#disconnect").click(function () {
+        disconnect();
+    });
+    $("#send").click(function () {
+        sendName();
+    });
+});
+
+function getQueryVariable(variable) {
+    var query = window.location.search.substring(1);
+    var vars = query.split("&");
+    for (var i = 0; i < vars.length; i++) {
+        var pair = vars[i].split("=");
+        if (pair[0] == variable) {
+            return pair[1];
+        }
+    }
+    return (false);
+}

+ 43 - 0
src/main/resources/static/client.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <script src="js/jquery-3.5.1.min.js"></script>
+    <script src="js/qrcode.min.js"></script>
+</head>
+<body>
+token:<input type="text" id="msg"><input type="button" id="sendBtn" onclick="send()" value="发送">
+<script type="application/javascript">
+    const msgBox = document.getElementById("msg")
+
+    //调用后台生成Ticket请求
+    function send() {
+        let msg = msgBox.value
+        $.ajax({
+            url: "http://192.168.0.65:18090/api/xms/qrscan/ticket?id="+getQueryVariable("id"),
+            method: "post",
+            dataType: "json",
+            async: false,
+            beforeSend: function (XMLHttpRequest) {
+                XMLHttpRequest.setRequestHeader("Authorization", msg);
+            }
+        })
+    }
+
+    //获取查询参数
+    function getQueryVariable(variable) {
+        var query = window.location.search.substring(1);
+        var vars = query.split("&");
+        for (var i = 0; i < vars.length; i++) {
+            var pair = vars[i].split("=");
+            if (pair[0] == variable) {
+                return pair[1];
+            }
+        }
+        return (false);
+    }
+
+</script>
+</body>
+</html>

+ 53 - 0
src/main/resources/static/index.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Hello WebSocket</title>
+    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link href="/main.css" rel="stylesheet">
+    <script src="/webjars/jquery/jquery.min.js"></script>
+    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
+    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
+    <script src="/app.js"></script>
+</head>
+<body>
+<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
+    enabled. Please enable
+    Javascript and reload this page!</h2></noscript>
+<div id="main-content" class="container">
+    <div class="row">
+        <div class="col-md-6">
+            <form class="form-inline">
+                <div class="form-group">
+                    <label for="connect">WebSocket connection:</label>
+                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
+                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
+                    </button>
+                </div>
+            </form>
+        </div>
+        <div class="col-md-6">
+            <form class="form-inline">
+                <div class="form-group">
+                    <label for="name">What is your name?</label>
+                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
+                </div>
+                <button id="send" class="btn btn-default" type="submit">Send</button>
+            </form>
+        </div>
+    </div>
+    <div class="row">
+        <div class="col-md-12">
+            <table id="conversation" class="table table-striped">
+                <thead>
+                <tr>
+                    <th>Greetings</th>
+                </tr>
+                </thead>
+                <tbody id="greetings">
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 85 - 0
src/main/resources/static/index2.html

@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <script src="js/jquery-3.5.1.min.js"></script>
+    <script src="js/qrcode.min.js"></script>
+</head>
+<body>
+<h4>websocket接收消息:</h4>
+<textarea id="msgBoxs"></textarea><br>
+<!--待发送消息:<input type="text" id="msg"><input type="button" id="sendBtn" onclick="send()" value="发送">-->
+<div id="qrcode"></div>
+<script type="application/javascript">
+    $().ready(getuuid());
+    const msgBoxs = document.getElementById("msgBoxs");
+    const msgBox = document.getElementById("msg")
+    // ws.onclose = function (evt) {
+    //     console.log("Connect closed.");
+    // }
+
+    // function send() {
+    //     let msg = msgBox.value
+    //     ws.send(msg)
+    //     msgBox.value = ""
+    // }
+
+    //获取uuid,在获取ticket后,执行认证流程
+    function getuuid() {
+        console.log("获取uuid...")
+        $.ajax({
+            url: "http://192.168.0.65:18090/api/xms/qrscan/uuid",
+            dataType: "json",
+            async: false,
+            success: function (data) {
+                new QRCode(document.getElementById("qrcode"), "http://192.168.0.65:18090/client.html?id=" + data.uuid);
+                let wsUrl = "ws://192.168.0.65:18090/qrscan/auth?id=" + data.uuid
+                let ws = new WebSocket(wsUrl)
+                ws.onopen = function (evt) {
+                    console.log("WebSocket Connection open ...");
+                    ws.send("connected!");
+                }
+
+                ws.onmessage = function (evt) {
+                    // console.log("Received Message: ", evt.data)
+                    var msgs = msgBoxs.value
+                    msgBoxs.innerText = msgs + "\n" + evt.data
+                    msgBoxs.scrollTop = msgBoxs.scrollHeight;
+                    let parse = JSON.parse(evt.data)
+                    console.log("WebSocket Received:")
+                    console.log(evt.data)
+                    console.log("获取ticket成功!")
+                    auth(parse)
+                }
+            }
+        })
+    }
+
+    //调用后台认证方法,获取系统登录Token
+    function auth(parse) {
+        console.log("调用认证接口对Ticket进行认证...")
+        let authForm = {
+            type: "qrcode",
+            data: parse.ticket
+        }
+        console.log("data:" + JSON.stringify(authForm))
+        $.ajax({
+            url: "http://127.0.0.1:18090/api/xms/qrscan/auth",
+            method: "post",
+            contentType: "application/json",
+            data: JSON.stringify(authForm),
+            dataType: "json",
+            async: false,
+            success: function (data) {
+                console.log("认证结果:")
+                console.log(JSON.stringify(data))
+                // console.log(data)
+            }
+        })
+    }
+
+
+</script>
+</body>
+</html>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2 - 0
src/main/resources/static/js/jquery-3.5.1.min.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
src/main/resources/static/js/qrcode.min.js


+ 13 - 0
src/test/kotlin/jit/xms/qrscan/QrscanApplicationTests.kt

@@ -0,0 +1,13 @@
+package jit.xms.qrscan
+
+import org.junit.jupiter.api.Test
+import org.springframework.boot.test.context.SpringBootTest
+
+@SpringBootTest
+class QrscanApplicationTests {
+
+    @Test
+    fun contextLoads() {
+    }
+
+}