[TOC]

The implementation of Spring Security features consists primarily of a series of filters that work together. Also known as filter chains,Spring Security loads 15 filters by default, but some can be added or removed as you configure them.

I. Introduction of filter chain

Filter is a typical AOP idea, the following simple understanding of these filter chain, the subsequent source code analysis in relation to the filter chain in detail

1.org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

WebAsyncManager is retrieved according to the request encapsulation, and the security context retrieved/registered from WebAsyncManager can be used to call the processing interceptor

2.org.springframework.security.web.context.SecurityContextPersistenceFilter

SecurityContextPersistenceFilter mainly use SecurityContextRepository save or update a SecurityContext in the session, The SecurityContext is given to future filters to establish the required context for subsequent fiFilters. The SecurityContext stores authentication and permission information for the current user.

3.org.springframework.security.web.header.HeaderWriterFilter

Add the appropriate information to the Header of the request, which can be controlled inside the HTTP tag using Security :headers

4.org.springframework.security.web.csrf.CsrfFilter

CSRF is also called cross-domain request forgery. SpringSecurity verifies that all post requests contain the token information generated by the CSRF. If they do not, an error is reported. It prevents CSRF attacks.

5.org.springframework.security.web.authentication.logout.LogoutFilter

Matches the request whose URL is /logout to log the user out and clear authentication information.

6.org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

Form authentication relies entirely on this filter, which by default matches the URL /login and must be a POST request

7.org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

If no authentication page is specified in the configuration file, a default authentication page is generated by the filter.

8.org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

From this filter a default exit login page can be produced

9.org.springframework.security.web.authentication.www.BasicAuthenticationFilter

This filter automatically parses HTTP requests whose header name is Authentication and starts with Basic

10.org.springframework.security.web.savedrequest.RequestCacheAwareFilter

A RequestCache is maintained internally with HttpSessionRequestCache to cache HttpServletRequest

11. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

The ServletRequest has been wrapped to make the Request have a richer API

12.org.springframework.security.web.authentication.AnonymousAuthenticationFilter

When the authentication information in the SecurityContextHolder is empty, an anonymous user is created and stored in the SecurityContextHolder. Spring Security also goes through an authentication process to accommodate unlogged access, but with an anonymous identity.

13. org.springframework.security.web.session.SessionManagementFilter

SecurityContextRepository limit the number of the same user to open multiple sessions

14.org.springframework.security.web.access.ExceptionTranslationFilter

The filters in the entire springSecurityFilterChain rear, abnormal conversion to convert the whole link is in

15. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

Gets authorization information for access to the configured resource and determines whether it has permission based on the user information stored in SecurityContextHolder.

Two, certification

2.1 HttpBasic certification

The HttpBasic login authentication mode is the simplest, if not the most rudimentary, way Spring Security implements login authentication. Its purpose is not to ensure the absolute security of login verification, but to provide a “gentleman not villain” login verification.

On earlier versions of Spring Boot that were 1.x and relied on Security 4.x, there is no need for any configuration and the default HttpBasic authentication will pop up when starting project access. With Spring Boot2.0 and above (relying on Security 5.x), HttpBasic is no longer the default authentication mode. In Spring Security 5.x, the default authentication mode is forms mode.

The HttpBasic mode requires that the transmitted username password be encrypted using Base64 mode. If the user name is admin and the password is admin, the string admin:admin is encrypted using the Base64 encoding algorithm. The encrypted result may be: YWtaW46YWRtaW4=. HttpBasic mode is really very simple and crude authentication mode, Base64 encryption algorithm is reversible, it is not difficult to crack

2.2 formLogin Login authentication mode

Spring Security’s HttpBasic mode, which is relatively simple, with simple login authentication via Http headers, and no custom login page, so the scenarios are narrow. For a complete application, the pages related to login verification are highly customized, aesthetically pleasing, and provide multiple login methods. This requires Spring Security to support our own custom login page, which Spring Boot2.0 and above (depending on Security 5.x) generates by default.

2.3 Form Authentication

2.3.1 Writing the SecurityConfiguration class in the Config package

/ * * *@author wuzhixuan
 * @version 1.0.0
 * @ClassName SecurityConfiguration.java
 * @DescriptionSecurity configuration class *@createTimeNovember 24, 2021 16:35:00 */

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /* http.httpbasic () // Enable base authentication.and ().authorizerequests ().anyRequest().authenticated(); // All requests require login authentication to access */
        http.formLogin().loginPage("/toLoginPage") // Enable form validation
                .and().authorizeRequests()
                .antMatchers("/toLoginPage").permitAll() // Allow the current request
                .anyRequest().authenticated(); // All requests require login authentication to access;}}Copy the code

