From bab15aee235af9c863382f39f26673c9c0118f0e Mon Sep 17 00:00:00 2001 From: orangebabu <2409692770@qq.com> Date: Sun, 18 Aug 2024 15:30:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E7=BB=B4=E7=A0=81=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../maxkey/authn/QrCodeCredentialDto.java | 42 ++++ .../org/dromara/maxkey/authn/ScanCode.java | 33 +++ .../maxkey/authn/session/SessionManager.java | 20 +- .../maxkey/authn/web/AuthorizationUtils.java | 53 +++-- .../AbstractAuthenticationProvider.java | 94 +++++---- .../impl/ScanCodeAuthenticationProvider.java | 88 ++++++++ .../provider/scancode/ScanCodeService.java | 106 ++++++++++ .../provider/scancode/ScanCodeState.java | 53 +++++ .../provider/scancode/ScancodeSignInfo.java | 19 ++ .../AuthnProviderAutoConfiguration.java | 42 ++-- .../maxkey/adapter/LocalDateTimeAdapter.java | 23 ++ .../dromara/maxkey/util/TimeJsonUtils.java | 37 ++++ .../maxkey/exception/BusinessException.java | 47 +++++ .../maxkey/web/GlobalExceptionHandler.java | 198 ++++++++++++++++++ .../persistence/service/ScanCodeService.java | 63 ------ .../routes/passport/login/login.component.ts | 44 +++- .../src/app/service/QrCode.service.ts | 6 +- .../web/contorller/LoginEntryPoint.java | 65 +++++- 18 files changed, 870 insertions(+), 163 deletions(-) create mode 100644 maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/QrCodeCredentialDto.java create mode 100644 maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/ScanCode.java create mode 100644 maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/impl/ScanCodeAuthenticationProvider.java create mode 100644 maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeService.java create mode 100644 maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeState.java create mode 100644 maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScancodeSignInfo.java create mode 100644 maxkey-common/src/main/java/org/dromara/maxkey/adapter/LocalDateTimeAdapter.java create mode 100644 maxkey-common/src/main/java/org/dromara/maxkey/util/TimeJsonUtils.java create mode 100644 maxkey-core/src/main/java/org/dromara/maxkey/exception/BusinessException.java create mode 100644 maxkey-core/src/main/java/org/dromara/maxkey/web/GlobalExceptionHandler.java delete mode 100644 maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java diff --git a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/QrCodeCredentialDto.java b/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/QrCodeCredentialDto.java new file mode 100644 index 000000000..811a37cff --- /dev/null +++ b/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; + } +} diff --git a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/ScanCode.java b/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/ScanCode.java new file mode 100644 index 000000000..b67dedd8e --- /dev/null +++ b/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; + } +} diff --git a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/session/SessionManager.java b/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/session/SessionManager.java index 5015bbef5..232d815ac 100644 --- a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/session/SessionManager.java +++ b/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 querySessions(); - + public int getValiditySeconds(); - + public void terminate(String sessionId,String userId,String username); } diff --git a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/web/AuthorizationUtils.java b/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/web/AuthorizationUtils.java index b70264374..c622d2050 100644 --- a/maxkey-authentications/maxkey-authentication-core/src/main/java/org/dromara/maxkey/authn/web/AuthorizationUtils.java +++ b/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()); } - + } diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/AbstractAuthenticationProvider.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/AbstractAuthenticationProvider.java index 5a9605335..f071789e0 100644 --- a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/AbstractAuthenticationProvider.java +++ b/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 grantedAdministratorsAuthoritys = new ArrayList(); - + 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 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 ); } diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/impl/ScanCodeAuthenticationProvider.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/impl/ScanCodeAuthenticationProvider.java new file mode 100644 index 000000000..27410f661 --- /dev/null +++ b/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; + } +} diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeService.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeService.java new file mode 100644 index 000000000..7bd4b9189 --- /dev/null +++ b/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, "该二维码失效"); + } + } +} diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeState.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScanCodeState.java new file mode 100644 index 000000000..74ac7c68d --- /dev/null +++ b/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; + } +} diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScancodeSignInfo.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/authn/provider/scancode/ScancodeSignInfo.java new file mode 100644 index 000000000..c2d2ccc3e --- /dev/null +++ b/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; + } +} diff --git a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/autoconfigure/AuthnProviderAutoConfiguration.java b/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/autoconfigure/AuthnProviderAutoConfiguration.java index 586c4861e..3138e8d77 100644 --- a/maxkey-authentications/maxkey-authentication-provider/src/main/java/org/dromara/maxkey/autoconfigure/AuthnProviderAutoConfiguration.java +++ b/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); } - + } diff --git a/maxkey-common/src/main/java/org/dromara/maxkey/adapter/LocalDateTimeAdapter.java b/maxkey-common/src/main/java/org/dromara/maxkey/adapter/LocalDateTimeAdapter.java new file mode 100644 index 000000000..d3ebb0193 --- /dev/null +++ b/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 { + 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); + } +} diff --git a/maxkey-common/src/main/java/org/dromara/maxkey/util/TimeJsonUtils.java b/maxkey-common/src/main/java/org/dromara/maxkey/util/TimeJsonUtils.java new file mode 100644 index 000000000..972d113e0 --- /dev/null +++ b/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 gsonStringToObject(String json, Class 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; + } +} diff --git a/maxkey-core/src/main/java/org/dromara/maxkey/exception/BusinessException.java b/maxkey-core/src/main/java/org/dromara/maxkey/exception/BusinessException.java new file mode 100644 index 000000000..daa0c7e15 --- /dev/null +++ b/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; + } +} diff --git a/maxkey-core/src/main/java/org/dromara/maxkey/web/GlobalExceptionHandler.java b/maxkey-core/src/main/java/org/dromara/maxkey/web/GlobalExceptionHandler.java new file mode 100644 index 000000000..14fd40fd5 --- /dev/null +++ b/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 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 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 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 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 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 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 methodArgumentNotValidException(MethodArgumentNotValidException e) + { + BindingResult bindingResult = e.getBindingResult(); + List 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 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 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 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 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 handleBindException(BindException e) { + BindingResult bindingResult = e.getBindingResult(); + List 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 handleBusinessException(BusinessException e) { + log.error("业务自定义异常:{},{}",e.getCode(),e.getMessage(),e); + return new Message<>(e.getCode(),e.getMessage()); + } +} diff --git a/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java b/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java deleted file mode 100644 index 79ed783b4..000000000 --- a/maxkey-persistence/src/main/java/org/dromara/maxkey/persistence/service/ScanCodeService.java +++ /dev/null @@ -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; - } -} diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts b/maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts index eed06507b..c441d5026 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/routes/passport/login/login.component.ts +++ b/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 = `QR Code`; } - // 设置三分钟后 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 { diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/service/QrCode.service.ts b/maxkey-web-frontend/maxkey-web-app/src/app/service/QrCode.service.ts index ab630a299..f663df41e 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/service/QrCode.service.ts +++ b/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); + } } diff --git a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java b/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java index c7306f7aa..4ed3bf96e 100644 --- a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java +++ b/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> 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 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 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 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, "成功"); + } }