springweb-applicationsspring-securityuser-rolesuserdetailsservice

spring security UserDetailsService with specific table Role


I'm creating an app where I must use Spring Security login. It's standard login/logout, and I found many tutorials how to create it. What is not standard - is a table role in Database. I can't change Database, I can just use it. I made right entities for user and role, but I can't get the way, how to write correctly UserDetailsServiceImpl with loadUserByUsername. I can't find even a close things...

Entities:

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

Role:

    @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 +
                '}';
    }
}

So the main question is how to create realization of UserDetailServiceImpl which implements UserDetailsService:

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

Maybe I should create special class, which return the exact role of user.. Or maybe there is another ways?

I don't ask to code for me, just help say me how to make it better way of realization of such role. The main goal is to divide Admin, Tutor and User.


Solution

  • Given that I somehow agree with holmis83 comment, as in fact there could be some situations where role table could have strange (or at least, repeated and even contradicting) info in some combinations, there are a couple of ways you could take.

    First of all, I suggest you to create a view in database to handle role table in a way that it would be more authorities-by-username-query friendly

    I would do it a kind this way:

    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;
    

    Just this way, for a database model like this

    enter image description here

    You will get this kind of results:

    enter image description here

    Now, you could make your authorities-by-username-query making an inner join from user with the newly created view instead of the original table.

    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 
    

    This would be the output:

    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
    

    As there could be some repeated info, you could make just a group by using username and role, just this way:

    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;
    

    Just to get this results:

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

    In fact, it is not necessary to do this since spring security would handle it to avoid having repeated roles, but for purposes of readability if the query is manually executed I think it is well worth.

    Once said this all, let's check the security config:

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

    Even if you cannot create a view in database, you could manage to get it work just typing the select-union sql around your role table in the authorities-by-username query.

    Note that with this workaround you do not need even to write a customized UserDetailsService