# 登录认证扩展

系统默认给我们提供了用户名密码登录、短信验证码、扫码登陆、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&lt;?> 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&lt;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 &amp;&amp;
                    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 &amp;&amp;
                        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&lt;?> 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数据为第一步自定义参数的值。

登录扩展测试