# 登录认证扩展

框架提供了用户名密码、短信验证码、CA登录方式, 如果不满足项目实际要求,我们可以按照以下流程对认证模块进行扩展:

# 第一步,在pom文件引入hos-security-login

        <dependency>
            <groupId>com.mediway.hos</groupId>
            <artifactId>hos-security-login</artifactId>
        </dependency>

# 第二步,定义Param信息类

创建一个类用来将前端登录传的参数进行实体化,继承AbstractHosLoginParam类。

import com.mediway.hos.security.core.authentication.param.AbstractHosLoginParam;
import lombok.Data;

@Data
public class HosFSLoginParam extends AbstractHosLoginParam {

 private String token;

}

# 第三步,定义token信息类

创建一个自定义的token类,继承AbstractHosAuthenticationToken<自定义Param类>。在该类中,定义构造函数并定义一个HosGrantType对象。 hosGrantType值和调用登录接口(/api/security/token?grantType=password)传的grantType值保持一致。

import com.mediway.hos.security.core.HosGrantType;
import com.mediway.hos.security.core.authentication.AbstractHosAuthenticationToken;

public class HosFSAuthenticationToken extends AbstractHosAuthenticationToken<HosFSLoginParam> {
 @Override
 public Object getCredentials() {
  return null;
 }

 @Override
 public Object getPrincipal() {
  return null;
 }

 private HosFSAuthenticationToken() {
  super();
 }

 public HosFSAuthenticationToken(HosFSLoginParam loginParam) {
  super(loginParam, new HosGrantType("FSToken"));
 }


}

# 第四步,定义Converter类

创建一个自定义的Converter类,该类继承AbstractHosAuthenticationConverter类。在该类中重写convert方法,将请求中的参数进行该认证方式需要的参数类转换。