After the service is restarted, static files such as CSS and JS are not loaded successfully

2.3.2 Solving the static Resource Interception Problem

@Override
public void configure(WebSecurity web) throws Exception {
    // Resolve static resource interception
    web.ignoring().antMatchers("/css/**"."/js/**"."/images/**"."/favicon.ico");
}
Copy the code

The differences between the Security builders HttpSecurity and WebSecurity in Spring Security are:

  • WebSecurity not only defines security controls for certain requests through HttpSecurity, but also defines security controls for other requests that can be ignored.
  • HttpSecurity is only used to define requests that require security control (although HttpSecurity can also specify that some requests do not require security control).
  • You can think of HttpSecurity as part of WebSecurity, which is a larger concept that includes HttpSecurity;
  • Different building goals
    • The WebSecurity build targets the entire Spring Security Security filter FilterChainProxy,
    • HttpSecurity is built to target only a SecurityFilterChain in FilterChainProxy.

Transform the login

Protected void configure(HttpSecurity HTTP) throws Exception {/* http.httpbasic () // Enable base authentication .and().authorizeRequests().anyRequest().authenticated(); // All requests require login authentication to access */ http.formLogin() // Enable form authentication. LoginPage ("/toLoginPage") // Customize the login page Login request URl.usernameParameter ("username") // Modify the custom form name value.passwordParameter ("password").SuccessForwardurl ("/") // .and().authorizerequests ().antmatchers ("/toLoginPage").permitall () // Allow current request.anyRequest ().authenticated(); // All requests require login authentication to access; // Disable CSRF protection http.csrf().disable(); // allow iframe to load the page http.headers().frameoptions ().sameOrigin(); }Copy the code

There is a problem with the inline frame iframe. In Spring Security, the default value of x-frame-options is DENY. In non-Spring Security, the default value of X-frame-options is also DENY. In this case, the browser refuses to load any Frame page. The meanings are as follows:

  • DENY: The browser denies the current page from loading any Frame page this selection is default.
  • SAMEORIGIN: the IP address of the frame page must be the SAMEORIGIN domain name

After setting it, you see the successful display

2.4 Implement authentication based on database

The usernames and passwords we used before were automatically generated by the framework, so how do we implement the database-based usernames and passwords function? To implement this, you need to implement a UserDetailsService interface for Security. Override this interface loadUserByUsername

  • Write MyUserDetailsService and implement UserDetailsService interface, rewrite loadUserByUsername method
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);// The username was not found
        }
        // Declare a permission set, because the constructor cannot pass null
        Collection<? extends GrantedAuthority> authorities = new ArrayList<>();

        return new org.springframework.security.core.userdetails.User(username,
                "{noop}" + user.getPassword(),// {noop} indicates unencrypted authentication
                true.// Whether the user is enabled True indicates that the user is enabled
                true.// Whether the user is expired True Indicates that the user is not expired
                true.// Whether the user credentials are expired True Indicates that the credentials are not expired
                true.// Whether the user is locked True indicates that the user is not lockedauthorities ); }}Copy the code
  • Specify custom user authentication in the SecurityConfiguration configuration class
/** * Authentication manager *@param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(myUserDetailsService);
}
Copy the code

2.5 Password Encryption Verification

In database based user login, we use the password in plaintext by prefixing the password in plaintext with {noop}. So let’s talk about the password encoding in Spring Security.

PasswordEncoder in Spring Security is our tool interface for encoding passwords. This interface has only two functions: one is match verification. The other is cryptography.

  • BCrypt algorithm introduction

BCrypt strong hash method each encryption results are not the same, so more secure.

2.6 Login

Org. Springframework. Security. Web. Authentication. Logout. LogoutFilter matching URL/logout request, realize the user exit, removal of authentication information. You can simply send the request to /logout, which can also be specified in the configuration class. At the same time, LogoutSuccessHandler also has the corresponding custom processing, execute after logging out successfully, if there is remember-me data at the same time, delete at the same time

/*** Custom login succeeded, failed, exit processing class */ @Service public class MyAuthenticationService implements AuthenticationSuccessHandler.AuthenticationFailureHandler.LogoutSuccessHandler { private RedirectStrategy redirectStrategy = newDefaultRedirectStrategy(); .@Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("Exit successful follow-up procedure...."); redirectStrategy.sendRedirect(request, response, "/toLoginPage"); }}Copy the code

