@[toc] In general, we use the login solution of Spring Security, configure the login interface, configure the login parameters, and configure the login callback. This is a best practice.

But!

There will always be some weird requirements, such as custom logins and writing your own logins like Shiro does. What if you want to implement this? Today Songo is here to share with you.

We have two ideas for customizing login logic in Spring Security, but the underlying implementation of the two ideas is similar, so let’s take a look.

1. Turn the bad into the good

Earlier, Songo shared a video on Spring Security:

  • Weird login I’ve never seen before

This video is to share with you that you can actually use HttpServletRequest to log in to your system, which is the JavaEE specification. This login method is not popular, but it is fun!

Then Songo shared a video:

  • Last lecture on SpringSecurity login data retrieval

This video is really on Spring Security’s implementation of the logins logic for HttpServletRequest, or in other words, the login apis that are provided in HttpServletRequest, Spring Security has rewritten them as they are implemented.

With these two reserves of knowledge in hand, the first DIY Spring Security login solution is coming up.

1.1 practice

Let’s see how it works.

Create a Spring Boot project with Web and Security dependencies as follows:

For convenience, let’s configure the default username and password in application.properties:

spring.security.user.name=javaboy
spring.security.user.password=123
Copy the code

Next we provide a SecurityConfig that permits the login interface:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login") .permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

The login interface is /login, and our custom login logic will be written in this. Let’s take a look:

@RestController
public class LoginController {
    @PostMapping("/login")
    public String login(String username, String password, HttpServletRequest req) {
        try {
            req.login(username, password);
            return "success";
        } catch (ServletException e) {
            e.printStackTrace();
        }
        return "failed"; }}Copy the code

Call the HttpServletRequest#login method directly, passing in the username and password to complete the login operation.

Finally, we provide a test interface as follows:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(a) {
        return "hello security!"; }}Copy the code

Just this!

To start the project, we first access the /hello interface, which will fail to access, and then we access the /login interface to perform the login operation, as follows:

Once you have logged in successfully, access the/Hello interface again.

Isn’t it Easy? After the login is successful, subsequent authorization operations remain unchanged.

1.2 Principle Analysis

If you are not familiar with this login method, you can watch these two videos to understand:

  • Weird login I’ve never seen before
  • Last lecture on SpringSecurity login data retrieval

I’m going to say a few words here.

We’re LoginController# login method is actually get into it from the HttpServlet3RequestFactory in an inner class Servlet3SecurityContextHolderAwareRequestWrapper object, in this class, rewrite the login and authenticate method of it, Let’s look at the login method first, as follows:

@Override
public void login(String username, String password) throws ServletException {
	if (isAuthenticated()) {
		throw new ServletException("Cannot perform login for '" + username + "' already authenticated as '"
				+ getRemoteUser() + "'");
	}
	AuthenticationManager authManager = HttpServlet3RequestFactory.this.authenticationManager;
	if (authManager == null) {
		HttpServlet3RequestFactory.this.logger.debug(
				"authenticationManager is null, so allowing original HttpServletRequest to handle login");
		super.login(username, password);
		return;
	}
	Authentication authentication = getAuthentication(authManager, username, password);
	SecurityContextHolder.getContext().setAuthentication(authentication);
}
Copy the code

You can see:

  1. If the user is already authenticated, an exception is thrown.
  2. An AuthenticationManager object is obtained.
  3. Call getAuthentication method to complete the login, in the method, according to the user name password build UsernamePasswordAuthenticationToken object, Then call the Authentication#authenticate method to complete the login with the following code:
private Authentication getAuthentication(AuthenticationManager authManager, String username, String password)
		throws ServletException {
	try {
		return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
	}
	catch (AuthenticationException ex) {
		SecurityContextHolder.clearContext();
		throw newServletException(ex.getMessage(), ex); }}Copy the code

This method returns an authenticated Authentication object.

  1. Finally, store the authenticated Authentication object into the SecurityContextHolder. I will not be verbose about the logic here. I have already described it several times in the previous video.

This is the execution logic of the login method.

Servlet3SecurityContextHolderAwareRequestWrapper class also rewrite the HttpServletRequest# authenticate method, this is also do authentication methods:

@Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
	AuthenticationEntryPoint entryPoint = HttpServlet3RequestFactory.this.authenticationEntryPoint;
	if (entryPoint == null) {
		HttpServlet3RequestFactory.this.logger.debug(
				"authenticationEntryPoint is null, so allowing original HttpServletRequest to handle authenticate");
		return super.authenticate(response);
	}
	if (isAuthenticated()) {
		return true;
	}
	entryPoint.commence(this, response,
			new AuthenticationCredentialsNotFoundException("User is not Authenticated"));
	return false;
}
Copy the code

As you can see, this method is used to determine whether the user has completed authentication, returning true to indicate that the user has completed authentication and false to indicate that the user has not completed authentication.

2. Power of source code

Instead of using the HttpServletRequest#login method, we call the AuthenticationManager for login authentication.

Take a look.

First we modify the configuration class as follows:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login"."/login2")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        provider.setUserDetailsService(manager);
        return newProviderManager(provider); }}Copy the code
  1. First in login Permit, add/login2Interface, which is the second login interface I will customize.
  2. Provide an AuthenticationManager instance. The way to play AuthenticationManager has been shared many times in the previous Spring Security series, so I won’t repeat it heress). Create the AuthenticationManager instance, also need to provide a DaoAuthenticationProvider, as we all know, the user password check work done in this class inside, And for a UserDetailsService DaoAuthenticationProvider configuration instance, the entity provides a user data source.

Next provide a login interface:

@RestController
public class LoginController {
    @Autowired
    AuthenticationManager authenticationManager;
    @PostMapping("/login2")
    public String login2(String username, String password, HttpServletRequest req) {
        try {
            Authentication token = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            SecurityContextHolder.getContext().setAuthentication(token);
            return "success";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "failed"; }}Copy the code

User name password in the login interface, was introduced into parameters, such as user name password will then be encapsulated into a UsernamePasswordAuthenticationToken parameters, such as object, Finally, call the AuthenticationManager#authenticate method for Authentication. After successful Authentication, an authenticated object will be returned. Manually store the Authentication object into the SecurityContextHolder.

After the configuration is complete, restart the project and perform the login test.

The second solution is the same as the first solution, and the second solution is essentially pulling out the bottom layer of the first solution and re-implementing it itself, and that’s it.

3. Summary

There are two kinds of DIY login solutions for Spring Security. These solutions may not be very common in your work, but they are very helpful for you to understand the principle of Spring Security

Also, if you’re having a hard time reading this post, you might want to reply to ss on the back of our wechat account and check out other articles in the Spring Security series to help you understand this article, as well as Songo’s new book:

Spring Security in Plain English has been published by Tsinghua University Press. Spring Security in Plain English has been published by Tsinghua University Press. Spring Security in Plain English has been published by Tsinghua University Press. Spring Security in Plain English has been published by Tsinghua University Press.