更多配置

以下说明描述了如何扩展 Jmix SAML 默认配置。在完成 Keycloak SAML 配置 或其他身份 provider 的等效基本配置后,再按需做以下配置。

内存中 SAML 用户的菜单可见性

在默认 SAML 配置下,SAML 用户仅存在于内存中。没有持久化 User 实体作为支撑,因此除非显式处理 SAML 断言属性,否则主视图中可能无法显示用户的名称或头像。

如需使内存中的 SAML 用户在用户菜单中可见:

  1. 在 SAML 断言中包含 FirstNameLastName 属性。

  2. 调整 generateUserName 方法,使其能够从持久化 User 实体或内存中用户的 SAML 断言属性构建显示名称。

    MainView.java
        private String generateUserName(UserDetails userDetails) {
            if (userDetails instanceof User user) {
                String userName = String.format("%s %s",
                                Strings.nullToEmpty(user.getFirstName()),
                                Strings.nullToEmpty(user.getLastName()))
                        .trim();
    
                return userName.isEmpty() ? user.getUsername() : userName;
            }
    
            if (userDetails instanceof Saml2AuthenticatedPrincipal samlUser) {
                String userName = String.format("%s %s",
                                Strings.nullToEmpty(samlUser.getFirstAttribute("FirstName")),
                                Strings.nullToEmpty(samlUser.getFirstAttribute("LastName")))
                        .trim();
    
                return userName.isEmpty() ? userDetails.getUsername() : userName;
            }
    
            return userDetails.getUsername();
        }

    然后使用生成的名称渲染用户菜单按钮和 header:

        @Install(to = "userMenu", subject = "buttonRenderer")
        private Component userMenuButtonRenderer(final UserDetails userDetails) {
    
            String userName = generateUserName(userDetails);
            Avatar avatar = createAvatar(userName);
            //...
        }
    
        @Install(to = "userMenu", subject = "headerRenderer")
        private Component userMenuHeaderRenderer(final UserDetails userDetails) {
    
            String name = generateUserName(userDetails);
            Avatar avatar = createAvatar(name);
    
            // ...
        }

SAML 属性映射

在应用程序中映射属性之前,请确保这些属性已包含在 SAML 断言中。请参阅 在 Keycloak 中添加 SAML 属性 或其他身份 provider 对应的等效步骤。

如果 SAML 断言包含有关用户的信息(例如其姓名、职位、部门或其他个人资料详细信息),则可以在用户会话有效时使用这些属性。

请按以下步骤:

  1. 创建一个扩展 DefaultJmixSamlUserDetails 的类:

    public class MyUser extends DefaultJmixSamlUserDetails {
    
        private String position; (1)
    
        public String getPosition() {
            return position;
        }
    
        public void setPosition(String position) {
            this.position = position;
        }
    }
    1 对应于附加 SAML 属性的附加字段。
  2. 创建一个 Spring bean,将传入的 SAML 断言转换为 Jmix 用户对象。最简单的方法是扩展 BaseSamlUserMapper 并重写其方法。

    @Component
    public class MySamlUserMapper extends BaseSamlUserMapper<MyUser> {
    
        @Autowired
        protected SamlAssertionRolesMapper rolesMapper;
    
        @Override
        protected MyUser initJmixUser(Assertion assertion) { (1)
            return new MyUser();
        }
    
        @Override
        protected void populateUserAttributes(Assertion assertion, OpenSaml4AuthenticationProvider.ResponseToken responseToken, MyUser jmixUser) { (2)
            Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
            List<Object> rawValues = assertionAttributes.get("Position");
            String positionValue = CollectionUtils.isNotEmpty(rawValues) ? rawValues.get(0).toString() : null;
            jmixUser.setPosition(positionValue);
            System.out.println(positionValue);
        }
    
        @Override
        protected void populateUserAuthorities(Assertion assertion, MyUser jmixUser) { (3)
            Collection<? extends GrantedAuthority> grantedAuthorities = rolesMapper.toGrantedAuthorities(assertion);
            jmixUser.setAuthorities(grantedAuthorities);
        }
    }
    1 该方法创建一个 Jmix 用户对象,表示已认证的用户。
    2 这里,将 SAML Assertion 中的值复制到用户对象中。在示例中,Assertion 中的 Position 属性存储在 MyUserposition 字段中。
    3 这里,为用户分配权限。该示例没有直接实现逻辑,而是将其代理给 SamlAssertionRolesMapper 接口,进而使用默认实现 DefaultSamlAssertionRolesMapper

