二次认证优化

This commit is contained in:
MaxKey 2025-07-28 07:41:31 +08:00
parent f32bcff8ae
commit 95a7c0daa1
41 changed files with 1647 additions and 21 deletions

View File

@ -37,6 +37,8 @@ public class SignPrincipal implements UserDetails {
String sessionId;
int twoFactor;
List<GrantedAuthority> grantedAuthority;
List<GrantedAuthority> grantedAuthorityApps;
@ -204,6 +206,18 @@ public class SignPrincipal implements UserDetails {
}
}
public int getTwoFactor() {
return twoFactor;
}
public void setTwoFactor(int twoFactor) {
this.twoFactor = twoFactor;
}
public void clearTwoFactor() {
this.twoFactor = 0;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();

View File

@ -104,7 +104,7 @@ public class AuthJwt implements Serializable {
this.email = principal.getUserInfo().getEmail();
this.instId = principal.getUserInfo().getInstId();
this.instName = principal.getUserInfo().getInstName();
this.twoFactor =principal.getTwoFactor();
this.authorities = new ArrayList<>();
for(GrantedAuthority grantedAuthority :authentication.getAuthorities()) {
this.authorities.add(grantedAuthority.getAuthority());

View File

@ -22,6 +22,7 @@ import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import org.dromara.maxkey.authn.SignPrincipal;
import org.dromara.maxkey.constants.ConstsJwt;
import org.dromara.maxkey.crypto.jwt.Hmac512Service;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.web.WebContext;
@ -61,7 +62,8 @@ public class AuthJwtService {
.expirationTime(expirationTime)
.claim("locale", userInfo.getLocale())
.claim("kid", Hmac512Service.MXK_AUTH_JWK)
.claim("institution", userInfo.getInstId())
.claim(ConstsJwt.USER_ID, userInfo.getId())
.claim(ConstsJwt.INST_ID, userInfo.getInstId())
.build();
return signedJWT(jwtClaims);

View File

@ -41,4 +41,10 @@ public interface SessionManager {
public void terminate(String sessionId,String userId,String username);
public void visited(String sessionId , VisitedDto visited);
public void createTwoFactor(String sessionId, Session session);
public Session removeTwoFactor(String sessionId);
public Session getTwoFactor(String sessionId);
}

View File

@ -38,21 +38,30 @@ public class InMemorySessionManager implements SessionManager{
static final long CACHE_MAXIMUM_SIZE = 2000000;
protected int validitySeconds = 60 * 30; //default 30 minutes.
protected static Cache<String, Session> sessionStore =
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(CACHE_MAXIMUM_SIZE)
.build();
Cache<String, Session> sessionStore;
Cache<String, Session> sessionTwoFactorStore;
public InMemorySessionManager(int validitySeconds) {
super();
this.validitySeconds = validitySeconds;
if(validitySeconds > 0) {
sessionStore =
Caffeine.newBuilder()
.expireAfterWrite(validitySeconds, TimeUnit.SECONDS)
.maximumSize(CACHE_MAXIMUM_SIZE)
.build();
}else {
sessionStore = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(CACHE_MAXIMUM_SIZE)
.build();
}
sessionTwoFactorStore = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(CACHE_MAXIMUM_SIZE)
.build();
}
@Override
@ -128,4 +137,23 @@ public class InMemorySessionManager implements SessionManager{
}
}
@Override
public void createTwoFactor(String sessionId, Session session) {
session.setExpiredTime(session.getLastAccessTime().plusSeconds(validitySeconds));
sessionTwoFactorStore.put(sessionId, session);
}
@Override
public Session removeTwoFactor(String sessionId) {
Session session = sessionTwoFactorStore.getIfPresent(sessionId);
sessionTwoFactorStore.invalidate(sessionId);
return session;
}
@Override
public Session getTwoFactor(String sessionId) {
Session session = sessionTwoFactorStore.getIfPresent(sessionId);
return session;
}
}

View File

@ -35,9 +35,14 @@ public class RedisSessionManager implements SessionManager {
protected int validitySeconds = 60 * 30; //default 30 minutes.
int twoFactorValidity = 10 * 60; //default 10 minutes.
RedisConnectionFactory connectionFactory;
public static String PREFIX="MXK_SESSION_";
public static final String PREFIX = "MXK_SESSION_";
public static final String PREFIX_TWOFACTOR = "mxk:session:twofactor:%s";
public String getKey(String sessionId) {
return PREFIX + sessionId;
@ -150,4 +155,34 @@ public class RedisSessionManager implements SessionManager {
}
}
public String formatTwoFactorKey(String sessionId) {
return PREFIX_TWOFACTOR.formatted(sessionId) ;
}
@Override
public void createTwoFactor(String sessionId, Session session) {
session.setExpiredTime(session.getLastAccessTime().plusSeconds(validitySeconds));
RedisConnection conn = connectionFactory.getConnection();
conn.setexObject( formatTwoFactorKey(sessionId), twoFactorValidity, session);
conn.close();
}
@Override
public Session removeTwoFactor(String sessionId) {
RedisConnection conn = connectionFactory.getConnection();
Session ticket = conn.getObject(formatTwoFactorKey(sessionId));
conn.delete(formatTwoFactorKey(sessionId));
conn.close();
return ticket;
}
@Override
public Session getTwoFactor(String sessionId) {
RedisConnection conn = connectionFactory.getConnection();
Session session = conn.getObject(formatTwoFactorKey(sessionId));
conn.close();
return session;
}
}

View File

@ -207,4 +207,35 @@ public class SessionManagerImpl implements SessionManager{
redisSessionManager.visited(sessionId,visited);
}
}
@Override
public void createTwoFactor(String sessionId, Session session) {
if(isRedis) {
redisSessionManager.createTwoFactor(sessionId, session);
}else {
inMemorySessionManager.createTwoFactor(sessionId, session);
}
}
@Override
public Session removeTwoFactor(String sessionId) {
Session session = null;
if(isRedis) {
session = redisSessionManager.removeTwoFactor(sessionId);
}else {
session = inMemorySessionManager.removeTwoFactor(sessionId);
}
return session;
}
@Override
public Session getTwoFactor(String sessionId) {
Session session = null;
if(isRedis) {
session = redisSessionManager.getTwoFactor(sessionId);
}else {
session = inMemorySessionManager.getTwoFactor(sessionId);
}
return session;
}
}

View File

@ -20,6 +20,7 @@ package org.dromara.maxkey.authn.provider;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.dromara.maxkey.authn.LoginCredential;
import org.dromara.maxkey.authn.SignPrincipal;
import org.dromara.maxkey.authn.jwt.AuthTokenService;
@ -30,6 +31,7 @@ import org.dromara.maxkey.authn.web.AuthorizationUtils;
import org.dromara.maxkey.configuration.ApplicationConfig;
import org.dromara.maxkey.constants.ConstsLoginType;
import org.dromara.maxkey.constants.ConstsStatus;
import org.dromara.maxkey.constants.ConstsTwoFactor;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.password.onetimepwd.MailOtpAuthnService;
@ -93,6 +95,10 @@ public abstract class AbstractAuthenticationProvider {
public abstract Authentication doAuthenticate(LoginCredential authentication);
public Authentication doTwoFactorAuthenticate(LoginCredential credential , UserInfo user) {
return null;
}
@SuppressWarnings("rawtypes")
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
@ -147,6 +153,13 @@ public abstract class AbstractAuthenticationProvider {
*/
session.setAuthentication(authenticationToken);
if(credential.getAuthType().equalsIgnoreCase(AuthType.NORMAL)
&& userInfo.getAuthnType() > ConstsTwoFactor.NONE ) {
//用户配置二次认证
principal.setTwoFactor(userInfo.getAuthnType());
this.sessionManager.createTwoFactor(session.getId(), session);
}
//create session
this.sessionManager.create(session.getId(), session);
@ -259,4 +272,17 @@ public abstract class AbstractAuthenticationProvider {
return true;
}
/**
* check input otp empty.
*
* @param password String
* @return
*/
protected boolean emptyOtpCaptchaValid(String otpCaptcha) {
if (StringUtils.isBlank(otpCaptcha)) {
throw new BadCredentialsException(WebContext.getI18nValue("login.error.otpCaptcha.null"));
}
return true;
}
}

View File

@ -0,0 +1,127 @@
package org.dromara.maxkey.authn.provider.twofactor;
import java.util.HashMap;
import java.util.Map;
import org.dromara.maxkey.authn.LoginCredential;
import org.dromara.maxkey.authn.SignPrincipal;
import org.dromara.maxkey.authn.jwt.AuthTokenService;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
import org.dromara.maxkey.authn.session.Session;
import org.dromara.maxkey.authn.session.SessionManager;
import org.dromara.maxkey.authn.web.AuthorizationUtils;
import org.dromara.maxkey.constants.ConstsJwt;
import org.dromara.maxkey.constants.ConstsLoginType;
import org.dromara.maxkey.constants.ConstsTwoFactor;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.persistence.service.LoginService;
import org.dromara.maxkey.web.WebConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import com.nimbusds.jwt.JWTClaimsSet;
/**
* TwoFactor Authentication provider.双因素认证提供者
*
* @author Crystal.Sea
*
*/
public class TwoFactorAuthenticationProvider extends AbstractAuthenticationProvider {
private static final Logger logger =LoggerFactory.getLogger(TwoFactorAuthenticationProvider.class);
Map<String,AbstractAuthenticationProvider> twoFactorProvider = new HashMap<>();
public String getProviderName() {
return "twoFactor" + PROVIDER_SUFFIX;
}
public TwoFactorAuthenticationProvider(
AbstractAuthenticationRealm authenticationRealm,
SessionManager sessionManager,
LoginService loginService,
AuthTokenService authTokenService) {
this.authenticationRealm = authenticationRealm;
this.sessionManager = sessionManager;
this.authTokenService = authTokenService;
}
public void addProvider(int twoFactor,AbstractAuthenticationProvider provider) {
twoFactorProvider.put(twoFactor+"", provider);
}
@Override
public Authentication doAuthenticate(LoginCredential credential) {
logger.debug("Credential {}" , credential);
emptyOtpCaptchaValid(credential.getOtpCaptcha());
try {
if(authTokenService.validateJwtToken(credential.getJwtToken())) {
//解析refreshToken转换会话id
JWTClaimsSet claim = authTokenService.resolve(credential.getJwtToken());
String sessionId = claim.getJWTID();
String userId = claim.getClaim(ConstsJwt.USER_ID).toString();
//String style = claim.getClaim(AuthorizationUtils.STYLE).toString();
//尝试刷新会话
logger.trace("Try to get user {} , sessionId [{}]" , userId, sessionId);
Session session = sessionManager.getTwoFactor(sessionId);
if(session != null) {//有会话
Authentication twoFactorAuth = null;
SignPrincipal principal =(SignPrincipal) session.getAuthentication().getPrincipal();
String loginType;
switch(principal.getTwoFactor()) {
case ConstsTwoFactor.TOTP -> {
loginType = ConstsLoginType.TwoFactor.TWO_FACTOR_TOTP;
}
case ConstsTwoFactor.EMAIL -> {
loginType = ConstsLoginType.TwoFactor.TWO_FACTOR_EMAIL;
}
case ConstsTwoFactor.SMS -> {
loginType = ConstsLoginType.TwoFactor.TWO_FACTOR_MOBILE;
}
default ->{
loginType = ConstsLoginType.TwoFactor.TWO_FACTOR_TOTP;
}
}
logger.debug("loginType {}",loginType);
AbstractAuthenticationProvider authenticationProvider = twoFactorProvider.get(principal.getTwoFactor()+"");
logger.debug("Provider {}",authenticationProvider.getProviderName());
UserInfo user = authenticationRealm.loadUserInfoById(userId);
//进行二次认证校验
twoFactorAuth = authenticationProvider.doTwoFactorAuthenticate(credential , user);
if(twoFactorAuth != null) {
logger.debug("twoFactorAuth success .");
//设置正常状态
principal.clearTwoFactor();
//重新设置令牌参数
sessionManager.create(sessionId, session);
sessionManager.removeTwoFactor(sessionId);
AuthorizationUtils.setAuthentication(session.getAuthentication());
authenticationRealm.insertLoginHistory(user,
loginType,
"",
"xe00000004",
WebConstants.LOGIN_RESULT.SUCCESS);
return session.getAuthentication();
}else {
logger.debug("twoFactorAuth fail .");
}
}else {//无会话
logger.debug("Session is timeout , sessionId [{}]" , sessionId);
}
}else {//验证失效
logger.debug("jwt token is not validate .");
}
}catch(Exception e) {
logger.error("Exception !",e);
}
return null;
}
}

View File

@ -0,0 +1,90 @@
package org.dromara.maxkey.authn.provider.twofactor.impl;
import org.dromara.maxkey.authn.LoginCredential;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.password.onetimepwd.MailOtpAuthnService;
import org.dromara.maxkey.web.WebConstants;
import org.dromara.maxkey.web.WebContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* TwoFactor Authentication provider.二次认证邮件认证提供者
*
* @author Crystal.Sea
*
*/
public class TwoFactorEmailAuthenticationProvider extends AbstractAuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(TwoFactorEmailAuthenticationProvider.class);
MailOtpAuthnService mailOtpAuthnService;
public String getProviderName() {
return "twoFactorEmail" + PROVIDER_SUFFIX;
}
public TwoFactorEmailAuthenticationProvider(MailOtpAuthnService mailOtpAuthnService) {
this.mailOtpAuthnService = mailOtpAuthnService;
}
@Override
public Authentication doAuthenticate(LoginCredential credential) {
return null;
}
@Override
public Authentication doTwoFactorAuthenticate(LoginCredential credential,UserInfo user) {
UsernamePasswordAuthenticationToken authenticationToken = null;
logger.debug("loginCredential {}" , credential);
try {
//短信验证码校验
matches(credential.getOtpCaptcha(),user);
authenticationToken = new UsernamePasswordAuthenticationToken(credential.getUsername(),"email");
} catch (AuthenticationException e) {
logger.error("Failed to authenticate user {} via {}: {}",credential.getPrincipal(),
getProviderName(),
e.getMessage() );
WebContext.setAttribute(
WebConstants.LOGIN_ERROR_SESSION_MESSAGE, e.getMessage());
} catch (Exception e) {
logger.error("Login error Unexpected exception in {} authentication:\n{}" ,
getProviderName(), e.getMessage());
}
return authenticationToken;
}
/**
* mobile validate.手机验证码校验
*
* @param otpCaptcha String
* @param authType String
* @param userInfo UserInfo
*/
protected void matches(String captcha, UserInfo userInfo) {
// for mobile password
UserInfo validUserInfo = new UserInfo();
validUserInfo.setUsername(userInfo.getUsername());
validUserInfo.setId(userInfo.getId());
AbstractOtpAuthn smsOtpAuthn = mailOtpAuthnService.getMailOtpAuthn(userInfo.getInstId());
if (captcha == null || !smsOtpAuthn.validate(validUserInfo, captcha)) {
String message = WebContext.getI18nValue("login.error.captcha");
logger.debug("login captcha valid error.");
throw new BadCredentialsException(message);
}
}
}

View File

@ -0,0 +1,88 @@
package org.dromara.maxkey.authn.provider.twofactor.impl;
import org.dromara.maxkey.authn.LoginCredential;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.password.sms.SmsOtpAuthnService;
import org.dromara.maxkey.web.WebConstants;
import org.dromara.maxkey.web.WebContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* TwoFactorMobile Authentication provider.二次认证手机认证提供者
*
* @author Crystal.Sea
*
*/
public class TwoFactorMobileAuthenticationProvider extends AbstractAuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(TwoFactorMobileAuthenticationProvider.class);
SmsOtpAuthnService smsOtpAuthnService;
public String getProviderName() {
return "twoFactorMobile" + PROVIDER_SUFFIX;
}
public TwoFactorMobileAuthenticationProvider(SmsOtpAuthnService smsOtpAuthnService) {
this.smsOtpAuthnService = smsOtpAuthnService;
}
@Override
public Authentication doAuthenticate(LoginCredential credential) {
return null;
}
@Override
public Authentication doTwoFactorAuthenticate(LoginCredential credential,UserInfo user) {
UsernamePasswordAuthenticationToken authenticationToken = null;
logger.debug("loginCredential {}" , credential);
try {
//短信验证码校验
matches(credential.getOtpCaptcha(),user);
authenticationToken = new UsernamePasswordAuthenticationToken(credential.getUsername(),"mobile");
} catch (AuthenticationException e) {
logger.error("Failed to authenticate user {} via {}: {}",credential.getPrincipal(),
getProviderName(),
e.getMessage() );
WebContext.setAttribute(WebConstants.LOGIN_ERROR_SESSION_MESSAGE, e.getMessage());
} catch (Exception e) {
logger.error("Login error Unexpected exception in {} authentication:\n{}" ,getProviderName(), e.getMessage());
}
return authenticationToken;
}
/**
* mobile validate.手机验证码校验
*
* @param otpCaptcha String
* @param authType String
* @param userInfo UserInfo
*/
protected void matches(String captcha, UserInfo userInfo) {
// for mobile password
UserInfo validUserInfo = new UserInfo();
validUserInfo.setUsername(userInfo.getUsername());
validUserInfo.setId(userInfo.getId());
AbstractOtpAuthn smsOtpAuthn = smsOtpAuthnService.getByInstId(userInfo.getInstId());
if (captcha == null || !smsOtpAuthn.validate(validUserInfo, captcha)) {
String message = WebContext.getI18nValue("login.error.captcha");
logger.debug("login captcha valid error.");
throw new BadCredentialsException(message);
}
}
}

