Display user details in jsp using spring security - spring

I have implemented an app in spring boot with spring security. I need to display User's firstname, lastname and the image path in jsp page (which is used for header, it means globally available even if we navigate to another URL) after successfully logged in. I'm using remember-me using PersistentLogin. So I can't use session to store details. Because If I close the browser, session will be destroyed.
I have implemented CustomUserDetailsService, it returns org.springframework.security.core.userdetails.User
#Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService{
//codes
return new org.springframework.security.core.userdetails.User(
username,
password,
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
authorities);
}
I know there are two limitaions
If I don't use remember-me, I can easily store within session.
If I return User model class in CustomUserDetailsService ...,I can easily get user details in jsp
page using<security:authentication property="principal.firstName">
tag in jsp. But I need to return org.springframework.security.core.userdetails.User
Unfortunately I need both limitation. My User model class has firstName, lastName, imagePath,.. etc.
How can I display user details in jsp page? Any approaches available? Thanks in advance.

Spring inbuilt provides the solution to do the same.
Java code :
public User getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
Object principal = auth.getPrincipal();
if (principal instanceof User) {
return ((User) principal);
}
}
}
JSP code :
${pageContext["request"].userPrincipal.principal}

HWat i have done is, I created a prototype of User called UserAuth
public class UserAuth extends org.springframework.security.core.userdetails.User{
private String firstName;
private String lastName;
private String imagePath;
public UserAuth(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities,
String firstName, String lastName, String imagePath) {
super(username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities);
this.firstName = firstName;
this.lastName = lastName;
this.imagePath = imagePath;
}
//getters and setters
}
In CustomeUserDetailsService
#Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService{
//codes
return UserAuth
username,
password,
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
authorities,
user.getFirstName(),
user.getLastName(),
user.getImagePath());
}

Related

Extends Spring security user class