SecurityConfig configuration modified

.and().logout().logoutUrl("/logout")LogoutSuccessHandler (myAuthenticationService)// Customize exit processing
Copy the code

2.7 Graphic verification code verification

Graphic verification code is generally to prevent malicious, human eyes look laborious, let alone the machine. Many websites have adopted verification code technology in order to prevent users from using robots to automatically register, log in and fill water. The so-called verification code is a series of random numbers or symbols, to generate a picture, the picture with some interference, there are currently need to manually slide graphic verification code. There could be a third party platform dedicated to that

Adding a captcha to Spring Security can be roughly divided into three steps:

  • Generate captcha image according to random number;
  • The verification code image is displayed on the login page
  • Add the verification code to the authentication process

Spring Security authentication validation is done by UsernamePasswordAuthenticationFilter filters, so our captcha validation logic should be before the filter. Follow-up operations can be performed only after the verification code is passed. The process is as follows:

User-defined verification code filter ValidateCodeFilter

/ * * *@author wuzhixuan
 * @version 1.0.0
 * @ClassName ValidateCodeFilter.java
 * @DescriptionThe verification code validates the filter by inheriting OncePerRequestFilter to ensure that the filter is passed only once in a request and does not need to be repeated *@createTimeNovember 25, 2021 11:19:00 */
@Component

public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    MyAuthenticationService myAuthenticationService;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // Check whether it is a login request. Only login requests verify the verification code
        if (httpServletRequest.getRequestURI().equals("/login") && httpServletRequest.getMethod().equals("POST")) {
            try {
                validate(httpServletRequest);
            } catch (ValidateCodeException e) {
                myAuthenticationService.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return; }}// If it is not a login request, call the following filter chain directly
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validate(HttpServletRequest request) {

        / / get the IP
        String remoteAddr = request.getRemoteAddr();
        // Concatenate the redis key
        String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + remoteAddr;

        // Get imageCode from redis
        String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
        String imageCode = request.getParameter("imageCode");

        if(! StringUtils.hasText(imageCode)) {throw new ValidateCodeException("The value of the captcha cannot be empty!"); }

        if (redisImageCode == null) { throw new ValidateCodeException("Verification code has expired!"); }

        if(! redisImageCode.equals(imageCode)) {throw new ValidateCodeException("Verification code is not correct!"); }

        // Delete imageCode from redisstringRedisTemplate.delete(redisKey); }}Copy the code

SecurityConfig adds a custom captcha interceptor

// Prefixes the username and password filter
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
Copy the code

Front-end verification code code

<img src="/code/image" alt="" width="100" height="32" class="passcode" style="height:43px; cursor:pointer;" onclick="this.src=this.src+'? '" >Copy the code

Obtain the verification code background interface

package com.lagou.controller;

import com.lagou.domain.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/** * Process the request to generate the verification code */
@RestController
@RequestMapping("/code")
public class ValidateCodeController {

    public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
    public final static int expireIn = 60;  // Verification code validity period 60s

    // Use sessionStrategy to store the generated capttos in Session and output the generated images to the login page via IO stream.
    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // Obtain the access IP address
        String remoteAddr = request.getRemoteAddr();
        // Generate a captcha object
        ImageCode imageCode = createImageCode();
        // The generated verification code object is stored in redis. The KEY is REDIS_KEY_IMAGE_CODE+IP address
        stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr)
                .set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
        // Output the generated image to the login page through the IO stream
        ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
    }

    /** * is used to generate the verification code object **@return* /
    private ImageCode createImageCode(a) {

        int width = 100;    // Captcha image width
        int height = 36;    // Captcha image length
        int length = 4;     // Verification code number
        // Create a buffered image object
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // Get the Graphics object to draw on the image
        Graphics g = image.getGraphics();

        Random random = new Random();

        // Set the color and draw a random line
        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        // Generate random numbers and draw
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }
        g.dispose();
        return new ImageCode(image, sRand.toString());
    }

    /** * get random demo **@param fc
     * @param bc
     * @return* /
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return newColor(r, g, b); }}Copy the code

This article is published by OpenWrite!