View File

@ -0,0 +1,84 @@
package org.dromara.maxkey.authn.provider.twofactor.impl;
import org.dromara.maxkey.authn.LoginCredential;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
import org.dromara.maxkey.entity.idm.UserInfo;
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.web.WebConstants;
import org.dromara.maxkey.web.WebContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* TwoFactorTotp Authentication provider.二次认证TOTP认证提供者
*
* @author Crystal.Sea
*
*/
public class TwoFactorTotpAuthenticationProvider extends AbstractAuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(TwoFactorTotpAuthenticationProvider.class);
public String getProviderName() {
return "twoFactorTotp" + PROVIDER_SUFFIX;
}
public TwoFactorTotpAuthenticationProvider(AbstractAuthenticationRealm authenticationRealm,AbstractOtpAuthn tfaOtpAuthn) {
this.authenticationRealm = authenticationRealm;
this.tfaOtpAuthn = tfaOtpAuthn;
}
@Override
public Authentication doAuthenticate(LoginCredential credential) {
return null;
}
@Override
public Authentication doTwoFactorAuthenticate(LoginCredential credential,UserInfo user) {
UsernamePasswordAuthenticationToken authenticationToken = null;
logger.debug("loginCredential {}" , credential);
try {
//验证码校验
UserInfo userTotp = authenticationRealm.loadUserInfoById(user.getId());
matches(credential.getOtpCaptcha(),userTotp.getSharedSecret());
authenticationToken = new UsernamePasswordAuthenticationToken(credential.getUsername(),"TOTP");
} catch (AuthenticationException e) {
logger.error("Failed to authenticate user {} via {}: {}",credential.getPrincipal(),
getProviderName(),
e.getMessage() );
WebContext.setAttribute(WebConstants.LOGIN_ERROR_SESSION_MESSAGE, e.getMessage());
} catch (Exception e) {
logger.error("Login error Unexpected exception in {} authentication:\n{}" , getProviderName(), e.getMessage());
}
return authenticationToken;
}
/**
* 双因素验证.
*
* @param otpCaptcha String
* @param authType String
* @param userInfo UserInfo
*/
protected void matches(String captcha, String sharedSecret) {
// for one time password 2 factor
if (captcha == null || !tfaOtpAuthn.validate(sharedSecret, captcha)) {
String message = WebContext.getI18nValue("login.error.captcha");
logger.debug("login captcha valid error.");
throw new BadCredentialsException(message);
}
}
}

