Spring Boot Security Application

Spring Security had some opinions of being complicated to use. Well, of course it's quite complicated when you look at it, as its scope covers a lot of use-cases. Thing is that, truly in a Spring spirit, you don't have to use every feature there is at once for the use-case you are having. In fact, when you start cherry-picking and back it with Spring Boot, it doesn't appear so complicated anymore.

Let's start with the use case, I was thinking on something relatively common, something that appears in almost every project with some basic access restrictions. So, the requirements of such an app could be:

  • The app will have users, each with role Admin or User
  • They log in by their emails and passwords
  • Non-admin users can view their info, but cannot peek at other users
  • Admin users can list and view all the users, and create new ones as well
  • Customized form for login
  • "Remember me" authentication for lazies
  • Possibility to logout
  • Home page will be available for everyone, authenticated or not

The source code for such application is on GitHub for you to check out. It's advised that you have the source code open somewhere, the below will be a commentary of some critical points. Ah, and the Security-related stuff comes near the end, so just scroll down if you know the basics.

Dependencies

Besides, the standard Spring Boot dependency, the most important dependencies are a starter module for Spring Security, Spring Data JPA - as we need somewhere to store the users, and an embedded, in-memory HSQLDB as a storage engine.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>

In real life, you will probably be more interested in connecting to some sort of external database engine, as for example described in this article on database connection pooling with BoneCP. I also use Freemarker as a template engine, but if that's not your thing, then rewriting it for JSP should also be easy enough, as in this article about Spring MVC application.

Domain Model

So we will have User entity, like this:

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, updatable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password_hash", nullable = false)
    private String passwordHash;

    @Column(name = "role", nullable = false)
    @Enumerated(EnumType.STRING)
    private Role role;

    // getters, setters

}

As you see only the hash of the password will be stored in a database, which is usually a good idea. There is also an unique constraint on the email field, but it's not a primary key. The user is identified by id for a reason that e-mail addresses is quite a sensitive information you don't want to appear in access logs, so we'll stick to using id whenever we can.

Role is a simple enumeration:

public enum Role {
    USER, ADMIN
}

Besides that, a form for creating a new user will be nice to have:

public class UserCreateForm {

    @NotEmpty
    private String email = "";

    @NotEmpty
    private String password = "";

    @NotEmpty
    private String passwordRepeated = "";

    @NotNull
    private Role role = Role.USER;

}

This will function as a data transfer object (DTO) between the web layer and service layer. It's annotated by Hibernate Validator validation constraints and sets some sane defaults. Notice that it's slightly different than User object, therefore as much as I'd wish to "leak" the User entity into the web layer I really cannot.

That's all we need for now.

Service Layer

In service layer, where the business logic should, we'd need something to retrieve the User by his id, email, list all the users and create a new one.

So the interface will be:

public interface UserService {

    Optional<User> getUserById(long id);

    Optional<User> getUserByEmail(String email);

    Collection<User> getAllUsers();

    User create(UserCreateForm form);

}

The implementation of the service:

@Service
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

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

    @Override
    public Optional<User> getUserById(long id) {
        return Optional.ofNullable(userRepository.findOne(id));
    }

    @Override
    public Optional<User> getUserByEmail(String email) {
        return userRepository.findOneByEmail(email);
    }

    @Override
    public Collection<User> getAllUsers() {
        return userRepository.findAll(new Sort("email"));
    }

    @Override
    public User create(UserCreateForm form) {
        User user = new User();
        user.setEmail(form.getEmail());
        user.setPasswordHash(new BCryptPasswordEncoder().encode(form.getPassword()));
        user.setRole(form.getRole());
        return userRepository.save(user);
    }

}

Not much worth a comment here - the service proxies to the UserRepository most of the time. Worth noticing is that in thecreate() method, the form is used to build a new User object. The hash is generated from the password usingBCryptPasswordEncoder, which is supposed to generate better hashes than infamous MD5.

