Question

I have a design issue that I don't know how to solve. I'm using Spring 3.2.4 and Spring security 3.1.4.

I have a Account table in my database that looks like this:

create table Account (id identity,
                        username varchar unique,
                        password varchar not null,
                        firstName varchar not null, 
                        lastName varchar not null,
                        university varchar not null,
                        primary key (id));

Until recently my username was just only a username but I changed it to be the email address instead since many users want to login with that instead.

I have a header that I include on all my pages which got a link to the users profile like this:

<a href="/project/users/<%= request.getUserPrincipal().getName()%>" class="navbar-link"><strong><%= request.getUserPrincipal().getName()%></strong></a>

The problem is that <%= request.getUserPrincipal().getName()%> returns the email now, I don't want to link the user's with thier emails. Instead I want to use the id every user have to link to the profile.

How do I reach the users id's from every page?

I have been thinking of two solutions but I'm not sure:

  1. Change the principal to contain the id as well, don't know how to do this and having problem finding good information on the topic.
  2. Add a model attribute to all my controllers that contain the whole user but this would be really ugly, like this.

Account account = entityManager.find(Account.class, email);
model.addAttribute("account", account);

There are more way's as well and I have no clue which one is to prefer.

I hope it's clear enough and thank you for any help on this.

====== Edit according to answer =======

I edited Account to implement UserDetails, it now looks like this (will fix the auto generated stuff later):

@Entity
@Table(name="Account")
public class Account implements UserDetails {

    @Id
    private int id;

    private String username;

    private String password;

    private String firstName;

    private String lastName;

    @ManyToOne
    private University university;

    public Account() {

    }

    public Account(String username, String password, String firstName, String lastName, University university) {
        this.username = username;
        this.password = password;
        this.firstName = firstName;
        this.lastName = lastName;
        this.university = university;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setUsername(String username) {
        this.username = username;
    }

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

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

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

    public University getUniversity() {
        return university;
    }

    public void setUniversity(University university) {
        this.university = university;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isEnabled() {
        // TODO Auto-generated method stub
        return true;
    }
}

I also added

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

To my jsp files and trying to reach the id by

<sec:authentication property="principal.id" />

This gives me the following

org.springframework.beans.NotReadablePropertyException: Invalid property 'principal.id' of bean class [org.springframework.security.authentication.UsernamePasswordAuthenticationToken]: Bean property 'principal.id' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

====== Edit 2 according to answer =======

I based my application on spring social samples and I never had to change anything until now.

This are the files I think are relevant, please tell me if theres something you need to see besides this.

AccountRepository.java

public interface AccountRepository {

    void createAccount(Account account) throws UsernameAlreadyInUseException;

    Account findAccountByUsername(String username);

}

JdbcAccountRepository.java

@Repository
public class JdbcAccountRepository implements AccountRepository {

    private final JdbcTemplate jdbcTemplate;

    private final PasswordEncoder passwordEncoder;

    @Inject
    public JdbcAccountRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
    }

    @Transactional
    public void createAccount(Account user) throws UsernameAlreadyInUseException {
        try {
            jdbcTemplate.update(
                    "insert into Account (firstName, lastName, username, university, password) values (?, ?, ?, ?, ?)",
                    user.getFirstName(), user.getLastName(), user.getUsername(), user.getUniversity(),
                    passwordEncoder.encode(user.getPassword()));
        } catch (DuplicateKeyException e) {
            throw new UsernameAlreadyInUseException(user.getUsername());
        }
    }

    public Account findAccountByUsername(String username) {
        return jdbcTemplate.queryForObject("select username, firstName, lastName, university from Account where username = ?",
                new RowMapper<Account>() {
                    public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
                        return new Account(rs.getString("username"), null, rs.getString("firstName"), rs.getString("lastName"), new University("test"));
                    }
                }, username);
    }

}

security.xml

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

    <http pattern="/resources/**" security="none" />
    <http pattern="/project/" security="none" />

    <http use-expressions="true">
        <!-- Authentication policy -->
        <form-login login-page="/signin" login-processing-url="/signin/authenticate" authentication-failure-url="/signin?error=bad_credentials" />
        <logout logout-url="/signout" delete-cookies="JSESSIONID" />
        <intercept-url pattern="/addcourse" access="isAuthenticated()" />
        <intercept-url pattern="/courses/**/**/edit" access="isAuthenticated()" />
        <intercept-url pattern="/users/**/edit" access="isAuthenticated()" />
    </http>

    <authentication-manager alias="authenticationManager">
        <authentication-provider>
            <password-encoder ref="passwordEncoder" />
            <jdbc-user-service data-source-ref="dataSource" 
                            users-by-username-query="select username, password, true from Account where username = ?"
                            authorities-by-username-query="select username, 'ROLE_USER' from Account where username = ?"/>
        </authentication-provider>
        <authentication-provider>
            <user-service>
                <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

And this is my try of implementing a UserDetailsService

public class RepositoryUserDetailsService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Autowired
    public RepositoryUserDetailsService(AccountRepository repository) {
        this.accountRepository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account user = accountRepository.findAccountByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException("No user found with username: " + username);
        }

        return user;

    }
}

Still gives me the same error, do I need to add the UserDetailsService somewhere? This is starting to be something else compared to my initial question, I should maybe start another question.

Sorry for my lack of experience in this. I have to read up.

Was it helpful?

Solution

If you are using Spring Security, it may be beneficial to ditch the scriptlets and instead use the Spring Security custom taglibs:

http://docs.spring.io/spring-security/site/docs/3.1.4.RELEASE/reference/taglibs.html

As long as you have an id attribute with a getId() method on your Account model, and your Account model is implementing Spring Security's UserDetails interface (as it should be,) then you should be able to access the id attribute using the following tag:

<security:authentication property="principal.id" />

You could use it inline or assign the value to another variable:

<security:authentication property="principal.id" var="accountId" />
...
${accountId}

This will work for any property on your Account object (as long as the user is authenticated, obviously.)

EDIT AFTER QUESTION UPDATED:

Make sure your implementation of UserDetailsService is returning an instance of Account. It sounds like you are not actually using the Account type as your principal.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        
    Account account = ... // fetch your Account
    return account;
}

EDIT #2 AFTER QUESTION UPDATED

Ok the problem is here:

<jdbc-user-service data-source-ref="dataSource" 
    users-by-username-query="select username, password, true from Account where username = ?"
    authorities-by-username-query="select username, 'ROLE_USER' from Account where username = ?"/>

You are using a "shortcut" so-to-speak to fetch a default implementation of the UserDetails interface as provided by Spring Security, specifically org.springframework.security.core.userdetails.User. However, you want your principal to be of type Account. This won't automagically happen for you. Instead of using the jdbc-user-service element, you should provide your own implementation of UserDetailsService. Something like this:

<authentication-manager ...>
    <authentication-provider user-service-ref="myUserDetailsServiceImpl"/>
</authentication-manager>

...with a UserDetailsService implementation like:

@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
    ...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = ...// load your Account
        return account;
    }
}

That should get you headed in the right direction.

OTHER TIPS

I don't know anything about spring but depending on how they've extended the identity principal there may be other fields like a user ID you could grab. You may also be able to configure the security in spring and add your own item(s) into the identity collection. If that's not doable, then consider an actionfilter that looks up the user ID based on the email address (in some repository) and populates your model/view bag. Adorn each controller or action that needs it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top