View File

@ -86,6 +86,10 @@ public abstract class AbstractAuthenticationRealm {
return loginService.find(username, password);
}
public UserInfo loadUserInfoById(String userId) {
return loginService.findById(userId);
}
public abstract boolean passwordMatches(UserInfo userInfo, String password);
public List<Groups> queryGroups(UserInfo userInfo) {

View File

@ -21,17 +21,27 @@ import org.dromara.maxkey.authn.jwt.AuthTokenService;
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
import org.dromara.maxkey.authn.provider.AuthenticationProviderFactory;
import org.dromara.maxkey.authn.provider.impl.*;
import org.dromara.maxkey.authn.provider.twofactor.TwoFactorAuthenticationProvider;
import org.dromara.maxkey.authn.provider.twofactor.impl.TwoFactorEmailAuthenticationProvider;
import org.dromara.maxkey.authn.provider.twofactor.impl.TwoFactorMobileAuthenticationProvider;
import org.dromara.maxkey.authn.provider.twofactor.impl.TwoFactorTotpAuthenticationProvider;
import org.dromara.maxkey.authn.realm.AbstractAuthenticationRealm;
import org.dromara.maxkey.authn.session.SessionManager;
import org.dromara.maxkey.authn.support.rememberme.AbstractRemeberMeManager;
import org.dromara.maxkey.authn.support.rememberme.JdbcRemeberMeManager;
import org.dromara.maxkey.configuration.ApplicationConfig;
import org.dromara.maxkey.constants.ConstsTwoFactor;
import org.dromara.maxkey.ip2location.IpLocationParser;
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
import org.dromara.maxkey.password.onetimepwd.MailOtpAuthnService;
import org.dromara.maxkey.password.sms.SmsOtpAuthnService;
import org.dromara.maxkey.persistence.service.CnfPasswordPolicyService;
import org.dromara.maxkey.persistence.service.LoginService;
import org.dromara.maxkey.persistence.service.PasswordPolicyValidatorService;
import org.dromara.maxkey.persistence.service.impl.PasswordPolicyValidatorServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.MessageSource;
@ -49,7 +59,8 @@ public class AuthnProviderAutoConfiguration {
MobileAuthenticationProvider mobileAuthenticationProvider,
TrustedAuthenticationProvider trustedAuthenticationProvider,
ScanCodeAuthenticationProvider scanCodeAuthenticationProvider,
AppAuthenticationProvider appAuthenticationProvider
AppAuthenticationProvider appAuthenticationProvider,
TwoFactorAuthenticationProvider twoFactorAuthenticationProvider
) {
AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory();
authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider);
@ -58,6 +69,9 @@ public class AuthnProviderAutoConfiguration {
authenticationProvider.addAuthenticationProvider(scanCodeAuthenticationProvider);
authenticationProvider.addAuthenticationProvider(appAuthenticationProvider);
//二次认证
authenticationProvider.addAuthenticationProvider(twoFactorAuthenticationProvider);
return authenticationProvider;
}
@ -156,4 +170,47 @@ public class AuthnProviderAutoConfiguration {
jdbcTemplate,applicationConfig,authTokenService,validity);
}
@Bean
TwoFactorAuthenticationProvider twoFactorAuthenticationProvider(
AbstractAuthenticationRealm authenticationRealm,
SessionManager sessionManager,
LoginService loginService,
AuthTokenService authTokenService,
IpLocationParser ipLocationParser,
TwoFactorTotpAuthenticationProvider twoFactorTotpAuthenticationProvider,
TwoFactorMobileAuthenticationProvider twoFactorMobileAuthenticationProvider,
TwoFactorEmailAuthenticationProvider twoFactorEmailAuthenticationProvider) {
_logger.debug("init TwoFactor authentication Provider .");
TwoFactorAuthenticationProvider twoFactorProvider =new TwoFactorAuthenticationProvider(
authenticationRealm,
sessionManager,
loginService,
authTokenService
);
twoFactorProvider.addProvider(ConstsTwoFactor.TOTP, twoFactorTotpAuthenticationProvider);
twoFactorProvider.addProvider(ConstsTwoFactor.EMAIL, twoFactorEmailAuthenticationProvider);
twoFactorProvider.addProvider(ConstsTwoFactor.SMS, twoFactorMobileAuthenticationProvider);
return twoFactorProvider;
}
@Bean
TwoFactorTotpAuthenticationProvider twoFactorTotpAuthenticationProvider(@Qualifier("tfaOtpAuthn") AbstractOtpAuthn tfaOtpAuthn,
AbstractAuthenticationRealm authenticationRealm) {
_logger.debug("init TwoFactor authentication Provider .");
return new TwoFactorTotpAuthenticationProvider(authenticationRealm,tfaOtpAuthn);
}
@Bean
TwoFactorMobileAuthenticationProvider twoFactorMobileAuthenticationProvider(SmsOtpAuthnService smsOtpAuthnService) {
_logger.debug("init TwoFactor Mobile authentication Provider .");
return new TwoFactorMobileAuthenticationProvider(smsOtpAuthnService);
}
@Bean
TwoFactorEmailAuthenticationProvider twoFactorEmailAuthenticationProvider(MailOtpAuthnService mailOtpAuthnService) {
_logger.debug("init TwoFactor Email authentication Provider .");
return new TwoFactorEmailAuthenticationProvider(mailOtpAuthnService);
}
}

