# 登录认证扩展
系统默认给我们提供了用户名密码登录、短信验证码、扫码登陆、CA登录等逻辑实现, 在很多时候根本不满足我们实际开发的要求,所以我们经常需要对认证模块进行扩展, 要自定义认证模块,你可以按照以下步骤进行:
# 第一步,定义Param信息类
创建一个自定义的认证参数信息类,该类继承AbstractHosLoginParam类。在该类中,根据认证的需要定义属性,将前端登录传的参数实体化,下面给的例子中的AbstractHosCaptchaLoginParam类继承了AbstractHosLoginParam类。
public abstract class AbstractHosCaptchaLoginParam extends AbstractHosLoginParam implements CaptchaCodeParams {
private String captchaCode;
private String captchaUUID;
public String getCaptchaCode() {
return captchaCode;
}
public void setCaptchaCode(String captchaCode) {
this.captchaCode = captchaCode;
}
public String getCaptchaUUID() {
return captchaUUID;
}
public void setCaptchaUUID(String captchaUUID) {
this.captchaUUID = captchaUUID;
}
}
public class SmsLoginParam extends AbstractHosCaptchaLoginParam implements SmsCodeParams {
private String smsCode;
private String smsId;
private String loginName;
public void setSmsCode(String smsCode) {
this.smsCode = smsCode;
}
public void setSmsId(String smsId) {
this.smsId = smsId;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public String getSmsCode() {
return this.smsCode;
}
@Override
public String getSmsId() {
return this.smsId;
}
@Override
public String getPhoneNumber() {
return this.loginName;
}
}
# 第二步,定义token信息类
创建一个自定义的token类,该类继承AbstractHosAuthenticationToken<自定义Param类>类。在该类中,定义构造函数并给hosGrantType赋值。 hosGrantType值和调用登录接口(/api/security/token?grantType=password)传的grantType值保持一致。
public class SmsAuthenticationToken extends AbstractHosAuthenticationToken<SmsLoginParam> {
private SmsAuthenticationToken() {
super();
}
public SmsAuthenticationToken(SmsLoginParam loginParam) {
super(loginParam, HosGrantType.SMS);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return null;
}
}
# 第三步,定义Converter类
创建一个自定义的Converter类,该类继承AbstractHosAuthenticationConverter类。在该类中重写convert方法,将请求中的参数进行该认证方式需要的参数类转换。
public class HosSmsAuthenticationConverter extends AbstractHosAuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
HosGrantType hosGrantType = getHosGrantType(request);
if (!HosGrantType.SMS.equals(hosGrantType)) {
return null;
}
try {
SmsLoginParam smsLoginParam = JsonUtils.getInstance().readValue(request.getInputStream(), SmsLoginParam.class);
smsLoginParam.setHosGrantType(HosGrantType.SMS.getValue());
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(smsLoginParam);
return smsAuthenticationToken;
} catch (IOException e) {
logger.error("短信登录参数解析错误", e);
SecurityUtil.throwError("101-002-004-012", "短信登录参数解析错误");
}
return null;
}
}
# 第四步,定义Provider类
方案一: 创建一个自定义的Provider类,该类继承 AbstractHosAuthenticationProvider<自定义Token类>类。重写以下四个方法:
1、loadHosUserDetails(自定义token类 authentication):该方法用于根据认证传参获取用户信息。输入参数authentication是一个封装了
用户认证信息的Authentication对象,包括用户名、密码等信息。该方法返回一个查询成功的HosUser对象,或者抛出SecurityAuthenticationException
异常表示获取用户失败。
2、supports(Class<?> authentication):该方法用于判断是否支持给定类型的认证请求。输入参数authentication是要被验证的对象类型,
通常是UsernamePasswordAuthenticationToken或JwtAuthenticationToken等。该方法返回一个boolean值,表示是否支持该类型的认证请求。
3、verifyParam(自定义token类 authentication, HosUser hosUser):该方法用于执行身份验证过程,可以不重写。
输入参数authentication是一个封装了用户认证信息的Authentication对象,包括用户名、密码等信息,hosUser是执行loadHosUserDetails返回的用户信息。
该方法不做任何返回,或者抛出AuthenticationException异常表示认证失败。
4、canMultiGrant():该方法用于设置是否需要多因素认证。父类默认设置的false,代表不需要多因素认证,如果该自定义登录需要进行对因素认证,则返回true。
```java
public class HosSmsAuthenticationProvider extends AbstractHosAuthenticationProvider<SmsAuthenticationToken> {
public final PersonUuidStore personUuidStore;
private final SmsCodeVerify smsCodeVerify;
private final PasswordPolicyManager passwordPolicyManager;
private final LoginFailCountStore loginFailCountStore;
private final HosUserDetailsService hosUserDetailsService;
private final CaptchaCodeVerify captchaCodeVerify;
public HosSmsAuthenticationProvider(CaptchaCodeVerify captchaCodeVerify,
HosUserDetailsService hosUserDetailsService, PasswordPolicyManager passwordPolicyManager,
SmsCodeVerify smsCodeVerify,
PersonUuidStore personUuidStore,
LoginFailCountStore loginFailCountStore, MultiFactorStore multiFactorStore,
MultiGrantProcess multiGrantProcess, HosTokenAuthenticationTokenGenerator hosTokenAuthenticationTokenGenerator) {
super(multiFactorStore, multiGrantProcess, hosTokenAuthenticationTokenGenerator);
this.smsCodeVerify = smsCodeVerify;
this.personUuidStore = personUuidStore;
this.passwordPolicyManager = passwordPolicyManager;
this.loginFailCountStore = loginFailCountStore;
this.hosUserDetailsService = hosUserDetailsService;
this.captchaCodeVerify = captchaCodeVerify;
}
protected void verifyParam(SmsAuthenticationToken authentication, HosUser hosUser) {
SmsLoginParam loginParam = authentication.getLoginParam();
String loginName = loginParam.getLoginName();
int count = loginFailCountStore.getCountByLoginName(loginName);
PasswordPolicy passwordPolicy = passwordPolicyManager.loadPasswordPolicy();
boolean enableCaptchaCode = passwordPolicy.enableCaptchaCode();
if (enableCaptchaCode) {
int captchaCodeCondition = passwordPolicy.getCaptchaCodeCondition();
if (captchaCodeCondition > -1 &&
count >= captchaCodeCondition) {
if (!StringUtils.hasText(loginParam.getCaptchaUUID()) || !StringUtils.hasText(loginParam.getCaptchaCode())) {
SecurityUtil.throwError("101-002-004-020", "请输入图形验证码");
}
captchaCodeVerify.verify(loginParam);
}
}
if (!smsCodeVerify.verify(loginParam)) {
loginFailCountStore.addCountByLoginName(loginName);
boolean enableLockUser = passwordPolicy.enableLockUser();
if (enableLockUser) {
int lockUserCondition = passwordPolicy.getLockUserCondition();
long lockUserDuration = passwordPolicy.getLockUserDuration();
if (count >= lockUserCondition) {
//锁定用户 时长为lockUserDuration
hosUserDetailsService.lockUser(loginName, lockUserDuration);
}
}
if (enableCaptchaCode) {
int captchaCodeCondition = passwordPolicy.getCaptchaCodeCondition();
//开启验证码,配置了错误次数,并且用户登录次数大于等于配置次数
if (captchaCodeCondition > -1 &&
count >= captchaCodeCondition) {
SecurityUtil.throwError("101-002-004-021",
"短信验证码失效或者错误");
}
}
SecurityUtil.throwError("101-002-004-021",
"短信验证码失效或者错误");
loginFailCountStore.deleteByLoginName(loginName);
}
}
/**
* 根据登录参数获取用户信息
*
* @param authentication
* @return
*/
@Override
protected HosUser loadHosUserDetails(SmsAuthenticationToken authentication) {
HosUserDetails hosUserDetails = hosUserDetailsService.loadUserByPhoneNumber(authentication.getLoginParam().getPhoneNumber());
return hosUserDetails.getHosUser();
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(SmsAuthenticationToken.class);
}
public boolean canMultiGrant() {
return true;
}
}
# 第五步,配置类增加扩展登录
找到SecurityLoginConfig配置类的securityFilterChain()方法,修改HosLoginConfigurer配置。
@Bean
@Order(value = Ordered.HIGHEST_PRECEDENCE + 100)
SecurityFilterChain securityFilterChain(HttpSecurity http, HosSecurityProperties hosSecurityProperties, HosPostInfoService hosPostInfoService) throws Exception {
...
// 加载登录配置
HosLoginConfigurer hosLoginConfigurer = new HosLoginConfigurer();
// 加载统一认证配置
if (hosSecurityProperties.getOauth2().isEnable()) {
hosLoginConfigurer.hosOAuth2Login();
}
hosLoginConfigurer.loginTokenEndpoint(loginTokenEndpoint -> {
//注入自定义Converter
loginTokenEndpoint.authenticationConverter(new HosSmsAuthenticationConverter());
//注入自定义Provider
loginTokenEndpoint.authenticationProvider(new HosSmsAuthenticationProvider());
});
http.apply(hosLoginConfigurer);
...
return http.build();
}
# 第六步,测试
启用后端服务器,调用登录接口/api/security/token
。
参数说明:query中grantType等于第二步自定义token类中HosGrantType的值,body数据为第一步自定义参数的值。