spring 具有特定 table 角色的安全 UserDetailsS​​ervice

spring security UserDetailsService with specific table Role

我正在创建一个必须使用 Spring 安全登录 的应用程序。它是标准的 login/logout,我找到了很多如何创建它的教程。不标准的是数据库中的 table 角色。我无法更改数据库,只能使用它。我为用户和角色创建了正确的实体,但我无法理解如何正确编写 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);
    }

也许我应该创建特殊的 class,其中 return 用户的确切角色。或者也许还有其他方法?

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

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

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

考虑到我以某种方式同意 holmis83 的评论,因为实际上在某些情况下 role table 可能在某些组合中有奇怪的(或者至少,重复甚至矛盾的)信息, 您可以采用多种方法。

首先,我建议您在数据库中创建一个视图来处理 role table,这样会更 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 使用新创建的视图而不是原始 table 从用户创建 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 安全会处理它以避免重复角色,但为了可读性,如果手动执行查询,我认为这是非常值得的。

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

<?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>

即使您无法在数据库中创建视图,您也可以设法让它工作,只需在 role table 周围键入 select-union sql authorities-by-username query.

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