View File

@ -0,0 +1,30 @@
package org.dromara.maxkey.constants;
/**
* Jwt.
* @author Crystal.Sea
*
*/
public final class ConstsJwt {
public static final String ACCESS_TOKEN = "access_token";
public static final String REFRESH_TOKEN = "refresh_token";
public static final String EXPIRES_IN = "expired";
public static final String INST_ID = "instId";
public static final String USER_ID = "userId";
public static final String STYLE = "style";
public static final String TWO_FACTOR = "twoFactor";
public static final String KID = "kid";
public static final String LOCALE = "locale";
}

View File

@ -43,4 +43,19 @@ public class ConstsLoginType {
public static final String HTTPHEADER = "HttpHeader";
public static final class TwoFactor{
/**
* 1=TOTP(动态验证码)
*/
public static final String TWO_FACTOR_TOTP = "TwoFactorTotp";
/**
* 2=邮箱验证码
*/
public static final String TWO_FACTOR_EMAIL = "TwoFactorEmail";
/**
* 3=手机短信
*/
public static final String TWO_FACTOR_MOBILE = "TwoFactorMobile";
}
}

View File

@ -0,0 +1,27 @@
package org.dromara.maxkey.constants;
/**
* 二次认证验证码
*/
public class ConstsTwoFactor {
/**
*
*/
public static final int NONE = 0;
/**
* 动态令牌TOTP
*/
public static final int TOTP = 1;
/**
* 邮件验证码
*/
public static final int EMAIL = 2;
/**
* 短信验证码
*/
public static final int SMS = 3;
}

