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
.
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
You will get this kind of results:
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