# 单体登录

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

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

前端可以通过修改environment.js文件下的配置项isForceLocalLogin为true来强制开启单体登录

 isForceLocalLogin: true, // 是否强制使用单体登录,后端服务出现故障时手动开启

# 表单登录

# 简版

简版只需要输入登录相关数据即可登录

img.png

# 精简版

精简版除了需要输入登录相关数据外,还需要选择人员定岗数据才能登录(系统自己维护人员定岗数据)

img.png

# 专业版

专业版除了需要输入登录相关数据外,还需要选择岗位单元数据才能登录(岗位单元数据来源于资源计划排班系统)

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: 3000                      #访问token过期时间,单位:秒
      sign-key: 123123                                  #生成jwt token 签名key
      access-token-key: access_token                    #保存token缓存的前缀

参数说明:

参数名称 参数说明 备注
access-token-expires-in 访问token过期时间 单位:秒,默认259200秒(3天)。
sign-key 生成jwt token 签名key 集群模式下,需要各个服务配置相同的签名key
access-token-key 保存token缓存的前缀 默认:hos_access_token

提示

岗位过期时间大于配置的访问token过期时间:当用户岗位配置了岗位过期时间,则token过期时间与岗位过期时间一致,当未配置岗位过期时间时,token过期时间与yml配置文件一致。


# 登录日志配置

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();
    }

# 获取登录用户信息

登录token中存储了登录相关的信息,框架提供了获取登录信息的AccountUtil工具类,工具类里面提供了获取账号id、登录名、租户、岗位id、岗位名称、登录全局唯一标识方法, 用户尽量使用以上提供的方法,如果现有方法不满足用户使用,也可以通过调用获取token对象或者用户对象接口进行相关数据的获取。 AccountUtil提供的具体方法如下:

  • 获取登录用户账号id
/**
 * 获取登录用户账号id
 * 对应账号表hos_user_account的id字段值 
 * @return
 */
public static String getAccountId();
  • 获取登录名
 /**
 * 获取登录名 
 * 对应账号表hos_user_account的code字段值
 * @return
 */
public static String getAccountCode();
  • 获取登录用户的租户Id
 /**
 * 获取登录用户的租户Id
 * 对应账号表hos_user_account的tenant_id字段值
 **/
public static String getTenantId();
  • 获取当前登录用户对象
  /**
 * 获取当前登录用户对象
 **/
 public static HosUser getLoginUser();

HosUser对象属性说明:

属性名称 属性说明 备注
accountCode 账号code
accountId 账号id
accountName 账号名称
phoneNumber 手机号
userName 账号名称
password 账号密码 为空
tenantId 租户id
activity 是否启用
locked 是否锁定
unlockDate 解锁时间
lastLoginDate 上次登录时间
lastUpdatePassword 上次更新密码时间
needRestPassword 是否需要修改密码
loginDate 登录时间
globalUniqueID 本次登录状态的全局唯一id
personId 唯一标识码
startDate 开始日期
endDate 结束日期
postOneVO 岗位对象
postId 开始日期
postName 结束日期
caInfo ca认证信息 记录登录时的CA认证信息
  • 获取登录token对象
 /**
 * 获取登录token对象
 * @return
 */
public static HosTokenAuthenticationToken getToken();

HosTokenAuthenticationToken对象属性说明:

属性名称 属性说明 备注
details 客户端信息 记录ip、mac、浏览器、操作系统、主机名称
authenticated true 是否认证成功
uniqueKey 本地登录的全局唯一id
credentials 登录信息记录 客户端信息、登录方式、登录传参信息
principal HosUser对象 登录用户信息
accessToken HosAccessToken对象 包含tokenValue、开始时间、结束时间、tokenType、uniqueKey
name 登录名
  • 设置认证信息
/**
 * 设置认证信息
 * @return
 */
public static void setAuthentication(Authentication authentication);
  • 获取认证信息
/**
 * 获取认证信息
 * @return
 */
public static Authentication getAuthentication();

Authentication对象属性说明:

