# 登录
登录就是用户通过输入用户信息来获取访问系统的权限。目前登录支持两种类型。 通过编辑配置文件中hos-security:oauth2:enable的值来实现表单和单点登录的切换。
hos-security:
oauth2: #统一认证oauth2登录配置
enable: false
# 表单登录
# 单点登录
- 编辑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();
}
# 免密认证源登录
免密认证源登录就是用户无感知访问第三方应用系统。
免密认证流程
免密认证流程描述
(1)、应用系统以iframe形式嵌入其他应用的页面。
(2)、应用系统需要新增其他应用菜单,其他应用提供页面访问地址、认证源编码。
(3)、应用系统打开第三方应用的菜单。
(4)、应用系统根据当前用户生成临时token
(5)、携带token以及认证源编码访问第三方应用地址。
(6)、第三方系统接收到字符串参数之后,调用免密认证源验证Token并返回用户的接口地址并完成应用系统的登录认证。
(7)、应用系统成功打开第三方应用的页面。
应用系统集成第三方应用系统页面--基于HOS开发的系统
第一步、第三方应用维护免密认证源
`免密登录配置访问地址:https://114.251.235.9:8346
接下来以HRP系统为例,在HRP系统免密登录配置模块配置OA系统认证源
进入【系统管理】->【免密登录】
提示
编码:免密访问认证源编码,本示例的该值是OA
名称:名称
是否启用:开启
验证token地址:OA系统验证token并返回登录用户的接口地址(https://114.251.235.9:8334/api/login/passwordFree/userInfo)
第二步、应用系统新增第三方应用的菜单
菜单配置访问地址:https://114.251.235.9:8334
接下来以OA系统为例,在OA系统配置菜单,菜单外链第三方系统的页面;
进入【系统管理】->【菜单管理】
提示
路由:第三方应用系统页面的访问地址
打开方式:选择iframe
是否外链:开启
是否免密:开启
认证源编码:第三方系统提供认证源编码,本示例的该值是OA
第三步、应用系统新增配置
应用系统需要在配置文件中hos-security:login:white-list新增免密认证源验证Token并返回用户的接口地址。
hos-security:
login:
white-list: #认证白名单
- /login/passwordFree/userInfo #免密认证源验证Token并返回用户的接口地址
第四步、在应用系统前端点击对应的菜单
正常情况下会成功进入第三方应用的系统页面
异常情况下,会在菜单页面显示错误信息,具体如下:
报错一:请维护免密认证源!!
解决思路:到第三方应用系统的【免密登录】模块确定是否配置了当前菜单指定的认证源,确保认证源配置的编码和验证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<?> 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数据为第一步自定义参数的值。