I'm Working on a Spring security project . I try to extends the security.core.userdetails.User class to add more details while registering the users.
User Extended class
public class UserDetails extends User {
private int id;
private String Country;
public UserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, int id,
String country) {
super(username, password, authorities);
this.id = id;
Country = country;
}
public UserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities,
int id, String country) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
Country = country;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCountry() {
return Country;
}
public void setCountry(String country) {
Country = country;
}
I have also added Id and country in my entity class(model class).
But when i try to register the user .
It give an error.org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into users (username, password, enabled) values (?,?,?)]; Field 'id' doesn't have a default value; nested exception is java.sql.SQLException: Field 'id' doesn't have a default value
(The value of id and country is hard coded)
Controller class
try {
List<GrantedAuthority> authority = new ArrayList<>();
authority.add(new SimpleGrantedAuthority(form.getRole()));
String encodedPassword = passwordEncoder.encode(form.getPassword());
UserDetails details = new UserDetails(form.getUsername(), encodedPassword, authority, 10 ,"India");
System.out.println(details.getId()+" "+details.getCountry() +" "+details.getUsername());
System.out.println(details);
detailsManager.createUser(details);
}
OUPUT
10 India alpha#gmail.com
com.example.demo.model.UserDetails [Username=alpha#gmail.com, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]]
I don't know why its is calling the parent class constructor.
The SQL is incorrect. Spring Security's INSERT by default populates the username, password, and enabled columns. However, the users table you created requires an id column as well. Since the query doesn't specify the value, it fails.
You could try extending JdbcUserDetailsManager's various methods to be aware of your id field as well. You'd need to at least extend createUser so it adds the id to the INSERT statement and findUserByUsername so it constructs your custom object.
A better way, though, would be to use Spring Data. This allows your domain object to be independent of Spring Security. Also, Spring Data has much broader SQL support.
It might be helpful to call your class something different than a Spring Security interface. So, let's imagine that your custom class (the one with the id) is called YourUser (instead of UserDetails). Now, you can wire a Spring Data-based UserDetailsService to Spring Security like so:
#Service
public class YourUserRepositoryUserDetailsService implements UserDetailsService {
private final YourUserRepository users; // your Spring Data repository
// ... constructor
#Override
public UserDetails findUserByUsername(String username) {
YourUser user = this.users.findByUsername(username)
.orElseThrow(() -> new UserNotFoundException("not found"));
return new UserDetailsAdapter(user);
}
private static class UserDetailsAdapter extends YourUser implements UserDetails {
UserDetailsAdapter(YourUser user) {
super(user); // copy constructor
}
// ... implement UserDetails methods
}
}
This UserDetailsService replaces the JdbcUserDetailsManager that you are publishing.

How to modify spring SecurityContextHolder.getContext().getAuthentication() object after successful login?

I'm working on spring boot application where i've created a CustomUserDetails class by extending UserDetails as follows..
public class CustomUserDetails
extends org.springframework.security.core.userdetails.User {
private static final long serialVersionUID = 1L;
/**
* The extra field in the login form is for the tenant name
*/
private String tenant;
private Long userId;
private String firstName;
private String middleName;
private String lastName;
private String email;
private String role;
i need to modify tenant details in UserDetails object. For this i've checked following
How to update Spring Security UserDetails impls after successful login?
https://stackanswers.net/questions/how-to-immediately-enable-the-authority-after-update-user-authority-in-spring-security
https://dev.to/onlineinterview/user-account-loginregistration-feature-with-spring-boot--3fc3
And Controller is here where i'm updating authentication object:
#PreAuthorize("hasRole('SUPER_ADMIN')")
#GetMapping(path = "/useTenant/{tenantId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<ResponseDTO> useTenant(#PathVariable Long tenantId) {
HttpStatus status = HttpStatus.OK;
boolean error = false;
String message = languageMessageService.getMessage(MultiLanguageKey.SUCCESS);
// fetch master tenant by id
Optional<MasterTenant> optional = masterTenantService.findById(tenantId);
if (optional.isPresent()) {
CustomUserDetails customUserDetails = customUserDetailsService.getUserDetail();
//Changing Tenant ID
customUserDetails.setTenant(optional.get().getTenantId());
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Update Current user by changing tenant id in SecurityContextHolder
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
auth.setDetails(customUserDetails);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} else {
error = false;
message = languageMessageService.getMessage(MultiLanguageKey.TENANT_NOT_FOUND);
}
return new ResponseEntity<>(new ResponseDTO(error, message), status);
}
My problem is that when i'm hitting another request to perform particular action, i didn't find tenant detail in CustomUserDetails object which is fetched from
SecurityContextHolder.getContext().getAuthentication()
Please let me know how can i update or modify UserDetails object of Authentication and save back so another request get updated CustomUserDetails.
The UserDetails should be set to the Principal of the UsernamePasswordAuthenticationToken rather than Details as suggested by the java docs :
The AuthenticationManager implementation will often return an
Authentication containing richer information as the principal for use
by the application. Many of the authentication providers will create a
UserDetails object as the principal.
Details in UsernamePasswordAuthenticationToken is normally stored user 's IP address or certificate serial number etc.
So change it to :
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
// Update Current user by changing tenant id in SecurityContextHolder
UsernamePasswordAuthenticationToken currentAuth = (UsernamePasswordAuthenticationToken) authentication;
CustomUserDetails userDetail = currentAuth.getPrincipal();
customUserDetails.updateTenanet("blablalb");
UsernamePasswordAuthenticationToken updateAuth = new UsernamePasswordAuthenticationToken(userDetail ,
currentAuth.getCredentials(),
currentAuth.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(updateAuth);
}

Update User's first name and last name in principal

I am updating user's information like first name and last name and I am getting first name and last name in all the pages for welcome message.
I have two controllers one for ajax request mapping and the other for normal request mapping.
Normal request mapping controller have this method. In this controller all page navigation is present and some request mapping which are not ajax calls
private String getPrincipalDisplay() {
GreenBusUser user = null;
String userName = "";
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
user = (GreenBusUser) principal;
userName = user.getFirstName() + " " + user.getLastName();
} else {
userName = "";
}
return userName;
}
This is how I am getting the username on every page by return string of this function I am adding it in ModelMap object.
When I update user's information I am doing in ajax request mapping.
#RequestMapping(value = "/restify/updateUserData", method = RequestMethod.PUT, headers = "Accept=application/json")
public ServiceResponse forgotPassword(#RequestBody Object user)
{
//logger.debug("getting response");
return setDataPut("http://localhost:7020/forgotPassword",user);
}
user is an Object type which has json data. Now how do I retrieve data from object and update my first name and last name in principal.
This is my GreenBusUser class
public class GreenBusUser implements UserDetails
{
private static final long serialVersionUID = 1L;
private String username;
private String password;
private Collection<? extends GrantedAuthority> grantedAuthorities;
private String firstName;
private String lastName;
public GreenBusUser(String username,String password,Collection<? extends GrantedAuthority> authorities,String firstName, String lastName)
{
this.username = username;
this.password = password;
this.grantedAuthorities = authorities;
this.firstName=firstName;
this.lastName=lastName;
this.grantedAuthorities.stream().forEach(System.out::println);
}
public Collection<? extends GrantedAuthority> getAuthorities()
{
return grantedAuthorities;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getPassword()
{
return password;
}
public String getUsername()
{
return username;
}
public boolean isAccountNonExpired()
{
return true;
}
public boolean isAccountNonLocked()
{
return true;
}
public boolean isCredentialsNonExpired()
{
return true;
}
public boolean isEnabled()
{
return true;
}
}
UPDATE:::::
I have updated your code and applied some part of your answer into mine but still I ran into a problem
#RequestMapping(value="/updateUser",method=RequestMethod.GET)
public String updateUser(ModelMap model) {
UserInfo user = getUserObject();
GreenBusUser newGreenBususer = null;
List<User> list = new ArrayList<User>();
list = FetchDataService.fetchDataUser("http://localhost:8060/GetuserbyUserName?username=" + getPrincipal(), user.getUsername(), user.getPassword());
logger.debug("new user list ----->>>"+list.size());
User newuser=(User)list.get(0);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(), SecurityContextHolder.getContext().getAuthentication().getCredentials());
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
newGreenBususer=(GreenBusUser)principal;
logger.debug("newGreenBususerDetails---->>>"+newGreenBususer.toString());
newGreenBususer.setFirstName(newuser.getFirstName());
newGreenBususer.setLastName(newuser.getLastName());
if(newGreenBususer.getFirstName()!=null) {
logger.debug("got my first name");
}
if(newGreenBususer.getLastName()!=null) {
logger.debug("got my last name");
}
auth.setDetails(newGreenBususer);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
model.addAttribute("user", getPrincipalDisplay());
model.addAttribute("userData", list);
model.addAttribute("check", true);
return "GreenBus_updateProfile_User";
}
At first it sets the firstname and lastname to GreenBusUser and then there is setDetails method when I reload the page it says No user found when I am calling getUserObject() method at the top of this method.
private X2CUser getUserObject() {
X2CUser userName = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
userName = ((X2CUser) principal);
} else {
logger.info("No user found");
}
return userName;
}
If you are updating the password, then it will be good to logout the user and tell him to relogin.
Try this code .. It might help you.
UsernamePasswordAuthenticationToken authReq = new UsernamePasswordAuthenticationToken(user, pass);
Authentication auth = authManager.authenticate(authReq);
SecurityContext sc = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
I have finally resolved my problem though I have later added some code in my question part in UPDATE section.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
newGreenBususer=(GreenBusUser)principal;
newGreenBususer.setFirstName(newuser.getFirstName());
newGreenBususer.setLastName(newuser.getLastName());
Yes that's all need to be done.
This part--->>
auth.setDetails(newGreenBususer);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
set new context making security pointing to null when I reload still not clear because I am setting the details before reload so its like I get new context but I have set the new user details.
Though I have finally resolved my problem but if anyone could shed some light why it was happening then I will accept his/her answer.
Thanks alot for your support. Keep Learning!

