认证

认证是验证与系统交互的用户或进程的身份的过程。例如,系统可以通过用户名和密码对用户进行认证。对已经认证的用户,系统可以执行 授权,检查用户对特定资源的权限。

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(","));
}

CurrentAuthentication 只是 SecurityContextHolder 的包装器,因此能与所有 Spring Security 机制兼容。

例如,可以使用 DelegatingSecurityContextRunnable 为新线程设置认证信息,参阅 Spring Security 文档

客户端认证

后端 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 包含用户实体。