Merge pull request #230 from orangebabu/main

二维码跳转登录
This commit is contained in:
orangebabu 2024-08-18 15:38:40 +08:00 committed by GitHub
commit 73e95e0c26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 870 additions and 163 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,19 +1,19 @@
/* /*
* Copyright [2022] [MaxKey of copyright http://www.maxkey.top] * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.dromara.maxkey.authn.session; package org.dromara.maxkey.authn.session;
@ -27,16 +27,16 @@ public interface SessionManager {
public void create(String sessionId, Session session); public void create(String sessionId, Session session);
public Session remove(String sessionId); public Session remove(String sessionId);
public Session get(String sessionId); public Session get(String sessionId);
public Session refresh(String sessionId ,LocalDateTime refreshTime); public Session refresh(String sessionId ,LocalDateTime refreshTime);
public Session refresh(String sessionId); public Session refresh(String sessionId);
public List<HistoryLogin> querySessions(); public List<HistoryLogin> querySessions();
public int getValiditySeconds(); public int getValiditySeconds();
public void terminate(String sessionId,String userId,String username); public void terminate(String sessionId,String userId,String username);
} }

View File

@ -1,25 +1,26 @@
/* /*
* Copyright [2022] [MaxKey of copyright http://www.maxkey.top] * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.dromara.maxkey.authn.web; package org.dromara.maxkey.authn.web;
import java.text.ParseException; import java.text.ParseException;
import com.nimbusds.jwt.SignedJWT;
import org.dromara.maxkey.authn.SignPrincipal; import org.dromara.maxkey.authn.SignPrincipal;
import org.dromara.maxkey.authn.jwt.AuthTokenService; import org.dromara.maxkey.authn.jwt.AuthTokenService;
import org.dromara.maxkey.authn.session.Session; import org.dromara.maxkey.authn.session.Session;
@ -37,14 +38,14 @@ import jakarta.servlet.http.HttpServletRequest;
public class AuthorizationUtils { public class AuthorizationUtils {
private static final Logger _logger = LoggerFactory.getLogger(AuthorizationUtils.class); private static final Logger _logger = LoggerFactory.getLogger(AuthorizationUtils.class);
public static final class BEARERTYPE{ public static final class BEARERTYPE{
public static final String CONGRESS = "congress"; public static final String CONGRESS = "congress";
public static final String AUTHORIZATION = "Authorization"; public static final String AUTHORIZATION = "Authorization";
} }
public static void authenticateWithCookie( public static void authenticateWithCookie(
HttpServletRequest request, HttpServletRequest request,
AuthTokenService authTokenService, AuthTokenService authTokenService,
@ -55,12 +56,12 @@ public class AuthorizationUtils {
String authorization = authCookie.getValue(); String authorization = authCookie.getValue();
_logger.trace("Try congress authenticate ."); _logger.trace("Try congress authenticate .");
doJwtAuthenticate(BEARERTYPE.CONGRESS,authorization,authTokenService,sessionManager); doJwtAuthenticate(BEARERTYPE.CONGRESS,authorization,authTokenService,sessionManager);
}else { }else {
_logger.debug("cookie is null , clear authentication ."); _logger.debug("cookie is null , clear authentication .");
clearAuthentication(); clearAuthentication();
} }
} }
public static void authenticate( public static void authenticate(
HttpServletRequest request, HttpServletRequest request,
AuthTokenService authTokenService, AuthTokenService authTokenService,
@ -71,9 +72,9 @@ public class AuthorizationUtils {
_logger.trace("Try Authorization authenticate ."); _logger.trace("Try Authorization authenticate .");
doJwtAuthenticate(BEARERTYPE.AUTHORIZATION,authorization,authTokenService,sessionManager); doJwtAuthenticate(BEARERTYPE.AUTHORIZATION,authorization,authTokenService,sessionManager);
} }
} }
public static void doJwtAuthenticate( public static void doJwtAuthenticate(
String bearerType, String bearerType,
String authorization, 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() { public static Authentication getAuthentication() {
Authentication authentication = (Authentication) getAuthentication(WebContext.getRequest()); Authentication authentication = (Authentication) getAuthentication(WebContext.getRequest());
return authentication; return authentication;
} }
public static Authentication getAuthentication(HttpServletRequest request) { public static Authentication getAuthentication(HttpServletRequest request) {
Authentication authentication = (Authentication) request.getSession().getAttribute(WebConstants.AUTHENTICATION); Authentication authentication = (Authentication) request.getSession().getAttribute(WebConstants.AUTHENTICATION);
return authentication; return authentication;
} }
//set Authentication to http session //set Authentication to http session
public static void setAuthentication(Authentication authentication) { public static void setAuthentication(Authentication authentication) {
WebContext.setAttribute(WebConstants.AUTHENTICATION, authentication); WebContext.setAttribute(WebConstants.AUTHENTICATION, authentication);
@ -118,24 +127,24 @@ public class AuthorizationUtils {
public static void clearAuthentication() { public static void clearAuthentication() {
WebContext.removeAttribute(WebConstants.AUTHENTICATION); WebContext.removeAttribute(WebConstants.AUTHENTICATION);
} }
public static boolean isAuthenticated() { public static boolean isAuthenticated() {
return getAuthentication() != null; return getAuthentication() != null;
} }
public static boolean isNotAuthenticated() { public static boolean isNotAuthenticated() {
return ! isAuthenticated(); return ! isAuthenticated();
} }
public static SignPrincipal getPrincipal() { public static SignPrincipal getPrincipal() {
Authentication authentication = getAuthentication(); Authentication authentication = getAuthentication();
return getPrincipal(authentication); return getPrincipal(authentication);
} }
public static SignPrincipal getPrincipal(Authentication authentication) { public static SignPrincipal getPrincipal(Authentication authentication) {
return authentication == null ? null : (SignPrincipal) authentication.getPrincipal(); return authentication == null ? null : (SignPrincipal) authentication.getPrincipal();
} }
public static UserInfo getUserInfo(Authentication authentication) { public static UserInfo getUserInfo(Authentication authentication) {
UserInfo userInfo = null; UserInfo userInfo = null;
SignPrincipal principal = getPrincipal(authentication); SignPrincipal principal = getPrincipal(authentication);
@ -144,9 +153,9 @@ public class AuthorizationUtils {
} }
return userInfo; return userInfo;
} }
public static UserInfo getUserInfo() { public static UserInfo getUserInfo() {
return getUserInfo(getAuthentication()); return getUserInfo(getAuthentication());
} }
} }

View File

@ -1,19 +1,19 @@
/* /*
* Copyright [2022] [MaxKey of copyright http://www.maxkey.top] * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.dromara.maxkey.authn.provider; package org.dromara.maxkey.authn.provider;
@ -45,37 +45,41 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetails;
/** /**
* login Authentication abstract class. * login Authentication abstract class.
* *
* @author Crystal.Sea * @author Crystal.Sea
* *
*/ */
public abstract class AbstractAuthenticationProvider { public abstract class AbstractAuthenticationProvider {
private static final Logger _logger = private static final Logger _logger =
LoggerFactory.getLogger(AbstractAuthenticationProvider.class); LoggerFactory.getLogger(AbstractAuthenticationProvider.class);
public static String PROVIDER_SUFFIX = "AuthenticationProvider"; public static String PROVIDER_SUFFIX = "AuthenticationProvider";
public class AuthType{ public class AuthType{
public static final String NORMAL = "normal"; public static final String NORMAL = "normal";
public static final String TFA = "tfa"; public static final String TFA = "tfa";
public static final String MOBILE = "mobile"; public static final String MOBILE = "mobile";
public static final String TRUSTED = "trusted"; public static final String TRUSTED = "trusted";
/**
* 扫描认证
*/
public static final String SCAN_CODE = "scancode";
} }
protected ApplicationConfig applicationConfig; protected ApplicationConfig applicationConfig;
protected AbstractAuthenticationRealm authenticationRealm; protected AbstractAuthenticationRealm authenticationRealm;
protected AbstractOtpAuthn tfaOtpAuthn; protected AbstractOtpAuthn tfaOtpAuthn;
protected MailOtpAuthnService otpAuthnService; protected MailOtpAuthnService otpAuthnService;
protected SessionManager sessionManager; protected SessionManager sessionManager;
protected AuthTokenService authTokenService; protected AuthTokenService authTokenService;
public static ArrayList<GrantedAuthority> grantedAdministratorsAuthoritys = new ArrayList<GrantedAuthority>(); public static ArrayList<GrantedAuthority> grantedAdministratorsAuthoritys = new ArrayList<GrantedAuthority>();
static { static {
grantedAdministratorsAuthoritys.add(new SimpleGrantedAuthority("ROLE_ADMINISTRATORS")); grantedAdministratorsAuthoritys.add(new SimpleGrantedAuthority("ROLE_ADMINISTRATORS"));
} }
@ -83,7 +87,7 @@ public abstract class AbstractAuthenticationProvider {
public abstract String getProviderName(); public abstract String getProviderName();
public abstract Authentication doAuthenticate(LoginCredential authentication); public abstract Authentication doAuthenticate(LoginCredential authentication);
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public boolean supports(Class authentication) { public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
@ -92,13 +96,13 @@ public abstract class AbstractAuthenticationProvider {
public Authentication authenticate(LoginCredential authentication){ public Authentication authenticate(LoginCredential authentication){
return null; return null;
} }
public Authentication authenticate(LoginCredential authentication,boolean trusted) { public Authentication authenticate(LoginCredential authentication,boolean trusted) {
return null; return null;
} }
/** /**
* createOnlineSession * createOnlineSession
* @param credential * @param credential
* @param userInfo * @param userInfo
* @return * @return
@ -112,7 +116,7 @@ public abstract class AbstractAuthenticationProvider {
List<GrantedAuthority> grantedAuthoritys = authenticationRealm.grantAuthority(userInfo); List<GrantedAuthority> grantedAuthoritys = authenticationRealm.grantAuthority(userInfo);
principal.setAuthenticated(true); principal.setAuthenticated(true);
for(GrantedAuthority administratorsAuthority : grantedAdministratorsAuthoritys) { for(GrantedAuthority administratorsAuthority : grantedAdministratorsAuthoritys) {
if(grantedAuthoritys.contains(administratorsAuthority)) { if(grantedAuthoritys.contains(administratorsAuthority)) {
principal.setRoleAdministrators(true); principal.setRoleAdministrators(true);
@ -120,37 +124,37 @@ public abstract class AbstractAuthenticationProvider {
} }
} }
_logger.debug("Granted Authority {}" , grantedAuthoritys); _logger.debug("Granted Authority {}" , grantedAuthoritys);
principal.setGrantedAuthorityApps(authenticationRealm.queryAuthorizedApps(grantedAuthoritys)); principal.setGrantedAuthorityApps(authenticationRealm.queryAuthorizedApps(grantedAuthoritys));
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
principal, principal,
"PASSWORD", "PASSWORD",
grantedAuthoritys grantedAuthoritys
); );
authenticationToken.setDetails( authenticationToken.setDetails(
new WebAuthenticationDetails(WebContext.getRequest())); new WebAuthenticationDetails(WebContext.getRequest()));
/* /*
* put Authentication to current session context * put Authentication to current session context
*/ */
session.setAuthentication(authenticationToken); session.setAuthentication(authenticationToken);
//create session //create session
this.sessionManager.create(session.getId(), session); this.sessionManager.create(session.getId(), session);
//set Authentication to http session //set Authentication to http session
AuthorizationUtils.setAuthentication(authenticationToken); AuthorizationUtils.setAuthentication(authenticationToken);
return authenticationToken; return authenticationToken;
} }
/** /**
* login user by j_username and j_cname first query user by j_cname if first * login user by j_username and j_cname first query user by j_cname if first
* step userinfo is null,query user from system. * step userinfo is null,query user from system.
* *
* @param username String * @param username String
* @param password String * @param password String
* @return * @return
@ -164,7 +168,7 @@ public abstract class AbstractAuthenticationProvider {
} else { } else {
_logger.debug("User Login. "); _logger.debug("User Login. ");
} }
} }
return userInfo; return userInfo;
@ -172,7 +176,7 @@ public abstract class AbstractAuthenticationProvider {
/** /**
* check input password empty. * check input password empty.
* *
* @param password String * @param password String
* @return * @return
*/ */
@ -185,7 +189,7 @@ public abstract class AbstractAuthenticationProvider {
/** /**
* check input username or password empty. * check input username or password empty.
* *
* @param email String * @param email String
* @return * @return
*/ */
@ -198,7 +202,7 @@ public abstract class AbstractAuthenticationProvider {
/** /**
* check input username empty. * check input username empty.
* *
* @param username String * @param username String
* @return * @return
*/ */
@ -219,8 +223,8 @@ public abstract class AbstractAuthenticationProvider {
loginUser.setDisplayName("not exist"); loginUser.setDisplayName("not exist");
loginUser.setLoginCount(0); loginUser.setLoginCount(0);
authenticationRealm.insertLoginHistory( authenticationRealm.insertLoginHistory(
loginUser, loginUser,
ConstsLoginType.LOCAL, ConstsLoginType.LOCAL,
"", "",
i18nMessage, i18nMessage,
WebConstants.LOGIN_RESULT.USER_NOT_EXIST); WebConstants.LOGIN_RESULT.USER_NOT_EXIST);
@ -228,22 +232,22 @@ public abstract class AbstractAuthenticationProvider {
} }
return true; return true;
} }
protected boolean statusValid(LoginCredential loginCredential , UserInfo userInfo) { protected boolean statusValid(LoginCredential loginCredential , UserInfo userInfo) {
if(userInfo.getIsLocked()==ConstsStatus.LOCK) { if(userInfo.getIsLocked()==ConstsStatus.LOCK) {
authenticationRealm.insertLoginHistory( authenticationRealm.insertLoginHistory(
userInfo, userInfo,
loginCredential.getAuthType(), loginCredential.getAuthType(),
loginCredential.getProvider(), loginCredential.getProvider(),
loginCredential.getCode(), loginCredential.getCode(),
WebConstants.LOGIN_RESULT.USER_LOCKED WebConstants.LOGIN_RESULT.USER_LOCKED
); );
}else if(userInfo.getStatus()!=ConstsStatus.ACTIVE) { }else if(userInfo.getStatus()!=ConstsStatus.ACTIVE) {
authenticationRealm.insertLoginHistory( authenticationRealm.insertLoginHistory(
userInfo, userInfo,
loginCredential.getAuthType(), loginCredential.getAuthType(),
loginCredential.getProvider(), loginCredential.getProvider(),
loginCredential.getCode(), loginCredential.getCode(),
WebConstants.LOGIN_RESULT.USER_INACTIVE WebConstants.LOGIN_RESULT.USER_INACTIVE
); );
} }

View File

@ -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;
}
}

View File

@ -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, "该二维码失效");
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,19 +1,19 @@
/* /*
* Copyright [2022] [MaxKey of copyright http://www.maxkey.top] * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.dromara.maxkey.autoconfigure; 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.AuthenticationProviderFactory;
import org.dromara.maxkey.authn.provider.impl.MobileAuthenticationProvider; import org.dromara.maxkey.authn.provider.impl.MobileAuthenticationProvider;
import org.dromara.maxkey.authn.provider.impl.NormalAuthenticationProvider; 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.provider.impl.TrustedAuthenticationProvider;
import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm; import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
import org.dromara.maxkey.authn.session.SessionManager; import org.dromara.maxkey.authn.session.SessionManager;
@ -44,21 +45,23 @@ import org.springframework.jdbc.core.JdbcTemplate;
@AutoConfiguration @AutoConfiguration
public class AuthnProviderAutoConfiguration { public class AuthnProviderAutoConfiguration {
static final Logger _logger = LoggerFactory.getLogger(AuthnProviderAutoConfiguration.class); static final Logger _logger = LoggerFactory.getLogger(AuthnProviderAutoConfiguration.class);
@Bean @Bean
public AbstractAuthenticationProvider authenticationProvider( public AbstractAuthenticationProvider authenticationProvider(
NormalAuthenticationProvider normalAuthenticationProvider, NormalAuthenticationProvider normalAuthenticationProvider,
MobileAuthenticationProvider mobileAuthenticationProvider, MobileAuthenticationProvider mobileAuthenticationProvider,
TrustedAuthenticationProvider trustedAuthenticationProvider TrustedAuthenticationProvider trustedAuthenticationProvider,
ScanCodeAuthenticationProvider scanCodeAuthenticationProvider
) { ) {
AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory(); AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory();
authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider); authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider);
authenticationProvider.addAuthenticationProvider(mobileAuthenticationProvider); authenticationProvider.addAuthenticationProvider(mobileAuthenticationProvider);
authenticationProvider.addAuthenticationProvider(trustedAuthenticationProvider); authenticationProvider.addAuthenticationProvider(trustedAuthenticationProvider);
authenticationProvider.addAuthenticationProvider(scanCodeAuthenticationProvider);
return authenticationProvider; return authenticationProvider;
} }
@Bean @Bean
public NormalAuthenticationProvider normalAuthenticationProvider( public NormalAuthenticationProvider normalAuthenticationProvider(
AbstractAuthenticationRealm authenticationRealm, AbstractAuthenticationRealm authenticationRealm,
@ -74,7 +77,18 @@ public class AuthnProviderAutoConfiguration {
authTokenService authTokenService
); );
} }
@Bean
public ScanCodeAuthenticationProvider scanCodeAuthenticationProvider(
AbstractAuthenticationRealm authenticationRealm,
SessionManager sessionManager
) {
return new ScanCodeAuthenticationProvider(
authenticationRealm,
sessionManager
);
}
@Bean @Bean
public MobileAuthenticationProvider mobileAuthenticationProvider( public MobileAuthenticationProvider mobileAuthenticationProvider(
AbstractAuthenticationRealm authenticationRealm, AbstractAuthenticationRealm authenticationRealm,
@ -104,22 +118,22 @@ public class AuthnProviderAutoConfiguration {
sessionManager sessionManager
); );
} }
@Bean @Bean
public PasswordPolicyValidator passwordPolicyValidator(JdbcTemplate jdbcTemplate,MessageSource messageSource) { public PasswordPolicyValidator passwordPolicyValidator(JdbcTemplate jdbcTemplate,MessageSource messageSource) {
return new PasswordPolicyValidator(jdbcTemplate,messageSource); return new PasswordPolicyValidator(jdbcTemplate,messageSource);
} }
@Bean @Bean
public LoginRepository loginRepository(JdbcTemplate jdbcTemplate) { public LoginRepository loginRepository(JdbcTemplate jdbcTemplate) {
return new LoginRepository(jdbcTemplate); return new LoginRepository(jdbcTemplate);
} }
@Bean @Bean
public LoginHistoryRepository loginHistoryRepository(JdbcTemplate jdbcTemplate) { public LoginHistoryRepository loginHistoryRepository(JdbcTemplate jdbcTemplate) {
return new LoginHistoryRepository(jdbcTemplate); return new LoginHistoryRepository(jdbcTemplate);
} }
/** /**
* remeberMeService . * remeberMeService .
* @return * @return
@ -135,5 +149,5 @@ public class AuthnProviderAutoConfiguration {
return new JdbcRemeberMeManager( return new JdbcRemeberMeManager(
jdbcTemplate,applicationConfig,authTokenService,validity); jdbcTemplate,applicationConfig,authTokenService,validity);
} }
} }

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -61,6 +61,8 @@ export class UserLoginComponent implements OnInit, OnDestroy {
state = ''; state = '';
count = 0; count = 0;
interval$: any; interval$: any;
//二维码内容
ticket = '';
constructor( constructor(
fb: FormBuilder, fb: FormBuilder,
@ -298,25 +300,63 @@ export class UserLoginComponent implements OnInit, OnDestroy {
}); });
} }
/**
*
*/
getLoginQrCode() { getLoginQrCode() {
this.qrexpire = false; this.qrexpire = false;
this.qrCodeService.getLoginQrCode().subscribe(res => { this.qrCodeService.getLoginQrCode().subscribe(res => {
if (res.code === 0 && res.data.rqCode) { // 使用返回的 rqCode if (res.code === 0 && res.data.rqCode) { // 使用返回的 rqCode
const qrImageElement = document.getElementById('div_qrcodelogin'); const qrImageElement = document.getElementById('div_qrcodelogin');
this.ticket = res.data.ticket;
if (qrImageElement) { if (qrImageElement) {
qrImageElement.innerHTML = `<img src="${res.data.rqCode}" alt="QR Code" style="width: 200px; height: 200px;">`; qrImageElement.innerHTML = `<img src="${res.data.rqCode}" alt="QR Code" style="width: 200px; height: 200px;">`;
} }
// 设置三分钟后 qrexpire 为 false /* // 5 qrexpire false
setTimeout(() => { setTimeout(() => {
this.qrexpire = true; this.qrexpire = true;
this.cdr.detectChanges(); // 更新视图 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 { getQrCode(): void {

View File

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

View File

@ -20,8 +20,12 @@ package org.dromara.maxkey.web.contorller;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.text.ParseException; import java.text.ParseException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dromara.maxkey.authn.LoginCredential; 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.AuthJwt;
import org.dromara.maxkey.authn.jwt.AuthTokenService; import org.dromara.maxkey.authn.jwt.AuthTokenService;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider; 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.crypto.password.PasswordReciprocal;
import org.dromara.maxkey.entity.*; import org.dromara.maxkey.entity.*;
import org.dromara.maxkey.entity.idm.UserInfo; 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.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.password.sms.SmsOtpAuthnService; 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.SocialsAssociatesService;
import org.dromara.maxkey.persistence.service.UserInfoService; import org.dromara.maxkey.persistence.service.UserInfoService;
import org.dromara.maxkey.util.RQCodeUtils; import org.dromara.maxkey.util.RQCodeUtils;
@ -51,6 +56,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -277,11 +283,9 @@ public class LoginEntryPoint {
@GetMapping("/genScanCode") @GetMapping("/genScanCode")
public Message<HashMap<String,String>> genScanCode() { public Message<HashMap<String,String>> genScanCode() {
log.debug("/genScanCode."); log.debug("/genScanCode.");
UserInfo userInfo = AuthorizationUtils.getUserInfo(); String ticket = scanCodeService.createTicket();
Long ticket = scanCodeService.createTicket(); log.debug("ticket: {}",ticket);
String ticketString = userInfo == null ? ticket.toString() : ticket+","+userInfo.getId(); String encodeTicket = PasswordReciprocal.getInstance().encode(ticket);
log.debug("ticket string {}",ticketString);
String encodeTicket = PasswordReciprocal.getInstance().encode(ticketString);
BufferedImage bufferedImage = RQCodeUtils.write2BufferedImage(encodeTicket, "gif", 300, 300); BufferedImage bufferedImage = RQCodeUtils.write2BufferedImage(encodeTicket, "gif", 300, 300);
String rqCode = Base64Utils.encodeImage(bufferedImage); String rqCode = Base64Utils.encodeImage(bufferedImage);
HashMap<String,String> codeMap = new HashMap<>(); HashMap<String,String> codeMap = new HashMap<>();
@ -289,4 +293,53 @@ public class LoginEntryPoint {
codeMap.put("ticket", encodeTicket); codeMap.put("ticket", encodeTicket);
return new Message<>(Message.SUCCESS, codeMap); 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, "成功");
}
} }