The UserRepository is defined as follows:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findOneByEmail(String email);
}

Only one non-default method findOneByEmail is added here. Notice that I want it to return a User wrapped in JDK8Optional, which is somewhat a new feature of Spring, and makes handling null values easier.

Web Layer

This are the controllers and their views. To satisfy the requirements of the application we need a couple at least.

Home page

We will handle the root / of the website in HomeController, which just returns a home view:

@Controller
public class HomeController {

    @RequestMapping("/")
    public String getHomePage() {
        return "home";
    }

}

Listing of users

Similarly, the listing of users will be mapped to /users, and handled by UsersController. It has UserService injected, asks it to return a Collection of User objects, puts them into the users model property, and then calls users view name:

@Controller
public class UsersController {

    private final UserService userService;

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

    @RequestMapping("/users")
    public ModelAndView getUsersPage() {
        return new ModelAndView("users", "users", userService.getAllUsers());
    }

}

Viewing and creating a user

Next, we need a Controller handling viewing and creating a new user, I called it UserController, and it is more complicated:

@Controller
public class UserController {

    private final UserService userService;
    private final UserCreateFormValidator userCreateFormValidator;

    @Autowired
    public UserController(UserService userService, UserCreateFormValidator userCreateFormValidator) {
        this.userService = userService;
        this.userCreateFormValidator = userCreateFormValidator;
    }

    @InitBinder("form")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(userCreateFormValidator);
    }

    @RequestMapping("/user/{id}")
    public ModelAndView getUserPage(@PathVariable Long id) {
        return new ModelAndView("user", "user", userService.getUserById(id)
                .orElseThrow(() -> new NoSuchElementException(String.format("User=%s not found", id))));
    }

    @RequestMapping(value = "/user/create", method = RequestMethod.GET)
    public ModelAndView getUserCreatePage() {
        return new ModelAndView("user_create", "form", new UserCreateForm());
    }

    @RequestMapping(value = "/user/create", method = RequestMethod.POST)
    public String handleUserCreateForm(@Valid @ModelAttribute("form") UserCreateForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user_create";
        }
        try {
            userService.create(form);
        } catch (DataIntegrityViolationException e) {
            bindingResult.reject("email.exists", "Email already exists");
            return "user_create";
        }
        return "redirect:/users";
    }

}

Viewing is mapped to /user/{id} URL, handled by getUserPage() method. It asks UserService for a user with id, which is extracted from URL, and passed as a parameter to this method. As you remember, UserService.getUserById() returns an instance of User wrapped in Optional. Therefore the .orElseThrow() is called on Optional to either get a User instance, or to throw the exception when it's null.

Creating a new User is mapped to /user/create, and is handled by two methods: getUserCreatePage() andhandleUserCreateForm(). The first one just returns a user_create view with an empty form as a form property of the model. The other one responds to POST request, and takes a validated UserCreateForm as a parameter. If there are errors in the form, as indicated by the BindingResult, the view is returned. If the form is ok, it is passed to UserService.create() method.

There is an additional checking for DataIntegrityViolationException. If it occurs, it is assumed it was because there was an attempt to create a User with an email address already in the database, therefore the form is rendered again.

In real life, getting to know which constraint was violated exactly is difficult (or not ORM-agnostic), so when doing assumptions like these, at least the exception should be logged for further inspection. The care should be taken to prevent this exception from happening in the first place, like proper validation in the form for a duplicate email.

Otherwise, if everything's ok, the redirection to /users URL is performed.

Customized validation of the UserCreateForm

The @InitBinder annotated method in UserController adds a UserCreateFormValidator to the form parameter, telling that it should be validated by it. The reason for this is that the UserCreateForm needs to be validated as a whole for two possible cases:

  • To check if the password and repeated passwords are matching
  • To check if the email exists in a database

To do so, UserCreateFormValidator is implemented as follows:

@Component
public class UserCreateFormValidator implements Validator {

