Analysis
Analysis

Reputation: 375

Get current user's extra information by thymeleaf sec tag working with spring security

I'm using thymeleaf-extras-springsecurity4 with spring security on my project. The problem is I cannot get user's extra fields (which means user information on database except username, password, enabled, etc. given by UserDetails) by using <span sec:authentication="principal.something" />.

Heres are my simple codes:

UserEntity (implements UserDetails)

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
@Table(name = "users", schema = "myschema")
public class UserEntity implements UserDetails {
  @Id
  @GeneratedValue
  @Column(name = "id", nullable = false)
  private int id;

  @Basic
  @Column(name = "username", nullable = false, unique = true, length = 64)
  private String username;

  @Basic
  @Column(name = "password", nullable = false, columnDefinition = "TEXT")
  private String password;

  @Basic
  @Column(name = "enabled", nullable = false, columnDefinition = "BIT")
  private boolean enabled;

  @Basic
  @Column(name = "phone", nullable = false, length = 16)
  private String phone;

  @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
  private List<AuthorityEntity> authorities;

  @Override
  public boolean isAccountNonExpired() {
    return enabled;
  }

  @Override
  public boolean isAccountNonLocked() {
    return enabled;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return enabled;
  }

  @Override
  public String toString() {
    return username;
  }
}

AuthorityEntity (implements GrantedAuthority)

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "authorities", schema = "myschema",
    uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "authority"}))
public class AuthorityEntity implements GrantedAuthority {
  @Id
  @GeneratedValue
  @Column(name = "id", nullable = false)
  private int id;

  @Basic
  @Column(name = "authority", nullable = false, length = 24)
  private String authority;

  @ManyToOne
  @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false)
  private UserEntity user;
}

UserRepository

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
  UserEntity findOneByUsernameAndEnabledTrue(String username);
}

UserService

@Service
public class UserService {
  private UserRepository userRepository;

  @Autowired
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public UserEntity loadUserByUsername(String username) {
    return userRepository.findOneByUsernameAndEnabledTrue(username);
  }
}

SecurityService (extends UserDetailService)

@Service
public class SecurityService implements UserDetailsService {
  private UserService userService;

  @Autowired
  public SecurityService(UserService userService) {
    this.userService = userService;
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserDetails user = userService.loadUserByUsername(username);
    if (user == null) {
      throw new UsernameNotFoundException(username);
    }
    return user;
  }
}

SecurityConfig (extends WebSecurityConfigurerAdapter)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  private SecurityService securityService;

  @Autowired
  public SecurityConfig(SecurityService securityService) {
    this.securityService = securityService;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
          .antMatchers("/").permitAll()
          .antMatchers("/user/login").anonymous()
          .antMatchers("/**").hasAnyRole("ADMIN", "USER")
          .and()
        .formLogin()
          .loginPage("/user/login")
          .defaultSuccessUrl("/")
          .and()
        .logout()
          .logoutUrl("/user/logout")
          .logoutSuccessUrl("/")
          .and()
        .exceptionHandling()
          .accessDeniedPage("/error/403");
  }

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    auth.userDetailsService(securityService).passwordEncoder(passwordEncoder);
  }
}

index.html (using thymeleaf-extras-springsecurity)

<!DOCTYPE html>
<html lang="ko"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
      layout:decorator="layout/base">

<th:block layout:fragment="content">
    <h1>Main Page</h1>
    <p sec:authentication="principal.username">Username</p>
    <p sec:authentication="principal.phone">Phone</p>
</th:block>

The Problem

In index.html, sec:authentication="principal.username" works as expected, but sec:authentication="principal.phone" does not despite my UserDetailsService implementation stores UserEntry which implements UserDetails with extra field phone.

Questions

(Additional) In many other examples applying spring security, they don't implement UserDetails on UserEntry (or similar classes), but make a new UserDetails instance in their UserDetailService implementation like

@Override
public UserDetails loadUserByUsername(String userName)
        throws UsernameNotFoundException {
    UserInfo activeUserInfo = userInfoDAO.getActiveUser(userName);
    GrantedAuthority authority = new SimpleGrantedAuthority(activeUserInfo.getRole());
    UserDetails userDetails = (UserDetails)new User(activeUserInfo.getUserName(),
            activeUserInfo.getPassword(), Arrays.asList(authority));
    return userDetails;
}

(from here). I think my structure is not a good design but I don't know exactly why. Is there any comment for my class design?

Thanks!

If my questions are too vague, let me know so then I would update this more concrete.

Upvotes: 3

Views: 3692

Answers (1)

Alain Cruz
Alain Cruz

Reputation: 5097

In order to use additional fields contained in your user's data in Thymeleaf, you must go through the next steps.

  1. Implement your own Spring Security's user.
  2. Override loadUserByUsername, so that it returns your custom user.
  3. Add the Spring Security's Thymeleaf Extras dependencies.
  4. Use ${#authentication.getPrincipal()}, instead of sec.

STEP 1

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

// Our own implementation of the Spring Security User.

public class MyUser extends User {

    // Here we add the extra fields of our users.
    private String phone;
    private static final long serialVersionUID = 1L;

    public MyUser(String username,
                      String password,
                      Collection<GrantedAuthority> authorities,
                      String phone) {
        super(username, password, authorities);
        this.phone = phone;
    }

    public String getPhone() {
        return realName;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

}

STEP 2

@Override
public MyUser loadUserByUsername(String userName)
        throws AuthenticationException {

    // Fetch the user.
    UserDetails user = userService.loadUserByUsername(username);

    // For each user's authority, add it into our authorities' collection.
    Collection<GrantedAuthority> grantedAuthorities = new LinkedList<GrantedAuthority>(); 
    if (user.getAuthorities().size() > 0){
        for (Authority authority : user.getAuthorities()) {
            // Add a new GrantedAuthority for each user's authorities.
            grantedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
         }
    }

    return new MyUser(user.getUsername(), user.getPassword(), grantedAuthorities, user.getPhone());
}

STEP 3

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

STEP 4

<th:block th:with="auth=${#authentication.getPrincipal()}">
    <p th:text="${auth ? auth.phone : 'NULL'}">Phone</p>
</th:block>

Upvotes: 2

Related Questions