# 登录

登录就是用户通过输入用户信息来获取访问系统的权限。目前登录支持两种类型。 通过编辑配置文件中hos-security:oauth2:enable的值来实现表单和单点登录的切换。

hos-security:
  oauth2:                                               #统一认证oauth2登录配置
    enable: false   

# 表单登录

img.png

# 单点登录

  • 编辑resources目录下的applicaiton-dev.yml
hos-security:
  front-url: https://114.251.235.9:8323                  #应用前端页面地址
  authentication-uri: https://117.107.157.46:8129             #统一认证地址
  oauth2:                                               #统一认证oauth2登录配置
    enable: true                                        #是否开启统一认证登录
    state-expire-in: 600                                #oauth2登录中state的有效时间,单位:秒
    client:                                             #统一认证登录配置
      registration:
        messaging-client-oidc:
          provider: spring                              #对应下边 provider 的配置,不需要修改
          client-id: messaging-client                   #统一用户平台中创建应用的id
          client-secret: secret                         #统一用户平台中创建应用的秘钥
          authorization-grant-type: authorization_code  #认证类型,不要修改
          redirect-uri: ${hos-security.front-url}/api/security/login/oauth2/code/messaging-client-oidc  #回调地址,不要修改
          scope: openid                                #统一用户平台中创建应用的作用域
          client-name: hos-app                          #该客户端名称
      provider:
        spring:
          issuer-uri: ${hos-security.authentication-uri}/api                      #发行地址
          authorization-uri: ${hos-security.authentication-uri}/oauth/authorize   #获取授权码页面

# 登录配置

# token 配置

hos-security:
  login:                                                #登录相关配置
    token:                                              #token相关配置
      access-token-expires-in: 300                      #访问token过期时间,单位:秒
      refresh-token-expires-in: 600                     #刷新token过期时间,单位:秒
      enable-refresh-token: true                        #是否开启刷新token
      sign-key: 123123                                  #生成jwt token 签名key
      access-token-key: access_token                    #保存token缓存的前缀
      refresh-token-key: refresh_token                  #保存刷新token缓存的前缀

参数说明:

参数名称 参数说明 备注
access-token-expires-in 访问token过期时间 单位:秒,默认259200秒(3天)。
refresh-token-expires-in 刷新token过期时间 单位:秒,默认345600秒(4天)。当enable-refresh-token为true时生效。
enable-refresh-token 是否开启刷新token 默认false,false:单token模式,true:双token模式
sign-key 生成jwt token 签名key 集群模式下,需要各个服务配置相同的签名key
access-token-key 保存token缓存的前缀 默认:hos_access_token
refresh-token-key 保存刷新token缓存的前缀 默认:hos_access_token

提示

岗位过期时间大于配置过期时间:当用户岗位配置了岗位过期时间,则token过期时间与岗位过期时间一致,当未配置岗位过期时间时,token过期时间与yml配置文件一致。
当配置为双token时,refresh-token-expires-in(刷新token过期时间)必须大于access-token-expires-in(访问token过期时间)。

# 登录日志配置

hos-security:
  login:                                              #登录相关配置
    log:
      policy: sync                                    #登录日志记录策略 sync:同步插入,async:异步插入
      async-size: 1000                                #日志异步插入时,批量插入的条数
      async-cron: 0/30 * * * * ?                      #日志异步插入时,多长时间清理一次

参数说明:

参数名称 参数说明 备注
policy 登录日志记录策略 sync:同步插入,async:异步插入。默认同步插入
async-size 日志异步插入时,批量插入的条数 当policy=async生效,默认2000
async-cron 日志异步插入时,多长时间清理一次 当policy=async生效,默认0/30 * * * * ?

提示

日志记录策略分为同步插入和异步插入。同步插入即为用户登录成功时立即插入日志。当登录并发量较大时同步插入登录日志会给数据库带来很大压力 从而影响其他主业务流程,为了解决这个问题我们提供了异步插入的机制。当开启异步插入时,触发批量插入的条件是配置的async-cron时间段内日志 条数达到async-size或者每隔async-cron时间段触发。由于异步记录在日志入库之前是在服务内存中存放的,所以当服务宕机时可能会有部分日志丢失 ,请注意。

# 接口跳过token续期

一般项目要求用户一段时间不操作,跳转到登录页。所以当接口访问HOS应用系统时,如果用户已经登录,则会在接口调用系统时续期用户登录token。 但是有的接口并不是用户操作才会调用的,比如前端定时轮训调用后端接口或者websocket接口等,都会导致token续期。这肯定不符合业务需求。 所以需要配置这些接口跳过token续期。

配置如下:

framework:
  security:
    renewal:
      ignores:
        - /websocket/heartbeat
        - /user/info

# 认证白名单配置

访问系统的接口绝大多数接口都需要用户认证,不需要用户认证的接口需要配置到白名单中。配置白名单分为两种方式。推荐使用方式一配置,可以简化yml文件中的配置显得更简洁。

# 方式一

在每个模块或者项目的resources/META-INF文件夹下创建hos-security.factories文件。然后按照如下格式编辑hos-security.factories文件。

security-white=\
/open/resource/info\
/openApi/*\
/test

# 方式二

在项目启动yml文件中配置白名单

hos-security:
    white-list:                                         #认证白名单
      - /open/resource/info                             #示例仅供参考
      - /openApi/*
      - /test

# 配置security白名单

找到SecurityLoginConfig类或者配置SecurityFilterChain的类,配置跳过认证。


@Bean
    @Order(value = Ordered.HIGHEST_PRECEDENCE + 100)
    SecurityFilterChain securityFilterChain(HttpSecurity http, HosSecurityProperties hosSecurityProperties, HosPostInfoService hosPostInfoService) throws Exception {
        // 配置白名单(加载yml和hos-security.factories配置的白名单)
        List<String> whiteList = SecurityUtil.getWhiteList(hosSecurityProperties.getLogin().getWhiteList());
        http
                .authorizeHttpRequests(authorize ->
                        authorize
                                .antMatchers(whiteList.stream().toArray(String[]::new)).permitAll()
                                .anyRequest().authenticated()
                );
        ......
        return http.build();
    }

# 免密认证源登录

免密认证源登录就是用户无感知访问第三方应用系统。

免密认证流程

img.png

免密认证流程描述

(1)、应用系统以iframe形式嵌入其他应用的页面。
(2)、应用系统需要新增其他应用菜单,其他应用提供页面访问地址、认证源编码。
(3)、应用系统打开第三方应用的菜单。
(4)、应用系统根据当前用户生成临时token
(5)、携带token以及认证源编码访问第三方应用地址。
(6)、第三方系统接收到字符串参数之后,调用免密认证源验证Token并返回用户的接口地址并完成应用系统的登录认证。
(7)、应用系统成功打开第三方应用的页面。

应用系统集成第三方应用系统页面--基于HOS开发的系统

第一步、第三方应用维护免密认证源

`免密登录配置访问地址:https://114.251.235.9:8346 接下来以HRP系统为例,在HRP系统免密登录配置模块配置OA系统认证源 进入【系统管理】->【免密登录】

img.png

img.png


提示

编码:免密访问认证源编码,本示例的该值是OA
名称:名称
是否启用:开启
验证token地址:OA系统验证token并返回登录用户的接口地址(https://114.251.235.9:8334/api/login/passwordFree/userInfo)


第二步、应用系统新增第三方应用的菜单

菜单配置访问地址:https://114.251.235.9:8334 接下来以OA系统为例,在OA系统配置菜单,菜单外链第三方系统的页面; 进入【系统管理】->【菜单管理】

img.png

img.png


提示

路由:第三方应用系统页面的访问地址
打开方式:选择iframe
是否外链:开启
是否免密:开启
认证源编码:第三方系统提供认证源编码,本示例的该值是OA


第三步、应用系统新增配置

应用系统需要在配置文件中hos-security:login:white-list新增免密认证源验证Token并返回用户的接口地址。

hos-security:
  login:   
    white-list:                                         #认证白名单
      - /login/passwordFree/userInfo   #免密认证源验证Token并返回用户的接口地址

第四步、在应用系统前端点击对应的菜单

正常情况下会成功进入第三方应用的系统页面

img.png

异常情况下,会在菜单页面显示错误信息,具体如下:

img.png

报错一:请维护免密认证源!!

解决思路:到第三方应用系统的【免密登录】模块确定是否配置了当前菜单指定的认证源,确保认证源配置的编码和验证token地址正确

报错二:freeToken无效或者异常!!

解决思路:首先到第三方应用系统的【免密登录】模块确定是否配置了当前菜单指定的认证源,确保认证源配置的验证token地址正确 另外token目前的有效期是五分钟,确定是否过期,如果过期,重新生成token

报错三:用户或人员不存在,请联系管理员

解决思路:第三方系统里面没有对应的登录用户,需要在第三方系统创建对应的用户

报错四:系统内部报错

解决思路:首先到第三方应用系统的【免密登录】模块确定是否配置了当前菜单指定的认证源,确保认证源配置的验证token地址正确,如果正确的话,联系开发人员进行bug排查

# 登录认证扩展

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

登录扩展测试