认证
认证是验证与系统交互的用户或进程的身份的过程。例如,系统可以通过用户名和密码对用户进行认证。对已经认证的用户,系统可以执行 授权,检查用户对特定资源的权限。
Jmix 直接使用 Spring Security servlet 认证,因此,如果对这个框架比较熟悉,可以很容易地扩展或覆盖 Jmix 提供的内置标准认证机制。
当前用户
如需确定当前的认证用户,请使用 CurrentAuthentication
bean。有以下方法:
-
getUser()
以 UserDetails 返回当前认证用户。可以将其转换为项目中定义的 用户实体。 -
getAuthentication()
返回当前线程中设置的 Authentication 对象。可以使用它来获取当前用户的 授权 集合。在标准的 Jmix 安全实现中,授权集合包含分配给用户的 资源 和 行级 角色对象。 -
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、GraphQL 或 REST API。每个客户端都有自己的标准认证机制,示例:
自定义密码验证
如需实现自定义密码验证,需要创建一个或多个 bean,实现接口 PasswordValidator
,示例:
package security.ex1.security;
import io.jmix.securityui.password.PasswordValidationContext;
import io.jmix.securityui.password.PasswordValidationException;
import io.jmix.securityui.password.PasswordValidator;
import org.springframework.stereotype.Component;
import security.ex1.entity.User;
@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
protected void onBeforeCommit(BeforeCommitChangesEvent event) {
if (entityStates.isNew(getEditedEntity())) {
// ...
List<String> validationErrors = passwordValidation.validate(getEditedEntity(), passwordField.getValue());
if (!validationErrors.isEmpty()) {
notifications.create(Notifications.NotificationType.WARNING)
.withCaption(String.join("\n", validationErrors))
.show();
event.preventCommit();
}
getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue()));
}
}
防暴力破解
框架具有防止暴力破解密码的机制。
使用 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
。
会话属性
如需在同一用户的不同请求中共享值,请使用 SessionData
bean。其中包含读写当前用户会话中命名值的方法。
可以直接在 UI 界面注入 SessionData
bean:
public class CustomerBrowse extends StandardLookup<Customer> {
@Autowired
private SessionData sessionData;
在 singleton 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 查询语句 中。 |
当处理 UI 请求时,共享值保存在 HTTP 会话中。
如需在使用同一个 token 的 REST API 请求之间共享会话属性,请在 build.gradle
添加下列依赖:
implementation 'io.jmix.sessions:jmix-sessions-starter'
系统认证
如果执行线程由内部调度程序启动或处理来自 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 包含用户实体。 |