    private final UserService userService;

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

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(UserCreateForm.class);
    }

    @Override
    public void validate(Object target, Errors errors) {
        UserCreateForm form = (UserCreateForm) target;
        validatePasswords(errors, form);
        validateEmail(errors, form);
    }

    private void validatePasswords(Errors errors, UserCreateForm form) {
        if (!form.getPassword().equals(form.getPasswordRepeated())) {
            errors.reject("password.no_match", "Passwords do not match");
        }
    }

    private void validateEmail(Errors errors, UserCreateForm form) {
        if (userService.getUserByEmail(form.getEmail()).isPresent()) {
            errors.reject("email.exists", "User with this email already exists");
        }
    }
}

Logging in

It will be mapped to /login URL and handled by LoginController:

@Controller
public class LoginController {

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
        return new ModelAndView("login", "error", error);
    }

}

Notice it only handles the GET request method, by returning the view with an optional parameter error in the model. ThePOST portion and actual handling of the form, will be done by Spring Security.

The template for the form looks like this:

<form role="form" action="/login" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    <div>
        <label for="email">Email address</label>
        <input type="email" name="email" id="email" required autofocus>
    </div>
    <div>
        <label for="password">Password</label>
        <input type="password" name="password" id="password" required>
    </div>
    <div>
        <label for="remember-me">Remember me</label>
        <input type="checkbox" name="remember-me" id="remember-me">
    </div>
    <button type="submit">Sign in</button>
</form>

<#if error.isPresent()>
<p>The email or password you have entered is invalid, try again.</p>
</#if>

It has ordinary emailpassword input fields, a remember-me checkbox, and a submit button. If the error is present, the message is displayed saying the authentication failed.

This efficiently contains everything that is needed for this application to be functional. What is left is to add the some security features.

CSRF protection

About the _csrf thing that appeared in the form view above. It is a CSRF token, generated by the application to validate the request. This is to make sure the form data is coming from your app and not from somewhere else. It is a feature of Spring Security, and is turned on by default by Spring Boot.

For both JSP and Freemarker, the _csrf variable is just exposed to the view. It contains an CsrfToken object with agetParameterName() method to get a CSRF parameter name (by default it is _csrf) and a getToken() method to get an actual token. You can then put it into a hidden field together with the rest of the form:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

Authentication

Now that we have an app ready, it's time to set up the authentication. Authentication means this part when we identify the guy using our login form as an existing user and get enough information about them to authorize their further requests.

Configuration

This configuration for Spring Security needs to be added:

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .usernameParameter("email")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .permitAll();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

}

This surely requires an explanation.

First thing to mention is @Order annotation, which basically keeps all the defaults set by Spring Boot, only overriding them in this file.

The configure(HttpSecurity http) method is where the actual URL-based security is set up. Let's see what it does here:

  • The login form is under /login, permitted for all. On login failure a redirect to /login?error will happen. We haveLoginController mapping this URL.
  • The parameter holding the username in login form is called 'email' because this is what we're using as username.
  • The logout URL is /logout, permitted for all. After that, the user will be redirected to /. One important remark here is that if the CSRF protection is turned on, the request to /logout should be POST.

The configure(AuthenticationManagerBuilder auth) method is where the authentication mechanism is set up. It is set up so that authentication will be handled by UserDetailsService, which implementation is injected, and the password is expected to be encrypted by BCryptPasswordEncoder.

It is worth to mention there are many other methods to authenticate than UserDetailsService. It is just a method that allows us to use existing service layer objects to do it, therefore it suits this application well.

UserDetailsService

The UserDetailsService is an interface the Spring Security uses to find out if the user using the login form exists, what their password should be, and what authority it has in the system. It has a single method:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

It is expected that the method loadUserByUsername() returns UserDetails instance if the username exists, or throwUsernameNotFoundException if it doesn't.

My implementation, the same one that gets injected into the SecurityConfig is as follows:

@Service
public class CurrentUserDetailsService implements UserDetailsService {
    private final UserService userService;

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

    @Override
    public CurrentUser loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userService.getUserByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException(String.format("User with email=%s was not found", email)));
        return new CurrentUser(user);
    }
}

As you see it's just asking UserService for a user with an email. If it doesn't exists, the exception is thrown. If it does, theCurrentUser object is returned.

But what's CurrentUser? It was supposed to be UserDetails. Well, firstly, the UserDetails is just an interface:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

It describes a user having username, password, list of GrantedAuthority objects and some self-explanatory flags for various reasons of account invalidation.

So we need to return an implementation. There is at least one provided by Spring Security, it'sorg.springframework.security.core.userdetails.User.

It can be used, but the tricky part is to relate our User domain object to UserDetails, as it may be needed for authorization. It can be done in multiple ways:

  • Make User domain object to implement UserDetails directly - it will allow to return the User exactly as received fromUserService. The downside is that is going to 'pollute' the domain object with a code related to Spring Security.
  • Use the provided implementation org.springframework.security.core.userdetails.User, just map a User entity to it. This is fine, however it would be nice to have some additional information about the user available, like id, direct access to role, or whatever else.
  • Therefore, the third solution is to extend the provided implementation and add whatever info can be needed, or just a whole User object as it is.

The last option is what I used here, so CurrentUser:

public class CurrentUser extends org.springframework.security.core.userdetails.User {

    private User user;

    public CurrentUser(User user) {
        super(user.getEmail(), user.getPasswordHash(), AuthorityUtils.createAuthorityList(user.getRole().toString()));
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    public Long getId() {
        return user.getId();
    }

    public Role getRole() {
        return user.getRole();
    }

}

...extends org.springframework.security.core.userdetails.User, which implements UserDetails. Additionally it just wraps our User domain object, adding (optional) convenience methods that proxy to it (getRole()getId(), etc.).

The mapping, as it's simple, happens in the constructor:

  • The UserDetails.username is populated from User.email
  • The UserDetails.password is populated from User.passwordHash
  • The User.role, converted to String, is wrapped in GrantedAuthority object by AuthorityUtils helper class. It will be available as the only element of the list when UserDetails.getAuthorities() is called.
  • I've decided to forget about the flags, as I'm not using them, they are all returned as true byorg.springframework.security.core.userdetails.User

Having said that, I have to mention that you should be careful when passing entities wrapped like that, or make them to directly implement UserDetails. It doesn't feel like a right thing to do to leak domain objects like that. This can also be problematic, i.e. if the entity has some LAZY-fetched associations you might run into problems when trying to fetch those outside of the Hibernate session.

Alternatively, just copy enough information from the entity to CurrentUser. Enough here means enough to authorize the user without calling a database for User entity, as this will increase the cost of performing authorizations.

Anyways, this should make our login form working. To round this up, what exactly happens step-by-step is that:

  • The user opens /login URL.
  • The LoginController returns a view with the login form.
  • User fills up the form, it gets submitted.
  • Spring Security calls CurrentUserDetailsService.loadUserByUsername() with a username (in this case email) just entered into the form.
  • CurrentUserDetailsService gets the user from UserDetailsService, and returns CurrentUser.
  • Spring Security calls CurrentUser.getPassword() and compares the password hash to the hashed password supplied in the form.
  • If it's ok, the user is redirected to where he came from to /login, if not, the redirection is to /login?error, which is again handled by LoginController.
  • Spring Security 'keeps' the CurrentUser object (wrapped in Authentication) to be used for authorization checks or by you should you require it.

Remember-me authentication

Sometimes it's convenient to have 'remember me' authentication, for those lazies who don't want to type into the login form, even if their session expires. This creates a security risk of course, so just be advised if you want to use it anyway.

It works like that:

  • The user logs in, and the form is posted with a remember-me parameter (in this application there is a checkbox for it)
  • Spring Security generates a token, keeps it, and sends it to the user in a cookie named remember-me.
  • Next time, when the accesses the application, Spring Security looks for the cookie, and if it holds a valid and non-expired token, it automatically authenticates the user.
  • Additionally you can find out whether the user was authenticated through 'remember me' or not.

To enable it, it just needs some additions to SecurityConfig.configure(HttpSecurity http), so it will look like that:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login?error")
            .usernameParameter("email")
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .deleteCookies("remember-me")
            .logoutSuccessUrl("/")
            .permitAll()
            .and()
            .rememberMe();
}

Notice rememberMe() at the end. It enables the whole thing. Worth noticing is that by default the tokens are stored in-memory. It means that when the application restarts, the tokens are lost. You may make them persistent, i.e. stored in a database, but for this application this is sufficient.

Another new thing is deleteCookies() on logout(). It forces the remember-me cookie to be deleted once the user logs out from the application.

Authorization

Authorization is a process of finding out if the guy, who is already authorized, and we know everything about him, can access a specified resource provided by the application.

In this application we have this information contained in a CurrentUser object. From Spring Security perspective it is important what GrantedAuthority he has. As you remember we populate those directly from User.role field, so they will be either 'USER' or 'ADMIN.

URL-based authorization

Now, per requirement, we want certain URLs to be accessible by everyone, some by authorized, and some by Admins.

