mirror of
https://gitee.com/dromara/MaxKey.git
synced 2025-12-07 01:18:27 +08:00
二次认证优化
This commit is contained in:
parent
f32bcff8ae
commit
95a7c0daa1
@ -37,6 +37,8 @@ public class SignPrincipal implements UserDetails {
|
|||||||
|
|
||||||
String sessionId;
|
String sessionId;
|
||||||
|
|
||||||
|
int twoFactor;
|
||||||
|
|
||||||
List<GrantedAuthority> grantedAuthority;
|
List<GrantedAuthority> grantedAuthority;
|
||||||
|
|
||||||
List<GrantedAuthority> grantedAuthorityApps;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|||||||
@ -104,7 +104,7 @@ public class AuthJwt implements Serializable {
|
|||||||
this.email = principal.getUserInfo().getEmail();
|
this.email = principal.getUserInfo().getEmail();
|
||||||
this.instId = principal.getUserInfo().getInstId();
|
this.instId = principal.getUserInfo().getInstId();
|
||||||
this.instName = principal.getUserInfo().getInstName();
|
this.instName = principal.getUserInfo().getInstName();
|
||||||
|
this.twoFactor =principal.getTwoFactor();
|
||||||
this.authorities = new ArrayList<>();
|
this.authorities = new ArrayList<>();
|
||||||
for(GrantedAuthority grantedAuthority :authentication.getAuthorities()) {
|
for(GrantedAuthority grantedAuthority :authentication.getAuthorities()) {
|
||||||
this.authorities.add(grantedAuthority.getAuthority());
|
this.authorities.add(grantedAuthority.getAuthority());
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import java.util.Date;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.dromara.maxkey.authn.SignPrincipal;
|
import org.dromara.maxkey.authn.SignPrincipal;
|
||||||
|
import org.dromara.maxkey.constants.ConstsJwt;
|
||||||
import org.dromara.maxkey.crypto.jwt.Hmac512Service;
|
import org.dromara.maxkey.crypto.jwt.Hmac512Service;
|
||||||
import org.dromara.maxkey.entity.idm.UserInfo;
|
import org.dromara.maxkey.entity.idm.UserInfo;
|
||||||
import org.dromara.maxkey.web.WebContext;
|
import org.dromara.maxkey.web.WebContext;
|
||||||
@ -61,7 +62,8 @@ public class AuthJwtService {
|
|||||||
.expirationTime(expirationTime)
|
.expirationTime(expirationTime)
|
||||||
.claim("locale", userInfo.getLocale())
|
.claim("locale", userInfo.getLocale())
|
||||||
.claim("kid", Hmac512Service.MXK_AUTH_JWK)
|
.claim("kid", Hmac512Service.MXK_AUTH_JWK)
|
||||||
.claim("institution", userInfo.getInstId())
|
.claim(ConstsJwt.USER_ID, userInfo.getId())
|
||||||
|
.claim(ConstsJwt.INST_ID, userInfo.getInstId())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return signedJWT(jwtClaims);
|
return signedJWT(jwtClaims);
|
||||||
|
|||||||
@ -41,4 +41,10 @@ public interface SessionManager {
|
|||||||
public void terminate(String sessionId,String userId,String username);
|
public void terminate(String sessionId,String userId,String username);
|
||||||
|
|
||||||
public void visited(String sessionId , VisitedDto visited);
|
public void visited(String sessionId , VisitedDto visited);
|
||||||
|
|
||||||
|
public void createTwoFactor(String sessionId, Session session);
|
||||||
|
|
||||||
|
public Session removeTwoFactor(String sessionId);
|
||||||
|
|
||||||
|
public Session getTwoFactor(String sessionId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,21 +38,30 @@ public class InMemorySessionManager implements SessionManager{
|
|||||||
static final long CACHE_MAXIMUM_SIZE = 2000000;
|
static final long CACHE_MAXIMUM_SIZE = 2000000;
|
||||||
protected int validitySeconds = 60 * 30; //default 30 minutes.
|
protected int validitySeconds = 60 * 30; //default 30 minutes.
|
||||||
|
|
||||||
protected static Cache<String, Session> sessionStore =
|
Cache<String, Session> sessionStore;
|
||||||
Caffeine.newBuilder()
|
|
||||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
Cache<String, Session> sessionTwoFactorStore;
|
||||||
.maximumSize(CACHE_MAXIMUM_SIZE)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public InMemorySessionManager(int validitySeconds) {
|
public InMemorySessionManager(int validitySeconds) {
|
||||||
super();
|
super();
|
||||||
this.validitySeconds = validitySeconds;
|
this.validitySeconds = validitySeconds;
|
||||||
|
if(validitySeconds > 0) {
|
||||||
sessionStore =
|
sessionStore =
|
||||||
Caffeine.newBuilder()
|
Caffeine.newBuilder()
|
||||||
.expireAfterWrite(validitySeconds, TimeUnit.SECONDS)
|
.expireAfterWrite(validitySeconds, TimeUnit.SECONDS)
|
||||||
.maximumSize(CACHE_MAXIMUM_SIZE)
|
.maximumSize(CACHE_MAXIMUM_SIZE)
|
||||||
.build();
|
.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
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,9 +35,14 @@ public class RedisSessionManager implements SessionManager {
|
|||||||
|
|
||||||
protected int validitySeconds = 60 * 30; //default 30 minutes.
|
protected int validitySeconds = 60 * 30; //default 30 minutes.
|
||||||
|
|
||||||
|
int twoFactorValidity = 10 * 60; //default 10 minutes.
|
||||||
|
|
||||||
RedisConnectionFactory connectionFactory;
|
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) {
|
public String getKey(String sessionId) {
|
||||||
return PREFIX + 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -207,4 +207,35 @@ public class SessionManagerImpl implements SessionManager{
|
|||||||
redisSessionManager.visited(sessionId,visited);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ package org.dromara.maxkey.authn.provider;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.dromara.maxkey.authn.LoginCredential;
|
import org.dromara.maxkey.authn.LoginCredential;
|
||||||
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;
|
||||||
@ -30,6 +31,7 @@ import org.dromara.maxkey.authn.web.AuthorizationUtils;
|
|||||||
import org.dromara.maxkey.configuration.ApplicationConfig;
|
import org.dromara.maxkey.configuration.ApplicationConfig;
|
||||||
import org.dromara.maxkey.constants.ConstsLoginType;
|
import org.dromara.maxkey.constants.ConstsLoginType;
|
||||||
import org.dromara.maxkey.constants.ConstsStatus;
|
import org.dromara.maxkey.constants.ConstsStatus;
|
||||||
|
import org.dromara.maxkey.constants.ConstsTwoFactor;
|
||||||
import org.dromara.maxkey.entity.idm.UserInfo;
|
import org.dromara.maxkey.entity.idm.UserInfo;
|
||||||
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
|
import org.dromara.maxkey.password.onetimepwd.AbstractOtpAuthn;
|
||||||
import org.dromara.maxkey.password.onetimepwd.MailOtpAuthnService;
|
import org.dromara.maxkey.password.onetimepwd.MailOtpAuthnService;
|
||||||
@ -93,6 +95,10 @@ public abstract class AbstractAuthenticationProvider {
|
|||||||
|
|
||||||
public abstract Authentication doAuthenticate(LoginCredential authentication);
|
public abstract Authentication doAuthenticate(LoginCredential authentication);
|
||||||
|
|
||||||
|
public Authentication doTwoFactorAuthenticate(LoginCredential credential , UserInfo user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public boolean supports(Class authentication) {
|
public boolean supports(Class authentication) {
|
||||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
|
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
|
||||||
@ -147,6 +153,13 @@ public abstract class AbstractAuthenticationProvider {
|
|||||||
*/
|
*/
|
||||||
session.setAuthentication(authenticationToken);
|
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
|
//create session
|
||||||
this.sessionManager.create(session.getId(), session);
|
this.sessionManager.create(session.getId(), session);
|
||||||
|
|
||||||
@ -259,4 +272,17 @@ public abstract class AbstractAuthenticationProvider {
|
|||||||
return true;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -86,6 +86,10 @@ public abstract class AbstractAuthenticationRealm {
|
|||||||
return loginService.find(username, password);
|
return loginService.find(username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserInfo loadUserInfoById(String userId) {
|
||||||
|
return loginService.findById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
public abstract boolean passwordMatches(UserInfo userInfo, String password);
|
public abstract boolean passwordMatches(UserInfo userInfo, String password);
|
||||||
|
|
||||||
public List<Groups> queryGroups(UserInfo userInfo) {
|
public List<Groups> queryGroups(UserInfo userInfo) {
|
||||||
|
|||||||
@ -21,17 +21,27 @@ import org.dromara.maxkey.authn.jwt.AuthTokenService;
|
|||||||
import org.dromara.maxkey.authn.provider.AbstractAuthenticationProvider;
|
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.*;
|
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.realm.AbstractAuthenticationRealm;
|
||||||
import org.dromara.maxkey.authn.session.SessionManager;
|
import org.dromara.maxkey.authn.session.SessionManager;
|
||||||
import org.dromara.maxkey.authn.support.rememberme.AbstractRemeberMeManager;
|
import org.dromara.maxkey.authn.support.rememberme.AbstractRemeberMeManager;
|
||||||
import org.dromara.maxkey.authn.support.rememberme.JdbcRemeberMeManager;
|
import org.dromara.maxkey.authn.support.rememberme.JdbcRemeberMeManager;
|
||||||
import org.dromara.maxkey.configuration.ApplicationConfig;
|
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.password.sms.SmsOtpAuthnService;
|
||||||
import org.dromara.maxkey.persistence.service.CnfPasswordPolicyService;
|
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.PasswordPolicyValidatorService;
|
||||||
import org.dromara.maxkey.persistence.service.impl.PasswordPolicyValidatorServiceImpl;
|
import org.dromara.maxkey.persistence.service.impl.PasswordPolicyValidatorServiceImpl;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
@ -49,7 +59,8 @@ public class AuthnProviderAutoConfiguration {
|
|||||||
MobileAuthenticationProvider mobileAuthenticationProvider,
|
MobileAuthenticationProvider mobileAuthenticationProvider,
|
||||||
TrustedAuthenticationProvider trustedAuthenticationProvider,
|
TrustedAuthenticationProvider trustedAuthenticationProvider,
|
||||||
ScanCodeAuthenticationProvider scanCodeAuthenticationProvider,
|
ScanCodeAuthenticationProvider scanCodeAuthenticationProvider,
|
||||||
AppAuthenticationProvider appAuthenticationProvider
|
AppAuthenticationProvider appAuthenticationProvider,
|
||||||
|
TwoFactorAuthenticationProvider twoFactorAuthenticationProvider
|
||||||
) {
|
) {
|
||||||
AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory();
|
AuthenticationProviderFactory authenticationProvider = new AuthenticationProviderFactory();
|
||||||
authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider);
|
authenticationProvider.addAuthenticationProvider(normalAuthenticationProvider);
|
||||||
@ -58,6 +69,9 @@ public class AuthnProviderAutoConfiguration {
|
|||||||
authenticationProvider.addAuthenticationProvider(scanCodeAuthenticationProvider);
|
authenticationProvider.addAuthenticationProvider(scanCodeAuthenticationProvider);
|
||||||
authenticationProvider.addAuthenticationProvider(appAuthenticationProvider);
|
authenticationProvider.addAuthenticationProvider(appAuthenticationProvider);
|
||||||
|
|
||||||
|
//二次认证
|
||||||
|
authenticationProvider.addAuthenticationProvider(twoFactorAuthenticationProvider);
|
||||||
|
|
||||||
return authenticationProvider;
|
return authenticationProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,4 +170,47 @@ public class AuthnProviderAutoConfiguration {
|
|||||||
jdbcTemplate,applicationConfig,authTokenService,validity);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
}
|
||||||
@ -43,4 +43,19 @@ public class ConstsLoginType {
|
|||||||
|
|
||||||
public static final String HTTPHEADER = "HttpHeader";
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -27,6 +27,8 @@ public interface LoginService {
|
|||||||
|
|
||||||
public UserInfo find(String username, String password);
|
public UserInfo find(String username, String password);
|
||||||
|
|
||||||
|
public UserInfo findById(String userId);
|
||||||
|
|
||||||
public List<UserInfo> findByUsername(String username, String password);
|
public List<UserInfo> findByUsername(String username, String password);
|
||||||
|
|
||||||
public List<UserInfo> findByUsernameOrMobile(String username, String password);
|
public List<UserInfo> findByUsernameOrMobile(String username, String password);
|
||||||
|
|||||||
@ -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 = "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 = "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 = ?) ";
|
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;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,8 @@ public abstract class AbstractOtpAuthn {
|
|||||||
|
|
||||||
public abstract boolean validate(UserInfo userInfo, String token);
|
public abstract boolean validate(UserInfo userInfo, String token);
|
||||||
|
|
||||||
|
public abstract boolean validate(String sharedSecret, String token);
|
||||||
|
|
||||||
protected String defaultProduce(UserInfo userInfo) {
|
protected String defaultProduce(UserInfo userInfo) {
|
||||||
return genToken(userInfo);
|
return genToken(userInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,4 +48,10 @@ public class CapOtpAuthn extends AbstractOtpAuthn {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,4 +72,10 @@ public class CounterBasedOtpAuthn extends AbstractOtpAuthn {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,4 +95,10 @@ public class HotpOtpAuthn extends AbstractOtpAuthn {
|
|||||||
this.truncation = truncation;
|
this.truncation = truncation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,6 +122,12 @@ public class MailOtpAuthn extends AbstractOtpAuthn {
|
|||||||
this.messageTemplate = messageTemplate;
|
this.messageTemplate = messageTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,4 +38,10 @@ public class MobileOtpAuthn extends AbstractOtpAuthn {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,4 +48,10 @@ public class RsaOtpAuthn extends AbstractOtpAuthn {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,9 +51,14 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean validate(UserInfo userInfo, String token) {
|
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()));
|
_logger.debug("utcTime : {}" , dateFormat.format(new Date()));
|
||||||
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
long currentTimeSeconds = System.currentTimeMillis() / 1000;
|
||||||
String sharedSecret = PasswordReciprocal.getInstance().decoder(userInfo.getSharedSecret());
|
String sharedSecret = PasswordReciprocal.getInstance().decoder(secret);
|
||||||
byte[] byteSharedSecret = Base32Utils.decode(sharedSecret);
|
byte[] byteSharedSecret = Base32Utils.decode(sharedSecret);
|
||||||
String hexSharedSecret = Hex.encodeHexString(byteSharedSecret);
|
String hexSharedSecret = Hex.encodeHexString(byteSharedSecret);
|
||||||
String timeBasedToken = "";
|
String timeBasedToken = "";
|
||||||
@ -79,7 +84,6 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,4 +56,10 @@ public class SmsOtpAuthn extends AbstractOtpAuthn {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(String sharedSecret, String token) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
464
maxkey-web-frontend/maxkey-web-app/package-lock.json
generated
464
maxkey-web-frontend/maxkey-web-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -291,9 +291,14 @@ export class UserLoginComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
// 清空路由复用信息
|
// 清空路由复用信息
|
||||||
this.reuseTabService.clear();
|
this.reuseTabService.clear();
|
||||||
|
if (res.data.twoFactor === '0') {
|
||||||
// 设置用户Token信息
|
// 设置用户Token信息
|
||||||
this.authnService.auth(res.data);
|
this.authnService.auth(res.data);
|
||||||
this.authnService.navigate({});
|
this.authnService.navigate({});
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(CONSTS.TWO_FACTOR_DATA, JSON.stringify(res.data));
|
||||||
|
this.router.navigateByUrl('/passport/tfa');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { UserLoginComponent } from './login/login.component';
|
|||||||
import { LogoutComponent } from './logout.component';
|
import { LogoutComponent } from './logout.component';
|
||||||
import { UserRegisterResultComponent } from './register-result/register-result.component';
|
import { UserRegisterResultComponent } from './register-result/register-result.component';
|
||||||
import { UserRegisterComponent } from './register/register.component';
|
import { UserRegisterComponent } from './register/register.component';
|
||||||
|
import { TfaComponent } from './tfa/tfa.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
// passport
|
// passport
|
||||||
@ -38,6 +39,11 @@ const routes: Routes = [
|
|||||||
component: UserLoginComponent,
|
component: UserLoginComponent,
|
||||||
data: { title: '登录', titleI18n: 'app.login.login' }
|
data: { title: '登录', titleI18n: 'app.login.login' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tfa',
|
||||||
|
component: TfaComponent,
|
||||||
|
data: { title: '登录二次认证', titleI18n: 'app.login.login' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'register',
|
path: 'register',
|
||||||
component: UserRegisterComponent,
|
component: UserRegisterComponent,
|
||||||
|
|||||||
@ -17,16 +17,26 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { SharedModule } from '@shared';
|
import { SharedModule } from '@shared';
|
||||||
import { NzStepsModule } from 'ng-zorro-antd/steps';
|
import { NzStepsModule } from 'ng-zorro-antd/steps';
|
||||||
|
|
||||||
import { CallbackComponent } from './callback.component';
|
import { CallbackComponent } from './callback.component';
|
||||||
import { SocialsProviderBindUserComponent } from './socials-provider-bind-user/socials-provider-bind-user.component';
|
|
||||||
import { ForgotComponent } from './forgot/forgot.component';
|
import { ForgotComponent } from './forgot/forgot.component';
|
||||||
import { UserLockComponent } from './lock/lock.component';
|
import { UserLockComponent } from './lock/lock.component';
|
||||||
import { UserLoginComponent } from './login/login.component';
|
import { UserLoginComponent } from './login/login.component';
|
||||||
import { PassportRoutingModule } from './passport-routing.module';
|
import { PassportRoutingModule } from './passport-routing.module';
|
||||||
import { UserRegisterResultComponent } from './register-result/register-result.component';
|
import { UserRegisterResultComponent } from './register-result/register-result.component';
|
||||||
import { UserRegisterComponent } from './register/register.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({
|
@NgModule({
|
||||||
imports: [SharedModule, PassportRoutingModule, NzStepsModule],
|
imports: [SharedModule, PassportRoutingModule, NzStepsModule],
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,6 +61,10 @@ export class AuthnService {
|
|||||||
return this.http.post('/login/signin?_allow_anonymous=true', authParam);
|
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) {
|
bindSocialsUser(authParam: any) {
|
||||||
return this.http.post('/login/signin/bindusersocials?_allow_anonymous=true', authParam);
|
return this.http.post('/login/signin/bindusersocials?_allow_anonymous=true', authParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
/(([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',
|
INST: 'inst',
|
||||||
CONGRESS: 'congress',
|
CONGRESS: 'congress',
|
||||||
|
TWO_FACTOR_DATA: 'two_factor_data',
|
||||||
ONLINE_TICKET: 'online_ticket',
|
ONLINE_TICKET: 'online_ticket',
|
||||||
REDIRECT_URI: 'redirect_uri',
|
REDIRECT_URI: 'redirect_uri',
|
||||||
REMEMBER: 'remember_me',
|
REMEMBER: 'remember_me',
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"sign-in-with": "Sign in with",
|
"sign-in-with": "Sign in with",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
"twoFactor": "2-Factor Authentication",
|
||||||
"text.username": "Username",
|
"text.username": "Username",
|
||||||
"text.mobile": "Mobile Number",
|
"text.mobile": "Mobile Number",
|
||||||
"text.password": "Password",
|
"text.password": "Password",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"sign-in-with": "其他登录方式",
|
"sign-in-with": "其他登录方式",
|
||||||
"signup": "用户注册",
|
"signup": "用户注册",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
|
"twoFactor": "二次身份认证",
|
||||||
"text.username": "用户名",
|
"text.username": "用户名",
|
||||||
"text.mobile": "手机号码",
|
"text.mobile": "手机号码",
|
||||||
"text.password": "密码",
|
"text.password": "密码",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"sign-in-with": "其他登錄方式",
|
"sign-in-with": "其他登錄方式",
|
||||||
"signup": "用戶註冊",
|
"signup": "用戶註冊",
|
||||||
"login": "登錄",
|
"login": "登錄",
|
||||||
|
"twoFactor": "二次身份认证",
|
||||||
"text.username": "用戶名",
|
"text.username": "用戶名",
|
||||||
"text.mobile": "手機號碼",
|
"text.mobile": "手機號碼",
|
||||||
"text.password": "密碼",
|
"text.password": "密碼",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user