具有特定表角色的spring security UserDetailsS​​ervice

Posted

技术标签:

【中文标题】具有特定表角色的spring security UserDetailsS​​ervice【英文标题】:spring security UserDetailsService with specific table Role 【发布时间】:2018-07-30 00:41:42 【问题描述】:

我正在创建一个必须使用 Spring Security 登录 的应用。它是标准的login/logout,我找到了很多如何创建它的教程。什么不是标准的 - 是数据库中的表角色。我不能更改数据库,我只能使用它。我为用户和角色创建了正确的实体,但我不知道如何正确编写UserDetailsServiceImplloadUserByUsername。我什至找不到接近的东西...

实体:

    @Entity
    @Table(name = "user")
    public class User implements model.Entity 

    @Id
    @GeneratedValue
    @Column(name = "userId", nullable = false)
    private int userId;

    @Column(name = "firstName")
    private String firstName;

    @Column(name = "lastName")
    private String lastName;

    @Column(name = "login", nullable = false)
    private String login;

    @Column(name = "password", nullable = false)
    private String password;

    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "roleId", nullable = false)
    private Set<Role> roleId;

    @Transient
    private String confirmPassword;

    public int getUserId() 
        return userId;
    

    public void setUserId(int userId) 
        this.userId = userId;
    

    public String getFirstName() 
        return firstName;
    

    public void setFirstName(String firstName) 
        this.firstName = firstName;
    

    public String getLastName() 
        return lastName;
    

    public void setLastName(String lastName) 
        this.lastName = lastName;
    

    public String getLogin() 
        return login;
    

    public void setLogin(String login) 
        this.login = login;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    public Set<Role> getRoleId() 
        return roleId;
    

    public void setRoleId(Set<Role> roleId) 
        this.roleId = roleId;
    
 

角色:

    @Entity
    @Table(name = "role")
    public class Role implements model.Entity 
    @Id
    @GeneratedValue
    @Column(name = "roleId", nullable = false)
    private int roleId;

    @Column(name = "user")
    private boolean user;

    @Column(name = "tutor")
    private boolean tutor;

    @Column(name = "admin")
    private boolean admin;

    public Role()  // Empty constructor to have POJO class

    public int getRoleId() 
        return roleId;
    

    public void setRoleId(int roleId) 
        this.roleId = roleId;
    

    public boolean isUser() 
        return user;
    

    public void setUser(boolean user) 
        this.user = user;
    

    public boolean isTutor() 
        return tutor;
    

    public void setTutor(boolean tutor) 
        this.tutor = tutor;
    

    public boolean isAdmin() 
        return admin;
    

    public void setAdmin(boolean admin) 
        this.admin = admin;
    

    @Override
    public String toString() 
        return "Role" +
                "roleId=" + roleId +
                ", user='" + user + '\'' +
                ", tutor=" + tutor + '\'' +
                ", admin=" + admin +
                '';
    

所以主要问题是如何创建实现UserDetailsS​​ervice的UserDetailServiceImpl的实现:

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        ...
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        ...
        return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), grantedAuthorities);
    

也许我应该创建一个特殊的类,它返回用户的确切角色。或者也许还有其他方法?

我不要求为我编写代码,只是帮助我告诉我如何使它更好地实现这种角色。主要目标是划分AdminTutorUser

【问题讨论】:

你的实体模型坏了;它说同一用户可以有多个角色行;例如用户可以在一行中是管理员,而不能在另一行中。 @holmis83 是的,它可以是同一用户的多个角色。为什么说它坏了?或者你说的是“ManyToMany”注解? 是的,ManyToMany 意味着可能存在相互矛盾的行。 【参考方案1】:

鉴于我在某种程度上同意 holmis83 的评论,因为事实上在某些情况下,role 表可能会在某些组合中包含奇怪的(或至少是重复的甚至是矛盾的)信息,您可以通过以下几种方式可以。

首先,我建议你在数据库中创建一个视图来处理role表,这样对authorities-by-username-query更友好

我会这样做:

SELECT roleId, 'ROLE_USER' as role FROM role WHERE user = 1
UNION
SELECT roleId, 'ROLE_TUTOR' as role FROM role WHERE tutor = 1
UNION
SELECT roleId, 'ROLE_ADMIN' as role FROM role WHERE admin = 1;

这样,对于这样的数据库模型

你会得到这样的结果:

现在,您可以使用新创建的视图而不是原始表让您的authorities-by-username-query 从用户创建inner join

SELECT user.login, roles_view.role FROM user as user 
INNER JOIN user_has_role as user_role ON user.userId = user_role.user_userId 
INNER JOIN roles_view ON user_role.role_roleId = roles_view.roleId 

这将是输出:

username  |  role
----------------------
jlumietu  | ROLE_USER
jlumietu  | ROLE_ADMIN
username  | ROLE_USER
username  | ROLE_TUTOR
username  | ROLE_ADMIN
username  | ROLE_ADMIN
username  | ROLE_USER
username  | ROLE_TUTOR
username  | ROLE_ADMIN
username  | ROLE_TUTOR