属性名称 属性说明 备注
details 客户端信息 记录ip、mac、浏览器、操作系统、主机名称
authenticated true 是否认证成功
credentials 登录信息记录 客户端信息、登录方式、登录传参信息
principal HosUser对象 登录用户信息
  • 获取登录全局唯一变量id
/**
 * 获取登录全局唯一变量id
 * @return
 */
public static String getGlobalUniqueID();
  • 获取岗位id
/**
 * 获取岗位id
 * @return
 */
public static String getPostId();
  • 获取岗位名称
/**
 * 获取岗位名称
 * @return
 */
public static String getPostName();

# 免密认证源登录

免密认证源登录就是用户无感知访问第三方应用系统。 传统免密登录源系统访问目标系统时会在URL链接上携带用户名信息,这种方式非常不安全。所以HOS免密登录采用了源系统生成临时token, 目标系统收到token之后,调用源系统获取用户信息接口返回当前用户信息,目标系统接收到用户信息之后登录的方式,避免了用户信息的泄露。
下面以OA系统免密登录HRP系统为例说明HOS免密登录流程以及配置。

# 免密认证流程

img.png

免密认证流程描述

(1)OA系统根据用户信息生成临时token。
(2)OA系统将生成临时token和登录类型loginType(固定值为freeAuth)、和来源source(双方系统约定好,用于区别免密访问的源系统)拼接到URL的query参数中;另外,如果业务系统是精简版而且要实现以指定岗位免密登录系统,需要新增参数 buUnitCode(业务单元编码)、buPostCode(业务岗位编码),如果业务系统是专业版而且要实现以指定岗位免密登录系统,需要新增参数 unitType(岗位单元类型)、unitId(岗位单元 id)
(3)浏览器跳转或打开以上生成的URL。
(4)HRP系统前端根据loginType=freeAuth进行拦截
(5)HRP系统获取OA系统生成的token,然后调用OA系统验证token的接口。
(6)OA系统验证成功,返回用户信息。
(7)HRP系统拿到用户信息之后本系统登录。

# 免密认证配置

# 源系统配置

以OA系统免密登录HRP系统为例,源系统则为OA系统。如果OA系统时基于HOS基础开发框架开发的,并且通过菜单打开HRP系统(目标系统)则需要在 相应的菜单中配置要方位HRP系统的URL地址,开启外链以及开启免密登录同时配置来源(双方系统约定好的来源)。

img.png

img.png


提示

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


当OA系统基于HOS基础平台开发的,但不是通过菜单打开HRP时,则需要开发人员按照以上流程规则拼接好URL参数(临时token和登录类型loginType和来源source),然后打开或者跳转到HRP系统。
当OA系统不是基于HOS基础平台开发的,则需要OA系统按照以上规则拼接URL参数的同时,还需要提供一个根据token获取用户信息的接口。
如果OA系统是基于HOS基础平台开发的,还需确认验证token接口是否配置了登录认证白名单。
hos-security:
  login:   
    white-list:                                         #认证白名单
      - /login/passwordFree/userInfo   #免密认证源验证Token并返回用户的接口地址

# 目标系统配置

当目标系统HRP是基于HOS基础平台开发的系统时,需要打开【系统管理】->【免密登录】,新增一条配置。

img.png

img.png


提示

编码:免密访问认证源编码,本示例的该值是OA
名称:名称
是否启用:开启
验证token地址:如果OA系统基于HOS基础平台开发的则配置OA系统的/login/passwordFree/userInfo接口即可,否则需要OA系统的开发人员提供


当HRP系统不是基于HOS基础框架开发时,则需要HRP系统按照以上免密登录流程开发相应功能。

# 测试和问题

在OA系统中点击相应的菜单或者打开相应的功能,正常情况下会成功进入第三方应用的系统页面

img.png

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

img.png

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

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

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

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

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

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

报错四:系统内部报错

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

报错五:没有免密验证token接口的调用权限

解决思路:需要将验证token接口地址配置到白名单

报错六:免密验证token接口异常

解决思路:配置的验证token接口地址错误或者调用不通

# 登录认证扩展

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

登录扩展测试