Pārlūkot izejas kodu

二维码跳转登录

orangebabu 8 mēneši atpakaļ
vecāks
revīzija
bab15aee23
18 mainītis faili ar 870 papildinājumiem un 163 dzēšanām
  1. 42 0
      maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/QrCodeCredentialDto.java
  2. 33 0
      maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/ScanCode.java
  3. 10 10
      maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/session/SessionManager.java
  4. 31 22
      maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/web/AuthorizationUtils.java
  5. 49 45
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/AbstractAuthenticationProvider.java
  6. 88 0
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/impl/ScanCodeAuthenticationProvider.java
  7. 106 0
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeService.java
  8. 53 0
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeState.java
  9. 19 0
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScancodeSignInfo.java
  10. 28 14
      maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/autoconfigure/AuthnProviderAutoConfiguration.java
  11. 23 0
      maxkey-common/src/main/java/org/dromara/maxkey/adapter/LocalDateTimeAdapter.java
  12. 37 0
      maxkey-common/src/main/java/org/dromara/maxkey/util/TimeJsonUtils.java
  13. 47 0
      maxkey-core/src/main/java/org/dromara/maxkey/exception/BusinessException.java
  14. 198 0
      maxkey-core/src/main/java/org/dromara/maxkey/web/GlobalExceptionHandler.java
  15. 0 63
      maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java
  16. 42 2
      maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts
  17. 5 1
      maxkey-web-frontend/maxkey-web-app/src/app/service/QrCode.service.ts
  18. 59 6
      maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java

+ 42 - 0
maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/QrCodeCredentialDto.java

@@ -0,0 +1,42 @@
+package org.dromara.maxkey.authn;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 AM10:54
+ */
+
+
+public class QrCodeCredentialDto {
+
+
+    @NotEmpty(message = "jwtToken不能为空")
+    @Schema(name = "jwtToken", description = "token")
+    String jwtToken;
+
+    @NotEmpty(message = "返回码不能为空")
+    @Schema(name = "code", description = "返回码")
+    String code;
+
+    public @NotEmpty(message = "jwtToken不能为空") String getJwtToken() {
+        return jwtToken;
+    }
+
+    public void setJwtToken(@NotEmpty(message = "jwtToken不能为空") String jwtToken) {
+        this.jwtToken = jwtToken;
+    }
+
+    public @NotEmpty(message = "返回码不能为空") String getCode() {
+        return code;
+    }
+
+    public void setCode(@NotEmpty(message = "返回码不能为空") String code) {
+        this.code = code;
+    }
+}

+ 33 - 0
maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/ScanCode.java

@@ -0,0 +1,33 @@
+package org.dromara.maxkey.authn;
+
+import jakarta.validation.constraints.NotEmpty;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM4:28
+ */
+public class ScanCode {
+
+    @NotEmpty(message = "二维码内容不能为空")
+    String code;
+
+    @NotEmpty(message = "登录方式不能为空")
+    String authType;
+
+    public @NotEmpty(message = "二维码内容不能为空") String getCode() {
+        return code;
+    }
+
+    public void setCode(@NotEmpty(message = "二维码内容不能为空") String code) {
+        this.code = code;
+    }
+
+    public @NotEmpty(message = "登录方式不能为空") String getAuthType() {
+        return authType;
+    }
+
+    public void setAuthType(@NotEmpty(message = "登录方式不能为空") String authType) {
+        this.authType = authType;
+    }
+}

+ 10 - 10
maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/session/SessionManager.java

@@ -1,19 +1,19 @@
 /*
  * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
- * 
+ *
  * 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
- * 
+ *
  *     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.
  */
- 
+
 
 package org.dromara.maxkey.authn.session;
 
@@ -27,16 +27,16 @@ public interface SessionManager {
 	public  void create(String sessionId, Session session);
 
     public  Session remove(String sessionId);
-    
+
     public  Session get(String sessionId);
-    
+
     public Session refresh(String sessionId ,LocalDateTime refreshTime);
-    
+
     public Session refresh(String sessionId);
-    
+
     public List<HistoryLogin> querySessions();
-    
+
     public int getValiditySeconds();
-    
+
     public void terminate(String sessionId,String userId,String username);
 }

+ 31 - 22
maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/web/AuthorizationUtils.java

@@ -1,25 +1,26 @@
 /*
  * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
- * 
+ *
  * 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
- * 
+ *
  *     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.
  */
- 
+
 
 package org.dromara.maxkey.authn.web;
 
 import java.text.ParseException;
 
 
+import com.nimbusds.jwt.SignedJWT;
 import org.dromara.maxkey.authn.SignPrincipal;
 import org.dromara.maxkey.authn.jwt.AuthTokenService;
 import org.dromara.maxkey.authn.session.Session;