由于可能有一些重复的信息,你可以使用用户名和角色来创建一个组,这样:

SELECT user.login, roles_view.role FROM user 
INNER JOIN user_has_role as user_role ON user.userId = user_role.user_userId 
INNER JOIN roles_view
ON user_role.role_roleId = roles_view.roleId 
GROUP BY login, role;

只是为了得到这个结果:

username  |  role
----------------------
jlumietu  | ROLE_ADMIN
jlumietu  | ROLE_USER
username  | ROLE_ADMIN
username  | ROLE_TUTOR
username  | ROLE_USER

事实上,没有必要这样做,因为 spring security 会处理它以避免重复角色,但如果手动执行查询,为了可读性,我认为这是值得的。

说了这么多,让我们检查一下安全配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:security="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security.xsd">


    <security:http use-expressions="true" authentication-manager-ref="authenticationManager">

        <security:intercept-url pattern="/simple/**" access="permitAll()" />
        <security:intercept-url pattern="/secured/**" access="isAuthenticated()" />

        <security:form-login 
            login-page="/simple/login.htm"
            authentication-failure-url="/simple/login.htm?error=true"
            default-target-url="/secured/home.htm"
            username-parameter="email" 
            password-parameter="password"
            login-processing-url="/secured/performLogin.htm" />

        <security:logout 
            logout-url="/secured/performLogout.htm"
            logout-success-url="/simple/login.htm" />

        <security:csrf disabled="true" />

    </security:http>

    <security:authentication-manager id="authenticationManager">

        <security:authentication-provider>      
            <security:password-encoder hash="md5" />
            <security:jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
                users-by-username-query="
                    SELECT login AS username, password AS password, '1' AS enabled 
                    FROM user           
                    WHERE user.login=?" 
                authorities-by-username-query="
                    SELECT user.login, roles_view.role 
                    FROM user 
                    INNER JOIN user_has_role as user_role ON user.userId = user_role.user_userId 
                    INNER JOIN roles_view ON user_role.role_roleId = roles_view.roleId 
                    where user.login = ?
                    GROUP BY login, role"
            />          
        </security:authentication-provider>
    </security:authentication-manager>

</beans:beans>

即使您无法在数据库中创建视图,您也可以通过在 authorities-by-username query 中的 role 表周围键入 select-union sql 来使其工作。

请注意,使用此解决方法,您甚至不需要编写自定义的UserDetailsService

【讨论】:

我以另一种方式做到了。我添加了 UserDetailService 检查:isAdmin isTutor isUser。如果是真的,那么 userRole:ROLE_* 现在又是一场战斗。:在 ServletContext 资源 [/WEB-INF/appconfig-data.xml] 中定义名称为 'entityManagerFactory' 的 bean 创建错误:调用 init 方法失败;嵌套异常是 javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory。如果我失败了,我会按照你的方式尝试。【参考方案2】:

我做类似的事情,虽然我的用户只能拥有一个角色。

@Override
@Transactional
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException 
    final User user = Optional.ofNullable(findUserByEmail(username)).orElseThrow(() -> new UsernameNotFoundException("User was not found"));
    List<GrantedAuthority> authorities = getUserAuthority(user.getRole());
    final boolean canLogin = user.isActive() && user.isVerified();
    if (!canLogin) 
        throw new AccountException("User is not enabled");
    
    return buildUserForAuthentication(user, authorities);


private List<GrantedAuthority> getUserAuthority(final Role userRole) 
    return Collections.singletonList(new SimpleGrantedAuthority(userRole.getRole()));


private UserDetails buildUserForAuthentication(final User user, final List<GrantedAuthority> authorities) 
    return new org.springframework.security.core.userdetails.User(
            user.getEmail(),
            user.getPassword(),
            true,
            true,
            true,
            true,
            authorities);

您可以将 getUserAuthority 调整为类似于:

    private List<GrantedAuthority> getUserAuthorities(final Set<Role> roles) 
    return roles.stream().map(roleId -> 
        final Role role = roleRepository.findOne(roleId);
        if (role.isAdmin) 
            return new SimpleGrantedAuthority("ROLE_ADMIN");    
         else if (role.isUser) 
            return new SimpleGrantedAuthority("ROLE_USER");
        
        // like wise for all your roles.
    ).collect(Collectors.toList());

【讨论】:

以上是关于具有特定表角色的spring security UserDetailsS​​ervice的主要内容,如果未能解决你的问题,请参考以下文章

具有 Active Directory 和数据库角色的 Spring Security

Spring Security 配置中的单角色多 IP 地址

Grails Spring Security 查询没有特定角色的用户

具有数据库角色的 Spring Security SAML

Spring Security:检查用户是不是具有分层角色的方法

拒绝Spring Security中具有相同角色的多个用户的访问