View File

@ -27,6 +27,8 @@ public interface LoginService {
public UserInfo find(String username, String password);
public UserInfo findById(String userId);
public List<UserInfo> findByUsername(String username, String password);
public List<UserInfo> findByUsernameOrMobile(String username, String password);

View File

@ -59,6 +59,8 @@ public class LoginServiceImpl implements LoginService{
private static final String DEFAULT_USERINFO_SELECT_STATEMENT = "select * from mxk_userinfo where username = ? ";
private static final String DEFAULT_USERINFO_SELECT_STATEMENT_BY_ID = "select * from mxk_userinfo where id = ? ";
private static final String DEFAULT_USERINFO_SELECT_STATEMENT_USERNAME_MOBILE = "select * from mxk_userinfo where (username = ? or mobile = ?)";
private static final String DEFAULT_USERINFO_SELECT_STATEMENT_USERNAME_MOBILE_EMAIL = "select * from mxk_userinfo where (username = ? or mobile = ? or email = ?) ";
@ -491,6 +493,16 @@ public class LoginServiceImpl implements LoginService{
return userInfo;
}
}
@Override
public UserInfo findById(String userId) {
List<UserInfo> listUserInfo = jdbcTemplate.query(
DEFAULT_USERINFO_SELECT_STATEMENT_BY_ID,
new UserInfoRowMapper(),
userId
);
return (CollectionUtils.isNotEmpty(listUserInfo) ? listUserInfo.get(0) : null);
}
}