使用不同的角色属性

默认情况下,DefaultSamlAssertionRolesMapper 会在 SAML 的 Assertion 中查找名为 Role 的属性。该属性包含角色名称的集合。对于每个角色名称,Jmix 会搜索匹配的资源角色和行级角色。如果找到匹配的角色,则为用户分配相应的角色。

如果身份提供商在 Role 之外的属性中发送角色,可以使用以下 Jmix SAML 属性 来更改属性名称:

application.properties
jmix.saml.default-saml-assertion-roles-mapper.roles-assertion-attribute=MyRole

这告诉默认 mapper 从 MyRole 读取角色名称,而非 Role

创建自定义角色映射器

如果需要更多控制,可以创建自定义的角色映射器。当身份提供商不直接发送 Jmix 角色代码,而是发送其他需要转换为特定角色的内容时,可以使用这个方法。

例如,像 Manager 这样的值可能来自 Position 属性,可以使用自定义映射器分配适当的 Jmix 角色。如需实现该功能,扩展 BaseSamlAssertionRolesMapper 并重写其 getResourceRolesCodes()getRowLevelRolesCodes() 方法:

@Component
public class MySamlAssertionRolesMapper extends BaseSamlAssertionRolesMapper {

    @Override
    protected Collection<String> getResourceRolesCodes(Assertion assertion) {
        Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
        List<Object> rawPositionAttributeValues = assertionAttributes.get("Position");

        Collection<String> jmixRoleCodes = new HashSet<>();
        rawPositionAttributeValues.stream()
                .map(Object::toString)
                .forEach(position -> {
                    if ("Manager".equals(position)) {
                        jmixRoleCodes.add("edit-contracts");
                        jmixRoleCodes.add("view-archive");
                    } else {
                        jmixRoleCodes.add("view-contracts");
                    }
                });

        return jmixRoleCodes;
    }

    @Override
    protected Collection<String> getRowLevelRoleCodes(Assertion assertion) {
        // Do something for row-level role codes
        return List.of();
    }
}

在此示例中,用户的 Position 值决定了分配哪些 Jmix 资源角色:

  • 如果 PositionManager,则为用户分配 edit-contractsview-archive 角色。

  • 否则,为用户分配 view-contracts 角色。

将用户持久化到数据库

默认情况下,Jmix SAML 的配置只是将认证用户保存在内存中。如果希望将 SAML 用户存储在数据库中,请按以下步骤:

  1. 扩展 JmixSamlUserEntity 抽象类,使 User 实体与 Jmix SAML 扩展组件兼容:

    @JmixEntity
    @Entity
    @Table(name = "USER_", indexes = {
            @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true)
    })
    public class User extends JmixSamlUserEntity implements JmixUserDetails, HasTimeZone {
    
        //...
    
    }
  2. 注册基于 SynchronizingSamlUserMapper 的用户映射器;该父类在数据库中更新并存储用户,并可以将角色的分配结果同步到数据库:

    @Component
    public class MySynchronizingSamlUserMapper extends SynchronizingSamlUserMapper<User> {
    
        public MySynchronizingSamlUserMapper() {
            super();
            setSynchronizeRoleAssignments(true); (1)
        }
    
        @Override
        protected Class<User> getApplicationUserClass() {
            return User.class;
        }
    
        @Override
        protected void populateUserAttributes(Assertion assertion, OpenSaml4AuthenticationProvider.ResponseToken responseToken, User jmixUser) {
            String username = SamlAssertionUtils.getUsername(assertion);
            Map<String, List<Object>> assertionAttributes = SamlAssertionUtils.getAssertionAttributes(assertion);
            String firstNameValue = getStringAttributeValue(assertionAttributes, "FirstName", username);
            String lastNameValue = getStringAttributeValue(assertionAttributes, "LastName", username);
    
            jmixUser.setUsername(username);
            jmixUser.setFirstName(firstNameValue);
            jmixUser.setLastName(lastNameValue);
        }
    
        protected String getStringAttributeValue(Map<String, List<Object>> assertionAttributes, String attributeName, String username) {
            List<Object> rawValues = assertionAttributes.get(attributeName);
            return CollectionUtils.isNotEmpty(rawValues)
                    ? rawValues.get(0).toString()
                    : "%s (%s)".formatted(attributeName, username);
        }
    }
    1 当设置为 true 时,角色分配也会存储到数据库中。