认证
认证是验证与系统交互的用户或进程的身份的过程。例如,系统可以通过用户名和密码对用户进行认证。对已经认证的用户,系统可以执行 授权,检查用户对特定资源的权限。
Jmix 直接使用 Spring Security servlet 认证,因此,如果对这个框架比较熟悉,可以很容易地扩展或覆盖 Jmix 提供的内置标准认证机制。
当前用户
如需确定当前的认证用户,请使用 CurrentAuthentication
bean。有以下方法:
-
getUser()
以 UserDetails 返回当前认证用户。可以将其转换为项目中定义的 用户实体。 -
getAuthentication()
返回当前线程中关联的 Authentication 对象。Authentication
对象中带有用户角色的名称信息。Jmix 使用 Spring Security 的 SimpleGrantedAuthority 类表示用户的角色。这个类用单一字符串表示一个角色。字符串的格式为:
如需从角色编码创建授权的 Java 类和内容,可以使用
RoleGrantedAuthorityUtils
类。通过标准的 Spring 机制可以自定义资源角色的前缀,需要配置
org.springframework.security.config.core.GrantedAuthorityDefaults
bean。类似地,行级角色的前缀可以通过
jmix.security.default-row-level-role-prefix
应用程序属性修改。 -
getLocale()
和getTimeZone()
返回当前用户的语言环境和时区。 -
isSet()
如果当前线程已通过认证,则返回true
,即已经包含用户的相关信息。否则,上面的getUser()
、getLocale()
和getTimeZone()
方法将抛出IllegalStateException
。
下面是获取当前用户信息的示例:
@Autowired
private CurrentAuthentication currentAuthentication;
private void printAuthenticationInfo() {
UserDetails user = currentAuthentication.getUser();
Authentication authentication = currentAuthentication.getAuthentication();
Locale locale = currentAuthentication.getLocale();
TimeZone timeZone = currentAuthentication.getTimeZone();
System.out.println(
"User: " + user.getUsername() + "\n" +
"Authentication: " + authentication + "\n" +
"Roles: " + getRoleNames(authentication) + "\n" +
"Locale: " + locale.getDisplayName() + "\n" +
"TimeZone: " + timeZone.getDisplayName()
);
}
private String getRoleNames(Authentication authentication) {
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
}
例如,可以使用 |
客户端认证
后端 Jmix 应用程序可以支持不同的客户端,例如,Jmix UI 或 REST API。每个客户端都有自己的标准认证机制,例如,UI 登录视图和 REST 访问 token。
自定义密码验证
如需实现自定义密码验证,需要创建一个或多个 bean,实现接口 PasswordValidator
,示例:
package com.company.demo.security;
import com.company.demo.entity.User;
import io.jmix.securityflowui.password.PasswordValidationContext;
import io.jmix.securityflowui.password.PasswordValidationException;
import io.jmix.securityflowui.password.PasswordValidator;
import org.springframework.stereotype.Component;
@Component
public class MyPasswordValidator implements PasswordValidator<User> {
@Override
public void validate(PasswordValidationContext<User> context) throws PasswordValidationException {
if (context.getPassword().length() < 3)
throw new PasswordValidationException("Password is too short, must be >= 3 characters");
}
}
在 ChangePassword - 修改密码
操作的对话框会自动使用所有的密码验证 bean。
如需在用户详情视图添加自定义密码验证,可以使用 PasswordValidation
辅助 bean:
@Autowired
private PasswordValidation passwordValidation;
@Subscribe
public void onValidation(final ValidationEvent event) {
// ...
if (entityStates.isNew(getEditedEntity())) {
List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
if (!validationErrors.isEmpty()) {
event.getErrors().add(String.join("\n", validationErrors));
}
}
防暴力破解
框架具有防止暴力破解密码的机制。
使用 jmix.security.bruteforceprotection.enabled 应用程序属性启用该保护机制。如果启用了保护,则在多次尝试登录失败后,在一段时间内阻止相同用户名和 IP 地址再次登录。相同用户名和 IP 地址的最大尝试登录次数由 jmix.security.bruteforceprotection.max-login-attempts-number 应用属性定义。阻止时长由 jmix.security.bruteforceprotection.block-interval 应用程序属性定义,单位为秒。
-
jmix.security.bruteforceprotection.enabled
启用防止暴力破解密码的机制。默认值:
false
。
-
jmix.security.bruteforceprotection.block-interval
如果 jmix.security.bruteforceprotection.enabled 属性启用,定义超过最大失败登录次数后阻止再次登录的时间(单位为秒)。默认值:
60 秒
。
-
jmix.security.bruteforceprotection.max-login-attempts-number
如果 jmix.security.bruteforceprotection.enabled 属性启用开,定义相同用户名和 IP 地址的最大失败尝试登录次数。默认值:
5
。
系统认证
如果执行线程由内部调度程序启动或处理来自 JMX 接口的请求,则无法对其进行认证。然而此时,业务逻辑或数据访问代码通常需要知道当前使用系统的用户信息,以便进行日志记录或授权。
如需将当前执行线程与用户临时关联,请使用 SystemAuthenticator
bean。有以下方法:
-
withSystem()
- 接收一个 lambda 方法,并以 system 用户执行。 -
withUser()
- 接收一个普通用户的用户名和一个 lambda 方法,并以给定用户的权限执行。
下面是对一个 MBean 操作进行认证的示例:
@Autowired
private SystemAuthenticator systemAuthenticator;
@Autowired
private CurrentAuthentication currentAuthentication;
@ManagedOperation
public String doSomething() {
return systemAuthenticator.withSystem(() -> {
UserDetails user = currentAuthentication.getUser();
System.out.println("User: " + user.getUsername()); // system
// ...
return "Done";
});
}
@ManagedOperation
public String doSomething2() {
return systemAuthenticator.withUser("admin", () -> {
UserDetails user = currentAuthentication.getUser();
System.out.println("User: " + user.getUsername()); // admin
// ...
return "Done";
});
}
也可以使用 @Authenticated
注解对整个 bean 方法进行认证,并使用 system
用户执行。示例:
@Autowired
private CurrentAuthentication currentAuthentication;
@Authenticated // authenticates the entire method
@ManagedOperation
public String doSomething3() {
UserDetails user = currentAuthentication.getUser();
System.out.println("User: " + user.getUsername()); // system
// ...
return "Done";
}
认证事件
Spring 框架会发送与认证相关的特定应用程序事件。
Studio 可以帮助你生成认证事件的监听器。在 Jmix 工具窗口中单击 New (+) → Event Listener,然后在对话框中选择 Authentication Event 即可。 |
下面是处理认证事件的示例。
@Component
public class AuthenticationEventListener {
private static final Logger log =
LoggerFactory.getLogger(AuthenticationEventListener.class);
@EventListener
public void onInteractiveAuthenticationSuccess(
InteractiveAuthenticationSuccessEvent event) { (1)
User user = (User) event.getAuthentication().getPrincipal(); (2)
log.info("User logged in: " + user.getUsername());
}
@EventListener
public void onAuthenticationSuccess(
AuthenticationSuccessEvent event) { (3)
User user = (User) event.getAuthentication().getPrincipal(); (4)
log.info("User authenticated " + user.getUsername());
}
@EventListener
public void onAuthenticationFailure(
AbstractAuthenticationFailureEvent event) { (5)
String username = (String) event.getAuthentication().getPrincipal(); (6)
log.info("User login attempt failed: " + username);
}
@EventListener
public void onLogoutSuccess(LogoutSuccessEvent event) { (7)
User user = (User) event.getAuthentication().getPrincipal(); (8)
log.info("User logged out: " + user.getUsername());
}
}
1 | InteractiveAuthenticationSuccessEvent 当用户通过 UI 或 REST API 登录时发送。 |
||
2 | InteractiveAuthenticationSuccessEvent 包含用户实体。 |
||
3 | AuthenticationSuccessEvent 任何认证(包括 系统认证)成功时发送。
|
||
4 | AuthenticationSuccessEvent 包含用户实体。 |
||
5 | AbstractAuthenticationFailureEvent 当尝试认证失败时发送,例如,由于凭证无效。 |
||
6 | AbstractAuthenticationFailureEvent 仅包含用于认证的用户名。 |
||
7 | LogoutSuccessEvent 当用户登出时发送。 |
||
8 | LogoutSuccessEvent 包含用户实体。 |
用户会话
当通过 Jmix UI 或 REST API 连接 Jmix 应用程序时,会创建一个基于 HTTP 会话的用户会话(user session)。本节介绍不同客户端中关于用户会话的细节,如何管理会话超时以及如何在用户会话中保存自定义的值。
在运行时可以通过 Audit 扩展组件的 用户会话 视图跟踪用户的会话信息。
每个用户的最大会话数量可以通过 jmix.core.session.maximum-sessions-per-user 应用程序属性定义。
UI 中的会话
默认情况下,只要有一个浏览器标签页打开了应用程序,UI 的用户会话就会一直保持。如果应用程序的所有浏览器标签页都关闭了,用户会话还会持续 server.servlet.session.timeout 应用程序属性定义的时长。
server.servlet.session.timeout 属性仅在通过可执行 JAR 部署应用程序的情况下对内嵌的 web 服务有效。如果项目是通过 WAR 部署至外部的 web 服务,那么需要使用该 web 服务的配置设置 HTTP 会话的超时时限。
|
下面的应用程序属性也会影响会话的超时:
-
vaadin.heartbeatInterval
- 当浏览器 tab 打开时,Vaadin 客户端往服务端发送的心跳请求频率(单位为秒)。默认值是 300(5 分钟)。通过心跳可以保证在用户没有操作的情况下会话的活跃度。该频率的配置必须小于server.servlet.session.timeout
的配置。 -
vaadin.closeIdleSessions
- 如果设置为true
,则忽略心跳请求,会话在server.servlet.session.timeout
定义的时间内如果没有活动则过期。
下面的示例中,设置会话的超时时限为 10 分钟,心跳频率为 3 分钟:
server.servlet.session.timeout=10m
vaadin.heartbeatInterval=180
参阅 Vaadin 文档:用户会话。
REST API 中的会话
默认情况下,REST API 中的用户会话是按请求创建的,过期时间依据 server.servlet.session.timeout
的设置。如果客户端支持 cookies,则会话在使用相同 cookie 的多个请求中保持。
Jmix Sessions 子系统将用户会话与 REST 端点中使用的 OAuth2 token 进行了绑定。也就是说,使用相同 token 的 REST 请求共享一个会话。如果需要在项目中使用 Jmix Sessions,请在 build.gradle
中添加:
implementation 'io.jmix.sessions:jmix-sessions-starter'
如果由于服务端重启导致会话丢失,但 token 却仍然保留(比如保存在数据库中),那么会为该 token 创建一个新的会话。 |
会话属性
如需在同一用户会话的不同请求中共享某些值,请使用 SessionData
bean。其中包含读写当前用户会话中命名值的方法。
可以直接在 UI 视图注入 SessionData
bean:
public class CustomerListView extends StandardListView<Customer> {
@Autowired
private SessionData sessionData;
在单例 bean 中,通过 org.springframework.beans.factory.ObjectProvider
使用 SessionData
:
@Component
public class CustomerService {
@Autowired
private ObjectProvider<SessionData> sessionDataProvider;
public void saveSessionValue(String value) {
sessionDataProvider.getObject().setAttribute("my-attribute", value);
}
会话属性也可以用在 JPQL 查询语句 中。 |