认证
认证是验证与系统交互的用户或进程的身份的过程。例如,系统可以通过用户名和密码对用户进行认证。对已经认证的用户,系统可以执行 授权,检查用户对特定资源的权限。
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
。
会话属性
如需在同一用户的不同请求中共享值,请使用 SessionData
bean。其中包含读写当前用户会话中命名值的方法。
可以直接在 UI 视图注入 SessionData
bean:
public class CustomerListView extends StandardListView<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 会话中。
系统认证
如果执行线程由内部调度程序启动或处理来自 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 包含用户实体。 |