How to use additional field in user details vs #PostAuthorize in Spring Security

I what to add new field "tenant" to user details to use it in #PostAuthorize.
On #PostAuthorize("returnObject == principal.tenant") i receive error:
SEVERE: Servlet.service() for servlet [appServlet] in context with path [/sectst] threw exception [Request processing failed; nested exception is java.lang.IllegalArgumentException: Failed to evaluate expression 'returnObject == principal.tenant'] with root cause
org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 26): Field or property 'tenant' cannot be found on object of type 'org.springframework.security.core.userdetails.User'
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:216)
Can't understand why it looks through default User class instead of my custom ExtendedUser
My custom User Class
public class ExtendedUser extends User {
private static final long serialVersionUID = 3149421282945554897L;
private final String tenant;
public ExtendedUser(String username, String password,
Collection<? extends GrantedAuthority> authorities, String tenant) {
super(username, password, authorities);
this.tenant = tenant;
}
public ExtendedUser(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities, String tenant) {
super(username, password, enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities);
this.tenant = tenant;
}
public String getTenant() {
return tenant;
}
}
And Custom UserDetails
public class ExtendedJdbcUserDetailsService extends JdbcDaoImpl {
private String extendedUsersByUsernameQuery;
public String getExtendedUsersByUsernameQuery() {
return extendedUsersByUsernameQuery;
}
public void setExtendedUsersByUsernameQuery(String extendedUsersByUsernameQuery) {
this.extendedUsersByUsernameQuery = extendedUsersByUsernameQuery;
}
#Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(extendedUsersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
String username = rs.getString(1);
String password = rs.getString(2);
String tenant = rs.getString(3);
boolean enabled = rs.getBoolean(4);
return new ExtendedUser(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES, tenant);
}
});
}
}
Edit:
I have overrided createUserDetails method and it solved the problem
#Override
protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery,
List<GrantedAuthority> combinedAuthorities) {
String returnUsername = userFromUserQuery.getUsername();
return new ExtendedUser(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(),
true, true, true, combinedAuthorities, ((ExtendedUser) userFromUserQuery).getTenant());
}