View File

@ -70,6 +70,8 @@ public abstract class AbstractOtpAuthn {
public abstract boolean validate(UserInfo userInfo, String token);
public abstract boolean validate(String sharedSecret, String token);
protected String defaultProduce(UserInfo userInfo) {
return genToken(userInfo);
}

View File

@ -48,4 +48,10 @@ public class CapOtpAuthn extends AbstractOtpAuthn {
return false;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -72,4 +72,10 @@ public class CounterBasedOtpAuthn extends AbstractOtpAuthn {
return false;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -95,4 +95,10 @@ public class HotpOtpAuthn extends AbstractOtpAuthn {
this.truncation = truncation;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -122,6 +122,12 @@ public class MailOtpAuthn extends AbstractOtpAuthn {
this.messageTemplate = messageTemplate;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -38,4 +38,10 @@ public class MobileOtpAuthn extends AbstractOtpAuthn {
return false;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -48,4 +48,10 @@ public class RsaOtpAuthn extends AbstractOtpAuthn {
return false;
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

View File

@ -51,9 +51,14 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn {
@Override
public boolean validate(UserInfo userInfo, String token) {
return validate(userInfo.getSharedSecret() , token);
}
@Override
public boolean validate(String secret, String token) {
_logger.debug("utcTime : {}" , dateFormat.format(new Date()));
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String sharedSecret = PasswordReciprocal.getInstance().decoder(userInfo.getSharedSecret());
String sharedSecret = PasswordReciprocal.getInstance().decoder(secret);
byte[] byteSharedSecret = Base32Utils.decode(sharedSecret);
String hexSharedSecret = Hex.encodeHexString(byteSharedSecret);
String timeBasedToken = "";
@ -79,7 +84,6 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn {
return true;
}
return false;
}
}

View File

@ -56,4 +56,10 @@ public class SmsOtpAuthn extends AbstractOtpAuthn {
}
@Override
public boolean validate(String sharedSecret, String token) {
// TODO Auto-generated method stub
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -291,9 +291,14 @@ export class UserLoginComponent implements OnInit, OnDestroy {
} else {
// 清空路由复用信息
this.reuseTabService.clear();
if (res.data.twoFactor === '0') {
// 设置用户Token信息
this.authnService.auth(res.data);
this.authnService.navigate({});
} else {
localStorage.setItem(CONSTS.TWO_FACTOR_DATA, JSON.stringify(res.data));
this.router.navigateByUrl('/passport/tfa');
}
}
this.cdr.detectChanges();
});

View File

@ -26,6 +26,7 @@ import { UserLoginComponent } from './login/login.component';
import { LogoutComponent } from './logout.component';
import { UserRegisterResultComponent } from './register-result/register-result.component';
import { UserRegisterComponent } from './register/register.component';
import { TfaComponent } from './tfa/tfa.component';
const routes: Routes = [
// passport
@ -38,6 +39,11 @@ const routes: Routes = [
component: UserLoginComponent,
data: { title: '登录', titleI18n: 'app.login.login' }
},
{
path: 'tfa',
component: TfaComponent,
data: { title: '登录二次认证', titleI18n: 'app.login.login' }
},
{
path: 'register',
component: UserRegisterComponent,

View File

@ -17,16 +17,26 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared';
import { NzStepsModule } from 'ng-zorro-antd/steps';
import { CallbackComponent } from './callback.component';
import { SocialsProviderBindUserComponent } from './socials-provider-bind-user/socials-provider-bind-user.component';
import { ForgotComponent } from './forgot/forgot.component';
import { UserLockComponent } from './lock/lock.component';
import { UserLoginComponent } from './login/login.component';
import { PassportRoutingModule } from './passport-routing.module';
import { UserRegisterResultComponent } from './register-result/register-result.component';
import { UserRegisterComponent } from './register/register.component';
import { SocialsProviderBindUserComponent } from './socials-provider-bind-user/socials-provider-bind-user.component';
import { TfaComponent } from './tfa/tfa.component';
const COMPONENTS = [SocialsProviderBindUserComponent,UserLoginComponent, UserRegisterResultComponent, UserRegisterComponent, UserLockComponent, CallbackComponent];
const COMPONENTS = [
TfaComponent,
SocialsProviderBindUserComponent,
UserLoginComponent,
UserRegisterResultComponent,
UserRegisterComponent,
UserLockComponent,
CallbackComponent
];
@NgModule({
imports: [SharedModule, PassportRoutingModule, NzStepsModule],

View File

@ -0,0 +1,101 @@
<!--二次认证-->
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
<div nz-row>
<nz-alert *ngIf="error" style="width: 100%" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
<nz-radio-group nzSize="large" style="margin-bottom: 8px; width: 100%">
<label nz-radio-button nzValue="normal" style="width: 368px; text-align: center">
<i nz-icon nzType="safety" nzTheme="outline"></i>
{{ 'mxk.login.twoFactor' | i18n }}
</label>
</nz-radio-group>
<nz-form-item *ngIf="twoFactorType == '1'" style="width: 100%">
<nz-form-control [nzErrorTip]="'' | i18n">
<nz-input-group nzSize="large" nzPrefixIcon="lock" nzSearch>
<input nz-input formControlName="otpCaptcha" placeholder="{{ 'mxk.login.text.captcha' | i18n }}" />
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="twoFactorType == '2'" style="width: 100%">
<nz-form-control [nzErrorTip]="">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="twoFactorEmail" placeholder="{{ 'mxk.login.text.mobile' | i18n }}" />
</nz-input-group>
<ng-template #mobileErrorTip let-i>
<ng-container *ngIf="i.errors.required">
{{ 'validation.phone-number.required' | i18n }}
</ng-container>
<ng-container *ngIf="i.errors.pattern">
{{ 'validation.phone-number.wrong-format' | i18n }}
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="twoFactorType == '2'" style="width: 100%">
<nz-form-control [nzErrorTip]="'' | i18n">
<nz-input-group nzSize="large" nzPrefixIcon="mail" nzSearch [nzAddOnAfter]="suffixSendOtpCodeButton">
<input nz-input formControlName="otpCaptcha" placeholder="{{ 'mxk.login.text.captcha' | i18n }}" />
</nz-input-group>
<ng-template #suffixSendOtpCodeButton>
<button
type="button"
nz-button
nzSize="large"
(click)="sendTwoFactorOtpCode()"
[disabled]="count > 0"
nzBlock
[nzLoading]="loading"
>
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
</button>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="twoFactorType == '3'" style="width: 100%">
<nz-form-control [nzErrorTip]="">
<nz-input-group nzSize="large" nzPrefixIcon="user">
<input nz-input formControlName="twoFactorMobile" placeholder="{{ 'mxk.login.text.mobile' | i18n }}" />
</nz-input-group>
<ng-template #mobileErrorTip let-i>
<ng-container *ngIf="i.errors.required">
{{ 'validation.phone-number.required' | i18n }}
</ng-container>
<ng-container *ngIf="i.errors.pattern">
{{ 'validation.phone-number.wrong-format' | i18n }}
</ng-container>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="twoFactorType == '3'" style="width: 100%">
<nz-form-control [nzErrorTip]="'' | i18n">
<nz-input-group nzSize="large" nzPrefixIcon="mail" nzSearch [nzAddOnAfter]="suffixSendOtpCodeButton">
<input nz-input formControlName="otpCaptcha" placeholder="{{ 'mxk.login.text.captcha' | i18n }}" />
</nz-input-group>
<ng-template #suffixSendOtpCodeButton>
<button
type="button"
nz-button
nzSize="large"
(click)="sendTwoFactorOtpCode()"
[disabled]="count > 0"
nzBlock
[nzLoading]="loading"
>
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
</button>
</ng-template>
</nz-form-control>
</nz-form-item>
</div>
<nz-form-item>
<nz-col [nzSpan]="12"></nz-col>
<nz-col [nzSpan]="12" class="text-right">
<a class="forgot" routerLink="/passport/login">{{ 'mxk.forgot.login' | i18n }}</a>
</nz-col>
</nz-form-item>
<nz-form-item>
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
{{ 'app.login.login' | i18n }}
</button>
</nz-form-item>
</form>

View File

@ -0,0 +1,75 @@
@import '@delon/theme/index';
:host {
display: block;
width: 368px;
margin: 0 auto;
::ng-deep {
.ant-tabs .ant-tabs-bar {
margin-bottom: 24px;
text-align: center;
border-bottom: 0;
}
.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 4px;
}
.login-tab {
color: #000;
background: unset;
border-color: unset;
border-right-color: unset;
}
.icon {
margin-left: 16px;
color: rgb(0 0 0 / 20%);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
nz-tooltip {
vertical-align: middle;
}
.register {
float: right;
}
}
}
}
[data-theme='dark'] {
:host ::ng-deep {
.icon {
color: rgb(255 255 255 / 20%);
&:hover {
color: #fff;
}
}
}
}
input {
font-weight: bold;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TfaComponent } from './tfa.component';
describe('TfaComponent', () => {
let component: TfaComponent;
let fixture: ComponentFixture<TfaComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TfaComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TfaComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,212 @@
/*
* Copyright [2025] [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.
*/
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
inject,
OnInit,
OnDestroy,
AfterViewInit,
Optional
} from '@angular/core';
import { AbstractControl, ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
import { ReuseTabService } from '@delon/abc/reuse-tab';
import { SettingsService, _HttpClient, I18nPipe } from '@delon/theme';
import { environment } from '@env/environment';
import { NzAlertModule } from 'ng-zorro-antd/alert';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzTabChangeEvent, NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { finalize } from 'rxjs/operators';
import { AuthnService } from '../../../service/authn.service';
import { ImageCaptchaService } from '../../../service/image-captcha.service';
import { CONSTS } from '../../../shared/consts';
@Component({
selector: 'app-tfa',
templateUrl: './tfa.component.html',
styleUrls: ['./tfa.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TfaComponent implements OnInit {
form: FormGroup;
error = '';
secretKey = '';
secretPublicKey = '';
captchaType = '';
twoFactorType = '0';
twoFactorJwt = '';
isFirstPasswordModify = 'N';
state = '';
defualtRedirectUri = '';
count = 0;
interval$: any;
loading = false;
constructor(
fb: FormBuilder,
private router: Router,
private settingsService: SettingsService,
private authnService: AuthnService,
private imageCaptchaService: ImageCaptchaService,
private route: ActivatedRoute,
private msg: NzMessageService,
@Optional()
@Inject(ReuseTabService)
private reuseTabService: ReuseTabService,
private cdr: ChangeDetectorRef
) {
this.form = fb.group({
userName: [null, [Validators.required]],
password: [null, [Validators.required]],
captcha: [null, [Validators.required]],
mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
twoFactorMobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]],
twoFactorEmail: [null, [Validators.required, Validators.pattern(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/)]],
otpCaptcha: [null, [Validators.required]],
remember: [false]
});
}
ngOnInit(): void {
this.authnService
.get({ remember_me: localStorage.getItem(CONSTS.REMEMBER) })
.pipe(
finalize(() => {
this.loading = false;
this.cdr.detectChanges();
})
)
.subscribe(res => {
this.loading = true;
if (res.code !== 0) {
this.error = res.msg;
} else {
this.state = res.data.state;
this.defualtRedirectUri = res.data.redirectUri;
this.captchaType = res.data.captcha;
this.secretKey = res.data.secretKey;
this.secretPublicKey = res.data.secretPublicKey;
this.isFirstPasswordModify = res.data.isFirstPasswordModify;
}
});
let twoFactorData = JSON.parse(localStorage.getItem(CONSTS.TWO_FACTOR_DATA) || '');
this.twoFactorType = twoFactorData.twoFactor;
this.twoFactorJwt = twoFactorData.token;
this.twoFactorMobile.setValue(twoFactorData.mobile);
this.twoFactorEmail.setValue(twoFactorData.email);
}
get userName(): AbstractControl {
return this.form.get('userName')!;
}
get password(): AbstractControl {
return this.form.get('password')!;
}
get mobile(): AbstractControl {
return this.form.get('mobile')!;
}
get captcha(): AbstractControl {
return this.form.get('captcha')!;
}
get otpCaptcha(): AbstractControl {
return this.form.get('otpCaptcha')!;
}
get remember(): AbstractControl {
return this.form.get('remember')!;
}
get twoFactorMobile(): AbstractControl {
return this.form.get('twoFactorMobile')!;
}
get twoFactorEmail(): AbstractControl {
return this.form.get('twoFactorEmail')!;
}
sendTwoFactorOtpCode(): void {
this.authnService.sendTwoFactorCode({ jwtToken: this.twoFactorJwt }).subscribe(res => {
if (res.code !== 0) {
this.msg.success(`发送失败`);
}
});
this.count = 59;
this.interval$ = setInterval(() => {
this.count -= 1;
if (this.count <= 0) {
clearInterval(this.interval$);
}
this.cdr.detectChanges();
}, 1000);
}
submit(): void {
this.error = '';
this.otpCaptcha.markAsDirty();
this.otpCaptcha.updateValueAndValidity();
localStorage.setItem(CONSTS.REMEMBER, this.form.get(CONSTS.REMEMBER)?.value);
this.loading = true;
this.cdr.detectChanges();
this.authnService
.login({
authType: 'twoFactor',
state: this.state,
jwtToken: this.twoFactorJwt,
otpCaptcha: this.otpCaptcha.value,
remeberMe: this.remember.value
})
.pipe(
finalize(() => {
this.loading = false;
this.cdr.detectChanges();
})
)
.subscribe(res => {
this.loading = true;
if (res.code !== 0) {
this.error = res.msg;
} else {
localStorage.removeItem(CONSTS.TWO_FACTOR_DATA);
// 清空路由复用信息
this.reuseTabService?.clear();
// 设置用户Token信息
this.authnService.auth(res.data);
this.authnService.navigate({
defualtRedirectUri: this.defualtRedirectUri,
isFirstPasswordModify: this.isFirstPasswordModify,
passwordSetType: res.data.passwordSetType
});
}
this.cdr.detectChanges();
});
}
}

View File

@ -61,6 +61,10 @@ export class AuthnService {
return this.http.post('/login/signin?_allow_anonymous=true', authParam);
}
sendTwoFactorCode(authParam: any) {
return this.http.post(`/login/sendTwoFactorCode?_allow_anonymous=true`, authParam);
}
bindSocialsUser(authParam: any) {
return this.http.post('/login/signin/bindusersocials?_allow_anonymous=true', authParam);
}

View File

@ -22,6 +22,7 @@ export const CONSTS = {
/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/gi,
INST: 'inst',
CONGRESS: 'congress',
TWO_FACTOR_DATA: 'two_factor_data',
ONLINE_TICKET: 'online_ticket',
REDIRECT_URI: 'redirect_uri',
REMEMBER: 'remember_me',

View File

@ -15,6 +15,7 @@
"sign-in-with": "Sign in with",
"signup": "Sign up",
"login": "Login",
"twoFactor": "2-Factor Authentication",
"text.username": "Username",
"text.mobile": "Mobile Number",
"text.password": "Password",

View File

@ -15,6 +15,7 @@
"sign-in-with": "其他登录方式",
"signup": "用户注册",
"login": "登录",
"twoFactor": "二次身份认证",
"text.username": "用户名",
"text.mobile": "手机号码",
"text.password": "密码",

View File

@ -15,6 +15,7 @@
"sign-in-with": "其他登錄方式",
"signup": "用戶註冊",
"login": "登錄",
"twoFactor": "二次身份认证",
"text.username": "用戶名",
"text.mobile": "手機號碼",
"text.password": "密碼",