@@ -37,14 +38,14 @@ import jakarta.servlet.http.HttpServletRequest;
 
 public class AuthorizationUtils {
 	private static final Logger _logger = LoggerFactory.getLogger(AuthorizationUtils.class);
-	
+
 	public static final class BEARERTYPE{
-		
+
 		public static final String CONGRESS 		= "congress";
-		
+
 		public static final String AUTHORIZATION 	= "Authorization";
 	}
-	
+
 	public static  void authenticateWithCookie(
 			HttpServletRequest request,
 			AuthTokenService authTokenService,
@@ -55,12 +56,12 @@ public class AuthorizationUtils {
 	    	String  authorization =  authCookie.getValue();
 	    	_logger.trace("Try congress authenticate .");
 	    	doJwtAuthenticate(BEARERTYPE.CONGRESS,authorization,authTokenService,sessionManager);
-		}else {			
+		}else {
 			_logger.debug("cookie is null , clear authentication .");
 			clearAuthentication();
 		}
 	}
-	
+
 	public static  void authenticate(
 			HttpServletRequest request,
 			AuthTokenService authTokenService,
@@ -71,9 +72,9 @@ public class AuthorizationUtils {
 			_logger.trace("Try Authorization authenticate .");
 			doJwtAuthenticate(BEARERTYPE.AUTHORIZATION,authorization,authTokenService,sessionManager);
 		}
-		 
+
 	}
-	
+
 	public static void doJwtAuthenticate(
 			String  bearerType,
 			String  authorization,
@@ -99,17 +100,25 @@ public class AuthorizationUtils {
 		}
 	}
 
-    
+	public static Session getSession(SessionManager sessionManager, String authorization) throws ParseException {
+		_logger.debug("get session by authorization {}", authorization);
+		SignedJWT signedJWT = SignedJWT.parse(authorization);
+		String sessionId = signedJWT.getJWTClaimsSet().getJWTID();
+		_logger.debug("sessionId {}", sessionId);
+		return sessionManager.get(sessionId);
+	}
+
+
     public static Authentication getAuthentication() {
     	Authentication authentication = (Authentication) getAuthentication(WebContext.getRequest());
         return authentication;
     }
-    
+
     public static Authentication getAuthentication(HttpServletRequest request) {
     	Authentication authentication = (Authentication) request.getSession().getAttribute(WebConstants.AUTHENTICATION);
         return authentication;
     }
-    
+
     //set Authentication to http session
     public static void setAuthentication(Authentication authentication) {
     	WebContext.setAttribute(WebConstants.AUTHENTICATION, authentication);
@@ -118,24 +127,24 @@ public class AuthorizationUtils {
     public static void clearAuthentication() {
     	WebContext.removeAttribute(WebConstants.AUTHENTICATION);
     }
-    
+
     public static  boolean isAuthenticated() {
     	return getAuthentication() != null;
     }
-    
+
     public static  boolean isNotAuthenticated() {
     	return ! isAuthenticated();
     }
-    
+
     public static SignPrincipal getPrincipal() {
     	 Authentication authentication =  getAuthentication();
     	return getPrincipal(authentication);
     }
-    
+
     public static SignPrincipal getPrincipal(Authentication authentication) {
     	return authentication == null ? null : (SignPrincipal) authentication.getPrincipal();
    }
-    
+
     public static UserInfo getUserInfo(Authentication authentication) {
     	UserInfo userInfo = null;
     	SignPrincipal principal = getPrincipal(authentication);
@@ -144,9 +153,9 @@ public class AuthorizationUtils {
         }
     	return userInfo;
     }
-    
+
     public static UserInfo getUserInfo() {
     	return getUserInfo(getAuthentication());
     }
-	
+
 }

+ 49 - 45
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/AbstractAuthenticationProvider.java

@@ -1,19 +1,19 @@
 /*
  * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
- * 
+ *
  * 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
- * 
+ *
  *     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.
  */
- 
+
 
 package org.dromara.maxkey.authn.provider;
 
@@ -45,37 +45,41 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 /**
  * login Authentication abstract class.
- * 
+ *
  * @author Crystal.Sea
  *
  */
 public abstract class AbstractAuthenticationProvider {
-    private static final Logger _logger = 
+    private static final Logger _logger =
             LoggerFactory.getLogger(AbstractAuthenticationProvider.class);
 
     public static String PROVIDER_SUFFIX = "AuthenticationProvider";
-    
+
     public class AuthType{
     	public static final  String NORMAL 	= "normal";
     	public static final  String TFA 		= "tfa";
     	public static final  String MOBILE 	= "mobile";
     	public static final  String TRUSTED 	= "trusted";
+        /**
+         * 扫描认证
+         */
+        public static final  String SCAN_CODE 	= "scancode";
     }
-    
+
     protected ApplicationConfig applicationConfig;
 
     protected AbstractAuthenticationRealm authenticationRealm;
 
     protected AbstractOtpAuthn tfaOtpAuthn;
-    
+
     protected MailOtpAuthnService otpAuthnService;
 
     protected SessionManager sessionManager;
-    
+
     protected AuthTokenService authTokenService;
-    
+
     public static  ArrayList<GrantedAuthority> grantedAdministratorsAuthoritys = new ArrayList<GrantedAuthority>();
-    
+
     static {
         grantedAdministratorsAuthoritys.add(new SimpleGrantedAuthority("ROLE_ADMINISTRATORS"));
     }
@@ -83,7 +87,7 @@ public abstract class AbstractAuthenticationProvider {
     public abstract String getProviderName();
 
     public abstract Authentication doAuthenticate(LoginCredential authentication);
-    
+
     @SuppressWarnings("rawtypes")
     public boolean supports(Class authentication) {
         return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
@@ -92,13 +96,13 @@ public abstract class AbstractAuthenticationProvider {
     public Authentication authenticate(LoginCredential authentication){
     	return null;
     }
-    
+
     public Authentication authenticate(LoginCredential authentication,boolean trusted) {
     	return null;
     }
-    
+
     /**
-     * createOnlineSession 
+     * createOnlineSession
      * @param credential
      * @param userInfo
      * @return
@@ -112,7 +116,7 @@ public abstract class AbstractAuthenticationProvider {
 
         List<GrantedAuthority> grantedAuthoritys = authenticationRealm.grantAuthority(userInfo);
         principal.setAuthenticated(true);
-        
+
         for(GrantedAuthority administratorsAuthority : grantedAdministratorsAuthoritys) {
             if(grantedAuthoritys.contains(administratorsAuthority)) {
             	principal.setRoleAdministrators(true);
@@ -120,37 +124,37 @@ public abstract class AbstractAuthenticationProvider {
             }
         }
         _logger.debug("Granted Authority {}" , grantedAuthoritys);
-        
+
         principal.setGrantedAuthorityApps(authenticationRealm.queryAuthorizedApps(grantedAuthoritys));
-        
+
         UsernamePasswordAuthenticationToken authenticationToken =
                 new UsernamePasswordAuthenticationToken(
-                		principal, 
-                        "PASSWORD", 
+                		principal,
+                        "PASSWORD",
                         grantedAuthoritys
                 );
-        
+
         authenticationToken.setDetails(
                 new WebAuthenticationDetails(WebContext.getRequest()));
-        
+
         /*
          *  put Authentication to current session context
          */
         session.setAuthentication(authenticationToken);
-        
+
         //create session
         this.sessionManager.create(session.getId(), session);
-        
+
         //set Authentication to http session
         AuthorizationUtils.setAuthentication(authenticationToken);
-     
+
         return authenticationToken;
     }
-    
+
     /**
      * login user by j_username and j_cname first query user by j_cname if first
      * step userinfo is null,query user from system.
-     * 
+     *
      * @param username String
      * @param password String
      * @return
@@ -164,7 +168,7 @@ public abstract class AbstractAuthenticationProvider {
             } else {
                 _logger.debug("User Login. ");
             }
-            
+
         }
 
         return userInfo;
@@ -172,7 +176,7 @@ public abstract class AbstractAuthenticationProvider {
 
     /**
      * check input password empty.
-     * 
+     *
      * @param password String
      * @return
      */
@@ -185,7 +189,7 @@ public abstract class AbstractAuthenticationProvider {
 
     /**
      * check input username or password empty.
-     * 
+     *
      * @param email String
      * @return
      */
@@ -198,7 +202,7 @@ public abstract class AbstractAuthenticationProvider {
 
     /**
      * check input username empty.
-     * 
+     *
      * @param username String
      * @return
      */
@@ -219,8 +223,8 @@ public abstract class AbstractAuthenticationProvider {
             loginUser.setDisplayName("not exist");
             loginUser.setLoginCount(0);
             authenticationRealm.insertLoginHistory(
-            			loginUser, 
-            			ConstsLoginType.LOCAL, 
+            			loginUser,
+            			ConstsLoginType.LOCAL,
             			"",
             			i18nMessage,
             			WebConstants.LOGIN_RESULT.USER_NOT_EXIST);
@@ -228,22 +232,22 @@ public abstract class AbstractAuthenticationProvider {
         }
         return true;
     }
-    
+
     protected boolean statusValid(LoginCredential loginCredential , UserInfo userInfo) {
     	if(userInfo.getIsLocked()==ConstsStatus.LOCK) {
-    		authenticationRealm.insertLoginHistory( 
-    				userInfo, 
-                    loginCredential.getAuthType(), 
-                    loginCredential.getProvider(), 
-                    loginCredential.getCode(), 
+    		authenticationRealm.insertLoginHistory(
+    				userInfo,
+                    loginCredential.getAuthType(),
+                    loginCredential.getProvider(),
+                    loginCredential.getCode(),
                     WebConstants.LOGIN_RESULT.USER_LOCKED
                 );
     	}else if(userInfo.getStatus()!=ConstsStatus.ACTIVE) {
-    		authenticationRealm.insertLoginHistory( 
-    				userInfo, 
-                    loginCredential.getAuthType(), 
-                    loginCredential.getProvider(), 
-                    loginCredential.getCode(), 
+    		authenticationRealm.insertLoginHistory(
+    				userInfo,
+                    loginCredential.getAuthType(),
+                    loginCredential.getProvider(),
+                    loginCredential.getCode(),
                     WebConstants.LOGIN_RESULT.USER_INACTIVE
                 );
     	}

+ 88 - 0
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/impl/ScanCodeAuthenticationProvider.java

@@ -0,0 +1,88 @@
+package org.dromara.maxkey.authn.provider.impl;
+
+import org.dromara.maxkey.authn.LoginCredential;
+import org.dromara.maxkey.authn.SignPrincipal;
+import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
+import org.dromara.maxkey.authn.provider.scancode.ScanCodeService;
+import org.dromara.maxkey.authn.provider.scancode.ScanCodeState;
+
+import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
+import org.dromara.maxkey.authn.session.SessionManager;
+
+import org.dromara.maxkey.constants.ConstsLoginType;
+import org.dromara.maxkey.crypto.password.PasswordReciprocal;
+import org.dromara.maxkey.entity.idm.UserInfo;
+;
+import org.dromara.maxkey.web.WebConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+
+import java.util.Objects;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM4:54
+ */
+public class ScanCodeAuthenticationProvider extends AbstractAuthenticationProvider {
+
+    private static final Logger _logger = LoggerFactory.getLogger(ScanCodeAuthenticationProvider.class);
+
+    @Autowired
+    ScanCodeService scanCodeService;
+
+    public ScanCodeAuthenticationProvider() {
+        super();
+    }
+
+    public ScanCodeAuthenticationProvider(
+            AbstractAuthenticationRealm authenticationRealm,
+            SessionManager sessionManager) {
+        this.authenticationRealm = authenticationRealm;
+        this.sessionManager = sessionManager;
+    }
+
+    @Override
+    public String getProviderName() {
+        return "scancode" + PROVIDER_SUFFIX;
+    }
+
+    @Override
+    public Authentication doAuthenticate(LoginCredential loginCredential) {
+        UsernamePasswordAuthenticationToken authenticationToken = null;
+
+        String encodeTicket = PasswordReciprocal.getInstance().decoder(loginCredential.getUsername());
+
+        ScanCodeState scanCodeState = scanCodeService.consume(encodeTicket);
+
+        if (Objects.isNull(scanCodeState)) {
+            return null;
+        }
+
+        SignPrincipal signPrincipal = (SignPrincipal) sessionManager.get(scanCodeState.getSessionId()).getAuthentication().getPrincipal();
+        //获取用户信息
+        UserInfo userInfo = signPrincipal.getUserInfo();
+
+        isUserExist(loginCredential , userInfo);
+
+        statusValid(loginCredential , userInfo);
+
+
+        //创建登录会话
+        authenticationToken = createOnlineTicket(loginCredential,userInfo);
+        // user authenticated
+        _logger.debug("'{}' authenticated successfully by {}.",
+                loginCredential.getPrincipal(), getProviderName());
+
+        authenticationRealm.insertLoginHistory(userInfo,
+                ConstsLoginType.LOCAL,
+                "",
+                "xe00000004",
+                WebConstants.LOGIN_RESULT.SUCCESS);
+
+        return  authenticationToken;
+    }
+}

+ 106 - 0
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeService.java

@@ -0,0 +1,106 @@
+package org.dromara.maxkey.authn.provider.scancode;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.maxkey.authn.session.Session;
+import org.dromara.maxkey.authn.session.SessionManager;
+import org.dromara.maxkey.exception.BusinessException;
+import org.dromara.maxkey.persistence.cache.MomentaryService;
+import org.dromara.maxkey.util.IdGenerator;
+import org.dromara.maxkey.util.JsonUtils;
+import org.dromara.maxkey.util.TimeJsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 15/8/2024 AM9:49
+ */
+
+@Repository
+public class ScanCodeService {
+    private static final Logger _logger = LoggerFactory.getLogger(ScanCodeService.class);
+
+    static final String SCANCODE_TICKET = "login:scancode:%s";
+
+    static final String SCANCODE_CONFIRM = "login:scancode:confirm:%s";
+
+    public static class STATE {
+        public static final String SCANED = "scaned";
+        public static final String CONFIRMED = "confirmed";
+        public static final String CANCELED = "canceled";
+
+        public static final String CANCEL = "cancel";
+        public static final String CONFIRM = "confirm";
+    }
+
+    int validitySeconds = 60 * 3; //default 3 minutes.
+
+    int cancelValiditySeconds = 60 * 1; //default 1 minutes.
+
+
+    @Autowired
+    IdGenerator idGenerator;
+
+    @Autowired
+    MomentaryService momentaryService;
+
+    private String getKey(String ticket) {
+        return SCANCODE_TICKET.formatted(ticket);
+    }
+
+    private String getConfirmKey(Long sessionId) {
+        return SCANCODE_CONFIRM.formatted(sessionId);
+    }
+
+    public String createTicket() {
+        String ticket = idGenerator.generate();
+        ScanCodeState scanCodeState = new ScanCodeState();
+        scanCodeState.setState("unscanned");
+
+        // 将对象序列化为 JSON 字符串
+        String jsonString = TimeJsonUtils.gsonToString(scanCodeState);
+        momentaryService.put(getKey(ticket), "", jsonString);
+        _logger.info("Ticket {} , Duration {}", ticket, jsonString);
+
+        return ticket;
+    }
+
+    public boolean validateTicket(String ticket, Session session) {
+        String key = getKey(ticket);
+        Object value = momentaryService.get(key, "");
+        if (Objects.isNull(value)) {
+            return false;
+        }
+
+        ScanCodeState scanCodeState = new ScanCodeState();
+        scanCodeState.setState("scanned");
+        scanCodeState.setTicket(ticket);
+        scanCodeState.setSessionId(session.getId());
+        momentaryService.put(key, "", TimeJsonUtils.gsonToString(scanCodeState));
+
+        return true;
+    }
+
+    public ScanCodeState consume(String ticket){
+        String key = getKey(ticket);
+        Object o = momentaryService.get(key, "");
+        if (Objects.nonNull(o)) {
+            String redisObject = o.toString();
+            ScanCodeState scanCodeState = TimeJsonUtils.gsonStringToObject(redisObject, ScanCodeState.class);
+            if ("scanned".equals(scanCodeState.getState())) {
+                momentaryService.remove(key, "");
+                return scanCodeState;
+            } else {
+                return null;
+            }
+        } else {
+            throw new BusinessException(20004, "该二维码失效");
+        }
+    }
+}

+ 53 - 0
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeState.java

@@ -0,0 +1,53 @@
+package org.dromara.maxkey.authn.provider.scancode;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.dromara.maxkey.authn.session.Session;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM5:42
+ */
+public class ScanCodeState {
+
+    String sessionId;
+
+    String ticket;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
+    Long confirmKey;
+
+    String state;
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public void setSessionId(String sessionId) {
+        this.sessionId = sessionId;
+    }
+
+    public String getTicket() {
+        return ticket;
+    }
+
+    public void setTicket(String ticket) {
+        this.ticket = ticket;
+    }
+
+    public Long getConfirmKey() {
+        return confirmKey;
+    }
+
+    public void setConfirmKey(Long confirmKey) {
+        this.confirmKey = confirmKey;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+}

+ 19 - 0
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScancodeSignInfo.java

@@ -0,0 +1,19 @@
+package org.dromara.maxkey.authn.provider.scancode;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 17/8/2024 PM5:08
+ */
+public class ScancodeSignInfo {
+
+    String username;
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+}

+ 28 - 14
maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/autoconfigure/AuthnProviderAutoConfiguration.java

@@ -1,19 +1,19 @@
 /*
  * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
- * 
+ *
  * 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
- * 
+ *
  *     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.
  */
- 
+
 
 package org.dromara.maxkey.autoconfigure;
 
@@ -22,6 +22,7 @@ import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
 import org.dromara.maxkey.authn.provider.AuthenticationProviderFactory;
 import org.dromara.maxkey.authn.provider.impl.MobileAuthenticationProvider;
 import org.dromara.maxkey.authn.provider.impl.NormalAuthenticationProvider;
+import org.dromara.maxkey.authn.provider.impl.ScanCodeAuthenticationProvider;
 import org.dromara.maxkey.authn.provider.impl.TrustedAuthenticationProvider;
 import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
 import org.dromara.maxkey.authn.session.SessionManager;
@@ -44,21 +45,23 @@ import org.springframework.jdbc.core.JdbcTemplate;
 @AutoConfiguration
 public class AuthnProviderAutoConfiguration {
     static final  Logger _logger = LoggerFactory.getLogger(AuthnProviderAutoConfiguration.class);
-    
+
     @Bean
     public AbstractAuthenticationProvider authenticationProvider(
     		NormalAuthenticationProvider normalAuthenticationProvider,
     		MobileAuthenticationProvider mobileAuthenticationProvider,
-    		TrustedAuthenticationProvider trustedAuthenticationProvider
+    		TrustedAuthenticationProvider trustedAuthenticationProvider,
+			ScanCodeAuthenticationProvider scanCodeAuthenticationProvider
     		) {
     	AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory();
     	authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider);
     	authenticationProvider.addAuthenticationProvider(mobileAuthenticationProvider);
     	authenticationProvider.addAuthenticationProvider(trustedAuthenticationProvider);
-    	
+    	authenticationProvider.addAuthenticationProvider(scanCodeAuthenticationProvider);
+
     	return authenticationProvider;
     }
-    		
+
     @Bean
     public NormalAuthenticationProvider normalAuthenticationProvider(
     		AbstractAuthenticationRealm authenticationRealm,
@@ -74,7 +77,18 @@ public class AuthnProviderAutoConfiguration {
         		authTokenService
         	);
     }
-    
+
+	@Bean
+	public ScanCodeAuthenticationProvider scanCodeAuthenticationProvider(
+			AbstractAuthenticationRealm authenticationRealm,
+			SessionManager sessionManager
+	) {
+		return new ScanCodeAuthenticationProvider(
+				authenticationRealm,
+				sessionManager
+		);
+	}
+
     @Bean
     public MobileAuthenticationProvider mobileAuthenticationProvider(
     		AbstractAuthenticationRealm authenticationRealm,
@@ -104,22 +118,22 @@ public class AuthnProviderAutoConfiguration {
         		sessionManager
         	);
     }
-    
+
     @Bean
     public PasswordPolicyValidator passwordPolicyValidator(JdbcTemplate jdbcTemplate,MessageSource messageSource) {
         return new PasswordPolicyValidator(jdbcTemplate,messageSource);
     }
-    
+
     @Bean
     public LoginRepository loginRepository(JdbcTemplate jdbcTemplate) {
         return new LoginRepository(jdbcTemplate);
     }
-    
+
     @Bean
     public LoginHistoryRepository loginHistoryRepository(JdbcTemplate jdbcTemplate) {
         return new LoginHistoryRepository(jdbcTemplate);
     }
-    
+
     /**
      * remeberMeService .
      * @return
@@ -135,5 +149,5 @@ public class AuthnProviderAutoConfiguration {
         return new  JdbcRemeberMeManager(
         		jdbcTemplate,applicationConfig,authTokenService,validity);
     }
-    
+
 }

+ 23 - 0
maxkey-common/src/main/java/org/dromara/maxkey/adapter/LocalDateTimeAdapter.java

@@ -0,0 +1,23 @@
+package org.dromara.maxkey.adapter;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
+    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public void write(JsonWriter out, LocalDateTime value) throws IOException {
+        out.value(value.format(formatter));
+    }
+
+    @Override
+    public LocalDateTime read(JsonReader in) throws IOException {
+        return LocalDateTime.parse(in.nextString(), formatter);
+    }
+}

+ 37 - 0
maxkey-common/src/main/java/org/dromara/maxkey/util/TimeJsonUtils.java

@@ -0,0 +1,37 @@
+package org.dromara.maxkey.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.dromara.maxkey.adapter.LocalDateTimeAdapter;
+
+import java.time.LocalDateTime;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM6:23
+ */
+public class TimeJsonUtils {
+    public static <T> T gsonStringToObject(String json, Class<T> cls) {
+        Gson gson = new GsonBuilder()
+                .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+                .create();
+        return gson.fromJson(json, cls);
+    }
+
+    /**
+     * Gson Transform java bean object to json string .
+     *
+     * @param bean Object
+     * @return string
+     */
+    public static String gsonToString(Object bean) {
+        String json = "";
+        Gson gson = new GsonBuilder()
+                .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+                .create();
+        json = gson.toJson(bean);
+
+        return json;
+    }
+}

+ 47 - 0
maxkey-core/src/main/java/org/dromara/maxkey/exception/BusinessException.java

@@ -0,0 +1,47 @@
+package org.dromara.maxkey.exception;
+
+import java.io.Serial;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM3:03
+ */
+public class BusinessException extends RuntimeException {
+    /**
+     * 异常编码
+     */
+    private Integer code;
+
+    /**
+     * 异常消息
+     */
+    private String message;
+
+
+    public BusinessException() {
+        super();
+    }
+
+    public BusinessException(Integer code, String message) {
+        this.message = message;
+        this.code = code;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}

+ 198 - 0
maxkey-core/src/main/java/org/dromara/maxkey/web/GlobalExceptionHandler.java

@@ -0,0 +1,198 @@
+package org.dromara.maxkey.web;
+
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.UnexpectedTypeException;
+import org.dromara.maxkey.entity.Message;
+import org.dromara.maxkey.exception.BusinessException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @description:
+ * @author: orangeBabu
+ * @time: 16/8/2024 PM3:02
+ */
+
+/**
+ * 全局异常处理器
+ *
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+
+    /**
+     * 缺少请求体异常处理器
+     * @param e 缺少请求体异常 使用get方式请求 而实体使用@RequestBody修饰
+     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    public Message<Void> parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',请求体缺失'{}'", requestURI, e.getMessage(),e);
+        return new Message<>(Message.FAIL, "缺少请求体");
+    }
+
+    // get请求的对象参数校验异常
+    @ExceptionHandler({MissingServletRequestParameterException.class})
+    public Message<Void> bindExceptionHandler(MissingServletRequestParameterException e,HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',get方式请求参数'{}'必传", requestURI, e.getMessage(),e);
+        return new Message<>(Message.FAIL, "请求的对象参数校验异常");
+    }
+
+    /**
+     * 请求方式不支持
+     */
+    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+    public Message<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址 '{}',不支持'{}' 请求", requestURI, e.getMethod(),e);
+        return new Message<>(HttpStatus.METHOD_NOT_ALLOWED.value(),HttpStatus.METHOD_NOT_ALLOWED.getReasonPhrase());
+    }
+
+
+
+
+    /**
+     * 参数不正确
+     */
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    public Message<Void> methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        String error = String.format("%s 应该是 %s 类型", e.getName(), e.getRequiredType().getSimpleName());
+        log.error("请求地址'{}',{},参数类型不正确", requestURI,error,e);
+        return new Message<>(Message.FAIL, "参数类型不正确");
+    }
+
+    /**
+     * 系统异常
+     */
+    @ExceptionHandler(Exception.class)
+    public Message<Void> handleException(Exception e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',发生系统异常.", requestURI, e);
+        return new Message<>(Message.FAIL, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
+    }
+
+    /**
+     * 捕获转换类型异常
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(UnexpectedTypeException.class)
+    public Message<String> unexpectedTypeHandler(UnexpectedTypeException e)
+    {
+        log.error("类型转换错误:{}",e.getMessage(), e);
+        return  new Message<>(HttpStatus.INTERNAL_SERVER_ERROR.value(),e.getMessage());
+    }
+
+    /**
+     * 捕获转换类型异常
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Message<String> methodArgumentNotValidException(MethodArgumentNotValidException e)
+    {
+        BindingResult bindingResult =  e.getBindingResult();
+        List<ObjectError> errors = bindingResult.getAllErrors();
+        log.error("参数验证异常:{}",e.getMessage(), e);
+        if (!errors.isEmpty()) {
+            // 只显示第一个错误信息
+            return new Message<>(HttpStatus.BAD_REQUEST.value(), errors.get(0).getDefaultMessage());
+        }
+        return new Message<>(HttpStatus.BAD_REQUEST.value(),"MethodArgumentNotValid");
+    }
+
+    // 运行时异常
+    @ExceptionHandler(RuntimeException.class)
+    public Message<String> runtimeExceptionHandler(RuntimeException e, HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',捕获运行时异常'{}'", requestURI, e.getMessage(),e);
+        return new Message<>(Message.FAIL, e.getMessage());
+    }
+    // 系统级别异常
+    @ExceptionHandler(Throwable.class)
+    public Message<String> throwableExceptionHandler(Throwable e,HttpServletRequest request) {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',捕获系统级别异常'{}'", requestURI,e.getMessage(),e);
+        return new Message<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
+    }
+
+    /**
+     * IllegalArgumentException 捕获转换类型异常
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(IllegalArgumentException.class)
+    public Message<String> illegalArgumentException(IllegalArgumentException e)
+    {
+        String message = e.getMessage();
+        log.error("IllegalArgumentException:{}",e.getMessage(),e);
+        if (Objects.nonNull(message)) {
+            //错误信息
+            return new Message<>(HttpStatus.BAD_REQUEST.value(),message);
+        }
+        return  new Message<>(HttpStatus.BAD_REQUEST.value(),"error");
+    }
+    /**
+     * InvalidFormatException 捕获转换类型异常
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(InvalidFormatException.class)
+    public Message<String> invalidFormatException(InvalidFormatException e)
+    {
+        String message = e.getMessage();
+        log.error("InvalidFormatException:{}",e.getMessage(),e);
+        if (Objects.nonNull(message)) {
+            //错误信息
+            return new Message<>(HttpStatus.BAD_REQUEST.value(),message);
+        }
+        return new Message<>(HttpStatus.BAD_REQUEST.value(),"error");
+    }
+
+
+
+    /**
+     * 自定义验证异常
+     */
+    @ExceptionHandler(BindException.class)
+    public Message<Void> handleBindException(BindException e) {
+        BindingResult bindingResult =  e.getBindingResult();
+        List<ObjectError> errors = bindingResult.getAllErrors();
+        log.error("参数验证异常:{}",e.getMessage(), e);
+        if (!errors.isEmpty()) {
+            // 只显示第一个错误信息
+            return new Message<>(HttpStatus.BAD_REQUEST.value(), errors.get(0).getDefaultMessage());
+        }
+        return new Message<>(HttpStatus.BAD_REQUEST.value(),"MethodArgumentNotValid");
+    }
+
+    /**
+     * 业务异常处理
+     * 业务自定义code 与 message
+     *
+     */
+    @ExceptionHandler(BusinessException.class)
+    public Message<String> handleBusinessException(BusinessException e) {
+        log.error("业务自定义异常:{},{}",e.getCode(),e.getMessage(),e);
+        return new Message<>(e.getCode(),e.getMessage());
+    }
+}

+ 0 - 63
maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java

@@ -1,63 +0,0 @@
-package org.dromara.maxkey.persistence.service;
-
-import org.dromara.maxkey.persistence.cache.MomentaryService;
-import org.dromara.maxkey.util.IdGenerator;
-import org.dromara.maxkey.util.ObjectTransformer;
-import org.dromara.mybatis.jpa.id.IdentifierGenerator;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Repository;
-
-import java.time.Duration;
-
-/**
- * @description:
- * @author: orangeBabu
- * @time: 15/8/2024 AM9:49
- */
-
-@Repository
-public class ScanCodeService {
-    private static final Logger _logger = LoggerFactory.getLogger(ScanCodeService.class);
-
-    static final String SCANCODE_TICKET = "login:scancode:%s";
-
-    static final String SCANCODE_CONFIRM = "login:scancode:confirm:%s";
-
-    public static class STATE{
-        public static final String SCANED 		= "scaned";
-        public static final String CONFIRMED 	= "confirmed";
-        public static final String CANCELED 	= "canceled";
-
-        public static final String CANCEL 		= "cancel";
-        public static final String CONFIRM 		= "confirm";
-    }
-
-    int validitySeconds 	= 60 * 3; //default 3 minutes.
-
-    int cancelValiditySeconds 	= 60 * 1; //default 1 minutes.
-
-
-    @Autowired
-    IdGenerator idGenerator;
-
-    @Autowired
-    MomentaryService momentaryService;
-
-    private String getKey(Long ticket) {
-        return SCANCODE_TICKET.formatted(ticket);
-    }
-
-    private String getConfirmKey(Long sessionId) {
-        return SCANCODE_CONFIRM.formatted(sessionId);
-    }
-
-    public Long createTicket() {
-        Long ticket = 0L;
-        ticket = Long.parseLong(idGenerator.generate());
-        momentaryService.put(getKey(ticket), "", Duration.ofSeconds(validitySeconds));
-        _logger.info("Ticket {} , Duration {}", ticket , Duration.ofSeconds(validitySeconds));
-        return ticket;
-    }
-}

+ 42 - 2
maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts

@@ -61,6 +61,8 @@ export class UserLoginComponent implements OnInit, OnDestroy {
   state = '';
   count = 0;
   interval$: any;
+  //二维码内容
+  ticket = '';
 
   constructor(
     fb: FormBuilder,
@@ -298,25 +300,63 @@ export class UserLoginComponent implements OnInit, OnDestroy {
     });
   }
 
+  /**
+   * 获取二维码
+   */
   getLoginQrCode() {
     this.qrexpire = false;
 
     this.qrCodeService.getLoginQrCode().subscribe(res => {
       if (res.code === 0 && res.data.rqCode) { // 使用返回的 rqCode
         const qrImageElement = document.getElementById('div_qrcodelogin');
+        this.ticket = res.data.ticket;
         if (qrImageElement) {
           qrImageElement.innerHTML = `<img src="${res.data.rqCode}" alt="QR Code" style="width: 200px; height: 200px;">`;
         }
 
-        // 设置三分钟后 qrexpire 为 false
+     /*   // 设置5分钟后 qrexpire 为 false
         setTimeout(() => {
           this.qrexpire = true;
           this.cdr.detectChanges(); // 更新视图
-        }, 3 * 60 * 1000); // 180000 毫秒 = 3 分钟
+        }, 5 * 60 * 1000); // 5 分钟*/
+        this.loginByQrCode();
       }
     });
   }
 
+  /**
+   * 二维码轮询登录
+   */
+  loginByQrCode() {
+    const interval = setInterval(() => {
+      this.qrCodeService.loginByQrCode({
+        authType: 'scancode',
+        code: this.ticket,
+      }).subscribe(res => {
+        if (res.code === 0) {
+          this.qrexpire = true;
+          // 清空路由复用信息
+          this.reuseTabService.clear();
+          // 设置用户Token信息
+          this.authnService.auth(res.data);
+          this.authnService.navigate({});
+        } else if (res.code === 20004) {
+          this.qrexpire = true;
+        }
+
+        // Handle response here
+
+        // If you need to stop the interval after a certain condition is met,
+        // you can clear the interval like this:
+        if (this.qrexpire) {
+          clearInterval(interval);
+        }
+
+        this.cdr.detectChanges(); // 更新视图
+      });
+    }, 5 * 1000); // 5 seconds
+  }
+
 
 
   getQrCode(): void {

+ 5 - 1
maxkey-web-frontend/maxkey-web-app/src/app/service/QrCode.service.ts

@@ -15,7 +15,7 @@
  */
 
 import { Injectable } from '@angular/core';
-import { SettingsService, _HttpClient, User } from '@delon/theme';
+import { _HttpClient } from '@delon/theme';
 
 @Injectable({
   providedIn: 'root'
@@ -26,4 +26,8 @@ export class QrCodeService {
   getLoginQrCode() {
     return this.http.get('/login/genScanCode');
   }
+
+  loginByQrCode(authParam: any) {
+    return this.http.post('/login/sign/qrcode', authParam);
+  }
 }

+ 59 - 6
maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java

@@ -20,8 +20,12 @@ package org.dromara.maxkey.web.contorller;
 import java.awt.image.BufferedImage;
 import java.text.ParseException;
 import java.util.HashMap;
+import java.util.Objects;
+
 import org.apache.commons.lang3.StringUtils;
 import org.dromara.maxkey.authn.LoginCredential;
+import org.dromara.maxkey.authn.QrCodeCredentialDto;
+import org.dromara.maxkey.authn.ScanCode;
 import org.dromara.maxkey.authn.jwt.AuthJwt;
 import org.dromara.maxkey.authn.jwt.AuthTokenService;
 import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
@@ -38,9 +42,10 @@ import org.dromara.maxkey.crypto.Base64Utils;
 import org.dromara.maxkey.crypto.password.PasswordReciprocal;
 import org.dromara.maxkey.entity.*;
 import org.dromara.maxkey.entity.idm.UserInfo;
+import org.dromara.maxkey.exception.BusinessException;
 import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
 import org.dromara.maxkey.password.sms.SmsOtpAuthnService;
-import org.dromara.maxkey.persistence.service.ScanCodeService;
+import org.dromara.maxkey.authn.provider.scancode.ScanCodeService;
 import org.dromara.maxkey.persistence.service.SocialsAssociatesService;
 import org.dromara.maxkey.persistence.service.UserInfoService;
 import org.dromara.maxkey.util.RQCodeUtils;
@@ -51,6 +56,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.security.core.Authentication;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -277,11 +283,9 @@ public class LoginEntryPoint {
 	 @GetMapping("/genScanCode")
 	 public Message<HashMap<String,String>> genScanCode() {
 		 log.debug("/genScanCode.");
-		 UserInfo userInfo = AuthorizationUtils.getUserInfo();
-		 Long ticket = scanCodeService.createTicket();
-		 String ticketString = userInfo == null ? ticket.toString() : ticket+","+userInfo.getId();
-		 log.debug("ticket string {}",ticketString);
-		 String encodeTicket = PasswordReciprocal.getInstance().encode(ticketString);
+		 String ticket = scanCodeService.createTicket();
+		 log.debug("ticket: {}",ticket);
+		 String encodeTicket = PasswordReciprocal.getInstance().encode(ticket);
 		 BufferedImage bufferedImage  =  RQCodeUtils.write2BufferedImage(encodeTicket, "gif", 300, 300);
 		 String rqCode = Base64Utils.encodeImage(bufferedImage);
 		 HashMap<String,String> codeMap = new HashMap<>();
@@ -289,4 +293,53 @@ public class LoginEntryPoint {
 		 codeMap.put("ticket", encodeTicket);
 		 return new Message<>(Message.SUCCESS, codeMap);
 	 }
+
+	@Operation(summary = "web二维码登录", description = "web二维码登录", method = "POST")
+	@PostMapping("/sign/qrcode")
+	public Message<AuthJwt> signByQrcode( HttpServletRequest request,
+										  HttpServletResponse response,
+										  @Validated @RequestBody ScanCode scanCode) {
+		LoginCredential loginCredential = new LoginCredential();
+		loginCredential.setAuthType(scanCode.getAuthType());
+		loginCredential.setUsername(scanCode.getCode());
+
+		try {
+			Authentication authentication = authenticationProvider.authenticate(loginCredential);
+			if (Objects.nonNull(authentication)) {
+				//success
+				AuthJwt authJwt = authTokenService.genAuthJwt(authentication);
+				return new Message<>(authJwt);
+			} else {
+				return new Message<>(Message.FAIL, "尚未扫码");
+			}
+		} catch (BusinessException businessException) {
+			return new Message<>(businessException.getCode(), businessException.getMessage());
+		}
+	}
+
+	@Operation(summary = "app扫描二维码", description = "扫描二维码登录", method = "POST")
+	@PostMapping("/scanCode")
+	public Message<String> scanCode(@Validated @RequestBody QrCodeCredentialDto credentialDto) throws ParseException {
+		log.debug("/scanCode.");
+		String jwtToken = credentialDto.getJwtToken();
+		String code = credentialDto.getCode();
+		try {
+			//获取登录会话
+			Session session = AuthorizationUtils.getSession(sessionManager, jwtToken);
+			if (Objects.isNull(session)) {
+				return new Message<>(Message.FAIL, "登录会话失效,请重新登录");
+			}
+			//查询二维码是否过期
+			String ticketString = PasswordReciprocal.getInstance().decoder(code);
+			boolean codeResult = scanCodeService.validateTicket(ticketString, session);
+			if (!codeResult) {
+				return new Message<>(Message.FAIL, "二维码已过期,请重新获取");
+			}
+
+		} catch (ParseException e) {
+			log.error("ParseException.",e);
+			return new Message<>(Message.FAIL, "token格式错误");
+		}
+		return new Message<>(Message.SUCCESS, "成功");
+	}
 }