It requires some additions to SecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/", "/public/**").permitAll()
            .antMatchers("/users/**").hasAuthority("ADMIN")
            .anyRequest().fullyAuthenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .failureUrl("/login?error")
            .usernameParameter("email")
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .deleteCookies("remember-me")
            .logoutSuccessUrl("/")
            .permitAll()
            .and()
            .rememberMe();
}

The new things are calls to antMatchers(). This enforces:

  • URL matching / pattern (home page in this app) and URLs matching /public/** will be permitted for all.
  • URLs matching /users/** pattern (list view in this app) will be permitted only for user with 'ADMIN' authority.
  • Any other request requires an authenticated user (either 'ADMIN' or 'USER').

Method-level authorization

All the steps above satisfy most of the requirements, but we have some that simply cannot be solved by URL-based authorization.

That is:

  • Admin users can list and view all the users, and create new ones as well

Here the problem is that the form to create a new user is mapped under /user/create. We could add this URL to theSecurityConfig, but we would end up micromanaging URLs in the config if we make a habit out of it. We could also map it to/users/create, which maybe makes sense here, but think of i.e. /user/{id}/edit that might appear in the future. It would be nice to leave it as it is, just to add method-level authorization to existing methods in UserController.

To make the method-level authorization work, the @EnableGlobalMethodSecurity(prePostEnabled = true) annotation needs to be added to SecurityConfig:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
class SecurityConfig extends WebSecurityConfigurerAdapter {
    // contents as before
}

This enables you to use two annotations on public methods in any layer of your application:

  • @PreAuthorize("expression") - if the expression, which is written in Spring Expression Language (SpEL), evaluates to 'true' the method will be called.
  • @PostAuthorize("expression") - the method is called first, when the check fails, the 403 status is returned to the user.

So, to solve the first requirement, the methods handling user creation in UserController need to be annotated by@PreAuthorize:

@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.GET)
public ModelAndView getUserCreatePage() {
    // contents as before
}

@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String handleUserCreateForm(@Valid @ModelAttribute("form") UserCreateForm form, BindingResult bindingResult) {
    // contents as before
}

The hasAuthority() SpEL expression is provided by Spring Security, among others, i.e.:

  • hasAnyAuthority() or hasAnyRole() ('authority' and 'role' are synonyms in Spring Security lingo!) - checks whether the current user has one of the GrantedAuthority in the list.
  • hasAuthority() or hasRole() - as above, but for just one.
  • isAuthenticated() or isAnonymous() - whether the current user is authenticated or not.
  • isRememberMe() or isFullyAuthenticated() - whether the current user is authenticated by 'remember me' token or not.

Domain object security

The last requirement to handle is as this:

  • Non-admin users can view their info, but cannot peek at other users

In other words we need a way to tell whenever it's ok for the user to make request to /user/{id}. It's ok when an user is an 'ADMIN' (which could be solved by URL-based authentication), but also when the current user makes the request on himself, which can't be solved that way.

This applies to the following method in UserController:

 @RequestMapping("/user/{id}")
 public ModelAndView getUserPage(@PathVariable Long id) {
     // contents as before
 }

We need to check if the current user is an 'ADMIN' OR the id passed to the method is the same as the id from the Userdomain object associated with the CurrentUser. Since CurrentUser has User instance wrapped, we have this information at hand. So, we can solve it in at least two ways:

  • You can cook an SpEL expression to compare id passed to the method with current user's id.
  • You can delegate this check to a service - this is preferred because the condition might change in the future, better to change in one place rather then in wherever the annotation is used.

To do so I've created CurrentUserService, with this interface:

public interface CurrentUserService {
    boolean canAccessUser(CurrentUser currentUser, Long userId);
}

And this implementation:

@Service
public class CurrentUserServiceImpl implements CurrentUserService {

    @Override
    public boolean canAccessUser(CurrentUser currentUser, Long userId) {
        return currentUser != null
                && (currentUser.getRole() == Role.ADMIN || currentUser.getId().equals(userId));
    }

}

Now to use it in @PreAuthorize annotation, just put:

@PreAuthorize("@currentUserServiceImpl.canAccessUser(principal, #id)")
@RequestMapping("/user/{id}")
public ModelAndView getUserPage(@PathVariable Long id) {
    // contents as before
}

Which is a SpEL expression for 'call a service with an instance of principal (CurrentUser), and id parameter passed to the method'.

This approach works ok if the the security model is relatively simple. If you find yourself writing something that resembles giving and checking multiple access permissions like view/edit/delete to multiple domain objects on per user basis, it's better to head into the direction of making use of Spring Security Domain Object ACLs.

This satisfies all the requirements, and the application is secured at this point.

Accessing current user in Spring Beans

Whenever you need to access the current user from a Controller or a Service you can inject an Authentication object. The current user instance can be obtained by calling Authentication.getPrincipal().

This works for constructors or properties, like this:

@Autowired
private Authentication authentication;

void someMethod() {
    UserDetails currentUser = (UserDetails) authentication.getPrincipal();
}

Also, this works for parameters in controller methods annotated by @RequestMapping or @ModelAttribute:

@RequestMapping("/")
public String getMainPage(Authentication authentication) {
    UserDetails currentUser = (UserDetails) authentication.getPrincipal();
    // something
}

In this application this can be cast directly to CurrentUser instead, since it is an actual UserDetails implementation that is being used:

CurrentUser currentUser = (CurrentUser) authentication.getPrincipal();

This way you also have an access to the domain object wrapped by it.

Accessing current user in views

It's useful to have an access to currently authenticated user in views, i.e. when some elements of the UI are rendered for users having a certain authority.

In JSP this can be done using a security taglib, like this:

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

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

This should print the current user's name. For Freemarker it's also possible to use this taglib, although it takes more than that.

You can also screw the taglib and just pass the Authentication object as a model property for a view:

@RequestMapping("/")
public String getMainPage(@ModelProperty("authentication") Authentication authentication) {
    return "some_view";
}

But why not for all the views, using @ControllerAdvice, and just UserDetails extracted from Authentication:

@ControllerAdvice
public class CurrentUserControllerAdvice {
    @ModelAttribute("currentUser")
    public UserDetails getCurrentUser(Authentication authentication) {
        return (authentication == null) ? null : (UserDetails) authentication.getPrincipal();
    }
}

After that you can access it through currentUser property in all views.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值