# 单体登录
登录就是用户通过输入用户信息来获取访问系统的权限。目前登录支持两种类型。 后端通过编辑配置文件中hos-security:oauth2:enable的值来实现表单和单点登录的切换。
hos-security:
oauth2: #统一认证oauth2登录配置
enable: false
前端可以通过修改environment.js文件下的配置项isForceLocalLogin为true来强制开启单体登录
isForceLocalLogin: true, // 是否强制使用单体登录,后端服务出现故障时手动开启
# 表单登录
# 简版
简版只需要输入登录相关数据即可登录
# 精简版
精简版除了需要输入登录相关数据外,还需要选择人员定岗数据才能登录(系统自己维护人员定岗数据)
# 专业版
专业版除了需要输入登录相关数据外,还需要选择岗位单元数据才能登录(岗位单元数据来源于资源计划排班系统)
# 单点登录
- 编辑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免密登录流程以及配置。
# 免密认证流程
免密认证流程描述
(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地址,开启外链以及开启免密登录同时配置来源(双方系统约定好的来源)。
提示
路由:第三方应用系统页面的访问地址
是否外链:开启
是否免密:开启
认证源编码:第三方系统提供认证源编码,本示例的该值是OA
当OA系统不是基于HOS基础平台开发的,则需要OA系统按照以上规则拼接URL参数的同时,还需要提供一个根据token获取用户信息的接口。
如果OA系统是基于HOS基础平台开发的,还需确认验证token接口是否配置了登录认证白名单。
hos-security:
login:
white-list: #认证白名单
- /login/passwordFree/userInfo #免密认证源验证Token并返回用户的接口地址
# 目标系统配置
当目标系统HRP是基于HOS基础平台开发的系统时,需要打开【系统管理】->【免密登录】,新增一条配置。
提示
编码:免密访问认证源编码,本示例的该值是OA
名称:名称
是否启用:开启
验证token地址:如果OA系统基于HOS基础平台开发的则配置OA系统的/login/passwordFree/userInfo接口即可,否则需要OA系统的开发人员提供
# 测试和问题
在OA系统中点击相应的菜单或者打开相应的功能,正常情况下会成功进入第三方应用的系统页面
异常情况下,会在菜单页面显示错误信息,具体如下:
报错一:请维护免密认证源!!
解决思路:到第三方应用系统的【免密登录】模块确定是否配置了当前菜单指定的认证源,确保认证源配置的编码和验证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<?> 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数据为第一步自定义参数的值。
← Chrome浏览器配置 统一认证登录 →