As of Spring Security doc: 34.1 @EnableWebMvcSecurity states, the @EnableWebMvcSecurity
was replaced by @EnableWebSecurity
.
But when I try to get the UserDetails
in controller by the @AuthenticationPrincipal
, I got a empty object: the username is ""
. I also tried the @EnableWebMvcSecurity
, but unfortunately the UserDetails
is null
.
But I can get the UserDetails
by the traditional way, like this:
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
My question is, what the correct way to get my custom UserDetails
(Account
) when I use @EnableWebSecurity
?
Below are the related source code:
Controller:
@RequestMapping(method = RequestMethod.POST)
@Secured("ROLE_USER")
public String postRoom(@Valid @ModelAttribute Room room, BindingResult result, Model model, @AuthenticationPrincipal Account principal) {
if (result.hasErrors()) {
return "room_form";
}
Account account = accountRepository.findByUsername(principal.getUsername());
room.setAccountId(account.getId());
room.setLastModified(new Date());
roomRepository.save(room);
return "room_list";
}
Security configuration:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties security;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll()
.and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
.and().logout().permitAll()
.and().rememberMe()
.and().csrf().disable();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(this.dataSource).passwordEncoder(new BCryptPasswordEncoder(8));
}
}
And the Account.java
:
@Entity
@Table(name = "users")
public class Account implements Serializable {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
private boolean enabled;
@Lob
private byte[] avatar;
// getter / setter ...
}
As mentioned in one of the comments on this post, the Account class you're returning needs to be assignable from authentication.getPrincipal()
which means your Account class most likely needs to implement the UserDetails
interface (as a minimum) since the org.springframework.security.core.userdetails.User
class does. Look at Java's API doc for UserDetails
or this page for a feel.
If you change the type after @AuthenticationPrincipal to User
, the null problem will probably go away.
Depending on how you setup your database, having the Account class implement UserDetails
might introduce too much logic into Account since it's supposed to be a model.
Since you're using @EnableWebSecurity
, you don't need to configure a custom HandlerMethodArgumentResolver
as some posts suggest.
Note I'm using Spring Boot
Since using jdbcAuthentication requires you to setup your database in a pre-defined way (and most likely the schema you had in mind is nothing like the one they created), your best bet is to create a custom schema and configure your own user authentication service (it's not hard). Personally, I configured a custom user authentication service in the configureGlobal...
method. One liner.
auth.userDetailsService(userService).passwordEncoder...
where my custom userService implements the UserDetailsService
interface which only provides one method for you to implement public UserDetails loadUserByUsername(String username)
Spring Security will call that method to authenticate users. Within that loadUserByUsername
method, I retrieved my user info from the database, created a new org.springframework.security.core.userdetails.User
object, stuffed username, password, and permissions into the User object and returned it.
I have the following at the end of my method.
return new User(user.getUsername(), user.getPassword(), permissions);
Because I returned User
which implements UserDetails
interface, @AuthenticationPrincipal User user
will work for me.
DO NOTE that permissions
variable needs to be a class which implements the GrantedAuthority
interface, or a collection of that type. That interface also has only one method for you to implement public String getAuthority()
, which basically can return any string you like (presumably the names of permission / roles in your database).
I'll assume you know how Spring Security deals with authorization. It's saddening that Spring Security uses solely Role-based authorization (if I'm wrong I'd be happy to be corrected), instead of group + permission for a more granular control. You can, however, work around that by creating a groups
table and a rights
table in your database and having a corresponding classes implement GrantedAuthority interface. You'll effectively be separating "roles" into those two categories.
If you are really keen on using your own class via @AuthenticationPrincipal
, or if you want more than username and password
I would probably create a wrapper class around your Account class (to take logic out of Account) and have the wrapper class implement UserDetails
I've done this successfully.
Lastly, I would highly recommend adding a service layer on top of your repository layer, and get your controllers to talk to the service layer. This setup adds a layer of security (since hackers will have to hack your service layer as well before reaching your repository layer) and also takes the logic out the repository layer since it should only be used for CRUD, nothing more.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With