更多配置
以下说明描述了如何扩展 Jmix SAML 默认配置。在完成 Keycloak SAML 配置 或其他身份 provider 的等效基本配置后,再按需做以下配置。
内存中 SAML 用户的菜单可见性
在默认 SAML 配置下,SAML 用户仅存在于内存中。没有持久化 User 实体作为支撑,因此除非显式处理 SAML 断言属性,否则主视图中可能无法显示用户的名称或头像。
如需使内存中的 SAML 用户在用户菜单中可见:
-
在 SAML 断言中包含
FirstName和LastName属性。 -
调整
generateUserName方法,使其能够从持久化User实体或内存中用户的 SAML 断言属性构建显示名称。MainView.javaprivate 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 断言包含有关用户的信息(例如其姓名、职位、部门或其他个人资料详细信息),则可以在用户会话有效时使用这些属性。
请按以下步骤:
-
创建一个扩展
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 属性的附加字段。 -
创建一个 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属性存储在MyUser的position字段中。3 这里,为用户分配权限。该示例没有直接实现逻辑,而是将其代理给 SamlAssertionRolesMapper接口,进而使用默认实现DefaultSamlAssertionRolesMapper。
使用不同的角色属性
默认情况下,DefaultSamlAssertionRolesMapper 会在 SAML 的 Assertion 中查找名为 Role 的属性。该属性包含角色名称的集合。对于每个角色名称,Jmix 会搜索匹配的资源角色和行级角色。如果找到匹配的角色,则为用户分配相应的角色。
如果身份提供商在 Role 之外的属性中发送角色,可以使用以下 Jmix SAML 属性 来更改属性名称:
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 资源角色:
-
如果
Position是Manager,则为用户分配edit-contracts和view-archive角色。 -
否则,为用户分配
view-contracts角色。
将用户持久化到数据库
默认情况下,Jmix SAML 的配置只是将认证用户保存在内存中。如果希望将 SAML 用户存储在数据库中,请按以下步骤:
-
扩展
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 { //... } -
注册基于
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时,角色分配也会存储到数据库中。