Read more technical articles about Angular, TypeScript, Node.js/Java, Spring, etc. welcome to my blog—— The road of building immortals in the whole stack
stay Spring Boot integrates Spring Security In this article, we introduced how to quickly integrate Spring Security in the Spring Boot project, as well as how to change the user name and password generated by the system by default. Next, this article will introduce how to implement custom user authentication in Spring Security based on the project created in the article of integrating Spring Security with Spring Boot.
I. user defined authentication process
Development environment and main framework version used in the project:
- java version "1.8.0_144"
- spring boot 2.2.0.RELEASE
- spring security 5.2.0.RELEASE
1.0 configuration project pom.xml file
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.semlinker</groupId> <artifactId>custom-user-authentication</artifactId> <version>0.0.1-SNAPSHOT</version> <name>custom-user-authentication</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- ellipsis spring-boot-starter-test,spring-security-test and spring-boot-devtools --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
1.1 user defined model
First, create a MyUser class to store the simulated user information (in actual development, the real user information is usually obtained from the database):
// com/semlinker/domain/MyUser.java @Data public class MyUser implements Serializable { private static final long serialVersionUID = -1090551705063344205L; private String userName; private String password; private boolean accountNonExpired = true; // Indicates whether the account has not expired private boolean accountNonLocked = true; // Indicates whether the account is unlocked private boolean credentialsNonExpired = true; // Indicates that the user's credentials have not expired, such as the user password private boolean enabled = true; // Indicates whether the user is enabled }
1.2 custom Security configuration class and PasswordEncoder object
Then configure the PasswordEncoder object, as the name implies, for password encryption. This object needs to be used in the following UserDetailsService service, so we need to configure it in advance. PasswordEncoder is a password encryption interface. There are many implementation classes in Spring Security, such as BCryptPasswordEncoder, Pbkdf2PasswordEncoder and LdapShaPasswordEncoder.
Of course, we can also customize PasswordEncoder, but the function of BCryptPasswordEncoder implemented in Spring Security is powerful enough. It can generate different results after encrypting the same password, which greatly improves the security of the system. That is to say, although some users who use the same password in the system accidentally disclose the password, it will not cause other users to disclose the password. Since BCryptPasswordEncoder is so powerful, we must use it directly. The specific configuration is as follows:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
1.3 customize UserDetailsService service
To customize the UserDetailsService service, you need to implement the UserDetailsService interface, which only contains a loadUserByUsername method, which is used to load matching users through username. A UsernameNotFoundException exception is thrown when the user corresponding to username cannot be found. The UserDetailsService interface is defined as follows:
// org/springframework/security/core/userdetails/UserDetailsService.java public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
The loadUserByUsername method returns the UserDetails object. The UserDetails here is also an interface. Its definition is as follows:
// org/springframework/security/core/userdetails/UserDetails.java public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
As the name implies, UserDetails represents detailed user information. This interface covers some necessary user information fields, which are extended by specific implementation classes. The specific functions of the above methods are as follows:
- getPassword(): used to get the password;
- getUsername(): used to get the user name;
- isAccountNonExpired(): used to determine whether the account has not expired;
- isAccountNonLocked(): used to determine whether the account is unlocked;
- isCredentialsNonExpired(): used to determine whether the user's credentials have not expired, that is, whether the password has not expired;
- isEnabled(): used to determine whether the user is available.
After introducing the above content, let's create a MyUserDetailsService class and implement the UserDetailsService interface, as follows:
// com/semlinker/service/MyUserDetailsService.java @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { MyUser myUser = new MyUser(); myUser.setUserName(username); myUser.setPassword(this.passwordEncoder.encode("hello")); // Use the implementation class User of UserDetails in Spring Security to create the User object return new User(username, myUser.getPassword(), myUser.isEnabled(), myUser.isAccountNonExpired(), myUser.isCredentialsNonExpired(), myUser.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
1.4 configure UserDetailsService Bean and AuthenticationManagerBuilder object
To use our customized MyUserDetailsService in Spring Security, you also need to configure it in the WebSecurityConfig class:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean UserDetailsService myUserDetailService() { return new MyUserDetailsService(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder()); } }
In the above configure method, we configured the custom MyUserDetailsService and PasswordEncoder objects.
1.5 create relevant Controller and user-defined login page and homepage
In Spring Security, the DefaultLoginPageGeneratingFilter will generate the default login interface for us:
I believe that many young people are "not used to" this page. Let's "facelift" this page.
HomeController class
// com/semlinker/controller/HomeController.java @Controller public class HomeController { @GetMapping("/") public String index() { return "index"; } }
UserController class
// com/semlinker/controller/UserController.java @Controller public class UserController { @GetMapping("/login") public String login() { return "login"; } }
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Semlinker Home page of Xiuxian Road </title> </head> <body> <h3>Welcome to Semlinker Home page of Xiuxian Road</h3> </body> </html>
login.html
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>Semlinker Road to immortality landing page</title> </head> <body> <form class="login-form" method="post" action="/login"> <h1>Login</h1> <div class="form-field"> <i class="fas fa-user"></i> <input type="text" name="username" id="username" class="form-field" placeholder=" " required> <label for="username">Username</label> </div> <div class="form-field"> <i class="fas fa-lock"></i> <input type="password" name="password" id="password" class="form-field" placeholder=" " required> <label for="password">Password</label> </div> <button type="submit" value="Login" class="btn">Login</button> </form> </body> </html>
1.6 configure default login page
After the login page is created, it needs to be configured in the WebSecurityConfig class to take effect. The corresponding configuration method is as follows:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // Omit the previously set content protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login"); } }
After completing the above configuration, let's test the effect. First, start the Spring Boot application, and then open it in the browser http://localhost:8080/login Address, if all goes well, you will see the following interface:
(page from https://codepen.io/alphardex/...)
Next, we will perform the login operation. The user name here can be arbitrary, and the password is the hello we set earlier. But when we input the correct user name and password and click to log in, we will see the following exception page:
Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon Oct 28 14:27:25 CST 2019 There was an unexpected error (type=Forbidden, status=403). Forbidden
What's the reason? Why is access forbidden? Don't worry. First open the application.properties file under the src/main/resources / directory of the current project, and then enter the following configuration information:
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
After the configuration is completed, restart the application, and then perform the above login operation again. If you have guessed correctly, you can log in again. The user name and password you entered are correct, but you still see the Whitelabel Error Page. In fact, the DEBUG mode of the Security FilterChainProxy that we have just enabled, so let's take a look at the exception information output by the console:
You can find the / login request through the figure above. After the csrfilter filter, it will no longer continue to execute. The csrfilter filter here is used to deal with cross site request forgery attacks. Cross Site Request Forgery, also known as one click attack or session riding, is usually abbreviated as CSRF or XSRF. It is an attack method to coerce users to perform unintended operations on the currently logged in Web applications.
Now we have a general idea of the reason. Since our login page does not need to enable Csrf defense, we first disable the Csrf filter:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") .and().csrf().disable(); } }
After updating the WebSecurityConfig configuration class, run the previous login process again. This time, when you click login, you will see the content welcome to the home page of Semlinker fairy road.
2. Handling different types of requests
By default, when users access the protected resources through the browser, they will be automatically redirected to the default login address by default. This is not a big problem for traditional Web projects, but it is not suitable for projects with front-end and back-end separation. For projects separated from the front and the back, the server generally only needs to provide the API interface in JSON format.
In view of the above problems, there is a scheme as follows for reference. That is, according to whether the request ends in. html, different processing methods are corresponding. If it ends with. html, redirect to the login page, otherwise return to "access resources need authentication!" Information, and the HTTP status code is 401 (HttpStatus.UNAUTHORIZED).
To implement the above functions, we first define a WebSecurityController class, which is implemented as follows:
// com/semlinker/controller/WebSecurityController.java @Slf4j @RestController public class WebSecurityController { // Cache and recovery of original request information private RequestCache requestCache = new HttpSessionRequestCache(); // For redirection private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); /** * Default login page, used to handle different login authentication logic * * @param request * @param response * @return */ @RequestMapping("/authentication/require") @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public String requireAuthenication(HttpServletRequest request, HttpServletResponse response) throws Exception { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { String targetUrl = savedRequest.getRedirectUrl(); log.info("The request that caused the jump is:" + targetUrl); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { redirectStrategy.sendRedirect(request, response, "/login.html"); } } return "The accessed service requires identity authentication. Please guide the user to the login page"; } }
Next, change the default login page of formLogin to / authentication/require, and set no interception through the ant matchers method:
// com/semlinker/config/WebSecurityConfig.java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .and() .authorizeRequests() .antMatchers("/authentication/require", "/login.html").permitAll() .anyRequest().authenticated() .and().csrf().disable() ; } }
At the same time, modify the UserController class defined earlier to support / login.html path mapping:
// com/semlinker/controller/UserController.java @Controller public class UserController { @GetMapping({"login", "/login.html"}) public String login() { return "login"; } }
After the above adjustment, visit us http://localhost:8080/index The page will automatically jump to http://localhost:8080/authentication/require , and output "the accessed service needs identity authentication, please guide the user to the login page". And when we visit http://localhost:8080/index.html When, the page will jump to the login page.
3. User defined login success and failure logic
In the front-end and back-end separation project, when users log in successfully or fail, they need to return corresponding information to the front-end, rather than directly jump the page. For the scenario of front-end and back-end separation, the two interfaces of AuthenticationSuccessHandler and AuthenticationFailureHandler in Spring Security, or inheriting simpleurauthenticationsuccesshandler or simpleurauthenticationfailurehandler class, can be used to implement the processing logic of user-defined login success and login failure.
3.1 user defined login success processing logic
Here we choose to inherit the SimpleUrlAuthenticationSuccessHandler class to implement the custom login success processing logic:
// com/semlinker/handler/MyAuthenctiationSuccessHandler.java @Slf4j @Component public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("Login successfully"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
3.2 user defined login failure processing logic
We also choose to inherit the SimpleUrlAuthenticationFailureHandler class to implement the custom login failure processing logic:
// com/semlinker/handler/MyAuthenctiationFailureHandler.java @Slf4j @Component public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException { log.info("Login failed"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage())); } }
3.3 configure myauthorizationsuccesshandler and myauthorizationfailurehandler
Finally, for the login success and failure logic of custom processing to take effect, you need to configure the successHandler and failureHandler properties of the FormLoginConfigurer object in the WebSecurityConfig class. So far, the complete configuration of the WebSecurityConfig class is as follows:
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler; @Autowired private MyAuthenctiationSuccessHandler myAuthenctiationSuccessHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean UserDetailsService myUserDetailService() { return new MyUserDetailsService(); } protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService()).passwordEncoder(passwordEncoder()); } protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") .successHandler(myAuthenctiationSuccessHandler) .failureHandler(myAuthenctiationFailureHandler) .and() .authorizeRequests() .antMatchers("/authentication/require", "/login").permitAll() .anyRequest().authenticated() .and().csrf().disable() ; } }
The previous article has introduced the process of implementing user-defined authentication in Spring Security. If you encounter other problems during the learning process, it is recommended to enable the DEBUG mode of FilterChainProxy for log troubleshooting.
Project address: Github - custom-user-authentication