Spring Security - Random salt + Hash + Custom UserDetailsService

Last question on this from me hopefully. So far I've implemented my own custom UserDetails and UserDetailsService classes so that I can pass the random salt that was used at the time of password creation. Hash of password is SHA512. However upon trying to login I always get user/pw combination incorrect and I can't seem to figure out why.
I store the hash and salt in the db as blobs, any ideas on where the issue lies?
Security-applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<sec:http auto-config='true' access-denied-page="/access-denied.html">
<!-- NO RESTRICTIONS -->
<sec:intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<sec:intercept-url pattern="/*.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<!-- RESTRICTED PAGES -->
<sec:intercept-url pattern="/admin/*.html" access="ROLE_ADMIN" />
<sec:intercept-url pattern="/athlete/*.html" access="ROLE_ADMIN, ROLE_STAFF" />
<sec:form-login login-page="/login.html"
login-processing-url="/loginProcess"
authentication-failure-url="/login.html?login_error=1"
default-target-url="/member" />
<sec:logout logout-success-url="/login.html"/>
</sec:http>
<beans:bean id="customUserDetailsService" class="PATH.TO.CustomUserDetailsService"/>
<beans:bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
<beans:constructor-arg value="512"/>
</beans:bean>
<sec:authentication-manager>
<sec:authentication-provider user-service-ref="customUserDetailsService">
<sec:password-encoder ref="passwordEncoder">
<sec:salt-source user-property="salt"/>
</sec:password-encoder>
</sec:authentication-provider>
</sec:authentication-manager>
</beans:beans>
CustomUserDetails.java
public class CustomUserDetails implements UserDetails {
private int userID;
private String username;
private String password;
private Collection<GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private String salt;
public CustomUserDetails() {
}
public CustomUserDetails(int userID, Collection<GrantedAuthority> authorities, String username, String password, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled, String salt) {
this.userID = userID;
this.authorities = authorities;
this.username = username;
this.password = password;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
this.salt = salt;
}
#Override
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
#Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
#Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
#Override
public boolean isEnabled() {
return enabled;
}
public String getSalt() {
return salt;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setAuthorities(Collection<GrantedAuthority> authorities) {
this.authorities = authorities;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
public void setSalt(String salt) {
this.salt = salt;
}
}
CustomUserDetailsService.java
public class CustomUserDetailsService implements UserDetailsService {
private User_dao userDao;
#Autowired
public void setUserDao(User_dao userDao) {
this.userDao = userDao;
}
#Override
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
MyUser myUser = new MyUser();
myUser.setUsername(username);
try {
userDao.getUserByUsername(myUser);
} catch (Throwable e) {
}
if (myUser == null) {
throw new UsernameNotFoundException("Username not found", username);
} else {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
authList.add(new GrantedAuthorityImpl(myUser.getUserRole().getAuthority()));
int userID = myUser.getUserID();
boolean accountNonExpired = true;
boolean accountNonLocked = myUser.isNonLocked();
boolean credentialsNonExpired = true;
boolean enabled = myUser.isEnabled();
String password = "";
String salt = "";
password = new String(myUser.getHash);
salt = new String(myUser.getSalt());
CustomUserDetails user = new CustomUserDetails(userID, authList, username, password, accountNonExpired, accountNonLocked, credentialsNonExpired, enabled, salt);
return user;
}
}
}
Password Creation
public byte[] generateSalt() throws NoSuchAlgorithmException {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[20];
random.nextBytes(salt);
return salt;
}
public byte[] generateHash(byte[] salt, String pass) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
digest.update(salt);
byte[] hash = digest.digest(pass.getBytes());
return hash;
}
Call in method:
byte[] salt = generateSalt();
byte[] hash = generateHash(salt, password);
Which I then store in the db.
I had the same original problem, which was never answered, so in the hopes that this might save someone time in the future:
Spring-Security adds braces by default prior to comparing digests. I missed that and spun my wheels for hours (d'oh).
Make sure you store (or generate) your salt values enclosed with curly braces (i.e., when Spring says '{salt}' they sincerely mean 'open curly brace + your salt value + close curly brace'.
I suppose this was obvious to most people, but I didn't notice it until I finally debugged down into it.
It's worth pointing out, I think, that storing the salt used for each user's password in a salt column in the database (although common) present a vulnerability. The reason for salting in the first place is to prevent dictionary attacks against a compromised database. If an attacker had access to your database and no salt were used, they could apply common hash algorithms to every word in a standard dictionary to create new dictionaries of hashes. When they find a match for one of those words in the database, they consult the mappings in their own dictionary to find the original unhashed word that produces that hash when the algorithm is applied. And voila! The attacker has a password.
Now... if you apply a salt and the salt is different for each user, you throw a massive monkey wrench into that attack plan. BUT... if you STORE the salt for each user in the database (and make it obvious by naming the column "salt") you're not actually interfering all that much with this attack plan.
The most secure approach I know of is this.
Have no salt column in your user table.
Implement a getSalt() method on your UserDetails class. Have it return some other user attribute that was set when the user registered and will NEVER change. e.g. join date. Concatenate that with a string literal/constant that's hard-coded in your UserDetails class.
In this manner, salts will be unique to every user. What value was used as a salt will not be evident from looking at the database. And even if an attacker guessed what was used for part of the salt, he/she would ALSO need access to your source code to know the REST of the salt. This means your database AND application code would BOTH need to be compromised before you end up with a real issue on your hands.
Assuming you understand the merits of everything I just said, there's also a considerable advantage here in that what I just said is actually easier to implement than what you're already doing. Love it when the right thing turns out to be the easier thing!

Resources