import com.mediway.hos.security.core.HosLoginParameterNames;
import com.mediway.hos.security.core.util.JsonUtils;
import com.mediway.hos.security.core.util.SecurityUtil;
import com.mediway.hos.security.login.authorization.convert.AbstractHosAuthenticationConverter;
import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class HosFSAuthenticationConverter extends AbstractHosAuthenticationConverter {
 @Override
 public Authentication convert(HttpServletRequest request) {
  String grantType = request.getParameter(HosLoginParameterNames.GRANT_TYPE);
  if (!"FSToken".equals(grantType)) {
   return null;
  }
  try {
   HosFSLoginParam smsLoginParam = JsonUtils.getInstance().readValue(request.getInputStream(), HosFSLoginParam.class);
   smsLoginParam.setHosGrantType(grantType);
   HosFSAuthenticationToken fsAuthenticationToken = new HosFSAuthenticationToken(smsLoginParam);
   return fsAuthenticationToken;
  } 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
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.mediway.hos.security.core.TokenUniqueKey;
import com.mediway.hos.security.core.authentication.AbstractHosAuthenticationToken;
import com.mediway.hos.security.core.userdetails.HosUser;
import com.mediway.hos.security.core.userdetails.HosUserDetails;
import com.mediway.hos.security.core.util.CommonUtils;
import com.mediway.hos.security.core.util.HttpUtils;
import com.mediway.hos.security.core.util.SecurityUtil;
import com.mediway.hos.security.core.util.StringUtil;
import com.mediway.hos.security.enums.BaseBusinessExceptionEnum;
import com.mediway.hos.security.enums.VersionEnum;
import com.mediway.hos.security.login.HosUserDetailsService;
import com.mediway.hos.security.login.authorization.provider.AbstractHosAuthenticationProvider;
import com.mediway.hos.security.login.multiFactor.store.PostStore;
import com.mediway.hos.security.login.service.ConvertPostService;
import com.mediway.hos.security.login.service.SelectUserInfoService;
import com.mediway.hos.security.login.token.HosTokenAuthenticationTokenGenerator;
import com.mediway.hos.security.properties.HosSecurityProperties;
import com.mediway.hos.security.properties.HosVersionProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import java.util.*;

@Slf4j
public class HosFSAuthenticationProvider extends AbstractHosAuthenticationProvider&lt;HosFSAuthenticationToken> {

    private final HosUserDetailsService hosUserDetailsService;
    private final HosFSProperties hosFSProperties;
    private final HosVersionProperties hosVersionProperties;
    private final ConvertPostService convertPostService;

    private final HosTokenAuthenticationTokenGenerator hosTokenAuthenticationTokenGenerator;

    private final PostStore postStore;
    public HosFSAuthenticationProvider(HosTokenAuthenticationTokenGenerator hosTokenAuthenticationTokenGenerator,
                                          HosSecurityProperties hosSecurityProperties,
                                          PostStore postStore,
                                          HosVersionProperties hosVersionProperties,
                                          ConvertPostService convertPostService,
                                          SelectUserInfoService selectUserInfoService,
                                          HosUserDetailsService hosUserDetailsService,
                                          HosFSProperties hosFSProperties) {
        super(null, null, hosTokenAuthenticationTokenGenerator, hosSecurityProperties, postStore, hosVersionProperties, convertPostService, selectUserInfoService);
        this.hosUserDetailsService = hosUserDetailsService;
        this.hosFSProperties = hosFSProperties;
        this.hosVersionProperties = hosVersionProperties;
        this.convertPostService = convertPostService;
        this.hosTokenAuthenticationTokenGenerator = hosTokenAuthenticationTokenGenerator;
        this.postStore = postStore;
    }

    @Override
    public boolean supports(Class&lt;?> authentication) {
        return authentication.isAssignableFrom(HosFSAuthenticationToken.class);
    }

    /**
     * 根据登录参数获取用户信息
     *
     * @param authentication
     * @return
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        HosFSAuthenticationToken fsAuthenticationToken = (HosFSAuthenticationToken) authentication;
        //获取token
        String token=fsAuthenticationToken.getLoginParam().getToken();
        //获取链接、系统标识system-identifier
        if (StringUtil.isBlank(hosFSProperties.getUrl())){
            SecurityUtil.throwError("000099-01","请联系管理员维护验证token接口地址");
        }
        if (StringUtil.isBlank(hosFSProperties.getSystemIdentifier())){
            SecurityUtil.throwError("000099-02","请联系管理员维护系统标识");
        }
        JSONObject param=new JSONObject();
        param.set("token", token);

        Map&lt;String, String> headers=new HashMap&lt;String, String>();
        headers.put("system-identifier", hosFSProperties.getSystemIdentifier());
        String value = HttpUtils.postJson(hosFSProperties.getUrl(),headers, param.toString());
        log.error("FS验证token===接口返回数据: " + value);
        if(CommonUtils.isEmpty(value) || value.trim().startsWith("&lt;")){
            log.error("FS验证token===接口返回数据: " + value);
            SecurityUtil.throwError("000099-03","验证token接口异常");
        }
        JSONObject result = JSONUtil.parseObj(value);
        String personId ="";
        String userCode="";
        if(CommonUtils.equals(result.getStr("code"),"000000")){
            //验证成功,获取登录信息
            JSONObject data =result.getJSONObject("data");
            personId = data.getStr("personId");
            if (StringUtil.isBlank(personId)){
                SecurityUtil.throwError("000099-05","用户名异常");
            }else{
                userCode=personId.split("@")[0];
            }
        }else{
            SecurityUtil.throwError("000099-05",result.getStr("message"));
        }

        HosUserDetails hosUserDetails = hosUserDetailsService.loadUserByUsername(userCode);
        //根据人员获取岗位
        String pId = "";
        if (!SecurityUtil.isAdmin(hosUserDetails.getHosUser().getAccountCode())) {
            pId = hosUserDetails.getHosUser().getPersonId();
        } else {
            pId = "admin";
        }
        JSONObject post = null;
        //如果当前版本不是简版而且传了岗位信息,需要获取对应的岗位,调用接口传了岗位信息,就以传的岗位登录
        if (!CommonUtils.equals(hosVersionProperties.getVersion(), VersionEnum.SIMPLE.getCode())) {
            String postStr = convertPostService.getPostByPostInfo(null, null, null, null, pId);
            if (CommonUtils.isEmpty(postStr)) {
                SecurityUtil.throwError(BaseBusinessExceptionEnum.POST_ERROR);
            } else {
                post = JSONUtil.parseObj(postStr, true);
            }

        }
        TokenUniqueKey tokenUniqueKey = new TokenUniqueKey();
        Map&lt;String, AbstractHosAuthenticationToken> authenticationMap = new HashMap&lt;>();
        authenticationMap.put(fsAuthenticationToken.getClass().getName(), fsAuthenticationToken);
        HosUser hosUser = hosUserDetails.getHosUser();
        if (CommonUtils.isNotNull(post)) {
            if (CommonUtils.equals(hosVersionProperties.getVersion(), VersionEnum.PROFESSIONAL.getCode())) {
                JSONArray postRecords = (JSONArray) post.get("postRecords");
                List&lt;String> postSourceIds = new ArrayList&lt;>();
                if (CommonUtils.isNotEmpty(postRecords)) {
                    for (Object postRecord : postRecords) {
                        JSONObject object = (JSONObject) postRecord;
                        String id = object.getStr("postSourceId");
                        if (StringUtil.isNotBlank(id)) {
                            postSourceIds.add(id);
                        }
                    }
                    if (CommonUtils.isNotEmpty(postSourceIds)) {
                        //todo 调用基础平台实现的service,获取岗位id转换信息
                        String postIds = convertPostService.convertPostId(postSourceIds);
                        hosUser.setPostId(postIds);

                        log.info("第一次放入postId---------------------------------");
                    } else {
                        SecurityUtil.throwError(BaseBusinessExceptionEnum.POST_ERROR);
                    }
                    hosUser.setPostName(post.getStr("name"));
                } else {
                    SecurityUtil.throwError(BaseBusinessExceptionEnum.POST_ERROR);
                }
            } else if (CommonUtils.equals(hosVersionProperties.getVersion(), VersionEnum.WROUGHT.getCode())) {
                String postCode = post.getStr("postCode");
                if (StringUtil.isBlank(postCode)) {
                    SecurityUtil.throwError(BaseBusinessExceptionEnum.POST_ERROR);
                } else {
                    //todo 调用基础平台实现的service,获取岗位id转换信息
                    String postIds = convertPostService.convertPostIdByCode(postCode);
                    hosUser.setPostId(postIds);
                    hosUser.setPostName(post.getStr("postName"));
                    log.info("第一次放入postId---------------------------------");
                }
            }
            hosUser.setPostOneVO(post);
        } else {
            hosUser.setAuthVersion(VersionEnum.SIMPLE.getCode());
        }
        return hosTokenAuthenticationTokenGenerator.generate(authenticationMap, hosUserDetails.getHosUser(), tokenUniqueKey);
    }

}

# 第六步,配置类增加扩展登录

找到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 HosFSAuthenticationConverter());
         //注入自定义Provider
         loginTokenEndpoint.authenticationProvider(new HosFSAuthenticationProvider(HosConfigurerUtils.getHosTokenAuthenticationTokenGenerator(http),
                 HosConfigurerUtils.getHosSecurityProperties(http),
                 SpringContextUtils.getBean(PostStore.class),
                 SpringContextUtils.getBean(HosVersionProperties.class),
                 SpringContextUtils.getBean(ConvertPostService.class),
                 SpringContextUtils.getBean(SelectUserInfoService.class),
                 SpringContextUtils.getBean(HosUserDetailsService.class),
                 SpringContextUtils.getBean(HosFSProperties.class)
         ));
        });
        http.apply(hosLoginConfigurer);
        
        ...

        return http.build();
    }

# 第七步,测试

启用后端服务器,调用登录接口/api/security/token

参数说明:query中grantType等于第二步自定义token类中HosGrantType的值,body数据为第一步自定义参数的值。

登录扩展测试