编程知识 cdmana.com

Spring boot is the most complete integration of spring security. Just read this article

I combine other blogs with my own information , Step by step, it's integrated security Security framework , There are a lot of holes in it , There are also many things I don't understand , Make a note of that .

development tool :ide、 database :mysql5.7、springboot edition :2.3.7

Individual to Spring Security The process of execution is generally understood ( For reference only )

 

 

Use Spring Security It's simple , As long as pom.xml In file , introduce spring security We can rely on

pom To configure :

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

At this time, we will not make any configuration in the configuration file , Write a random Controller 

@RestController
public class TestController {
    @GetMapping("/hello")
    public String request() {
        return "hello";
    }
}

Start project , We will find that there is such a log

Representation at this time Security take effect , The project is protected by default , We should visit Controller The interface (http://localhost:8080/securitydemo/hello), See the following login interface ( This interface is security The default login interface of the framework , Later, you can change to a custom login interface )

  What's the username and password in this ? At this point we need to enter the user name :user, The password is in the previous log "19262f35-9ded-49c2-a8f6-5431536cc50c", After typing , We can see that the interface can be accessed normally at this time

 

In the old version Springboot in ( for instance Springboot 1.x In the version ), You can turn off Spring Security In effect , But now Springboot 2 No longer supported in

security:
  basic:
    enabled: false

springboot2.x After that, you can set... In the startup class

1、 Configure memory based role authorization and authentication information

  1.1 Catalog

  

  1.2 WebSecurityConfg Configuration class

  Spring Security The core configuration class of is WebSecurityConfigurerAdapter abstract class , This is the entry point for permission management to start , Here we customize an implementation class to get rid of it .

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description SpringSecurity Security framework configuration 
 */
@Configuration
@EnableWebSecurity// Turn on Spring Security The function of 
//prePostEnabled Attribute decision Spring Security Whether annotations are available before interfaces @PreAuthorize,@PostAuthorize Etc , Set to true, Will intercept interfaces with these annotations 
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
        *  Memory based approach , Create two users admin/123456,user/123456
        * */
        auth.inMemoryAuthentication()
                .withUser("admin")// user name 
                .password(passwordEncoder().encode("123456"))// password 
                .roles("ADMIN");// role 
        auth.inMemoryAuthentication()
                .withUser("user")// user name 
                .password(passwordEncoder().encode("123456"))// password 
                .roles("USER");// role 
    }

    /**
     *  Specify the encryption method 
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        //  Use BCrypt Encrypted password 
        return new BCryptPasswordEncoder();
    }
}

  1.3 MainController Controller interface

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description  Main controller 
 */
@RestController
public class MainController {

    @GetMapping("/hello")
    public String printStr(){
        System.out.println("hello success");
        return "Hello success!";
    }

}

So when we run it again, we can go through admin/123456、user/123456 Two users logged in .

Of course , You can also Create user account based on configuration file , stay pom.xml Add :

 2、 Configure database based authentication information and role authorization

  2.1  Catalog

   2.2  CustomUserDetailsService Implementation class

UserDetailsService Is the login user query that needs to be implemented service Interface , Realization loadUserByUsername() Method , Here we customize CustomUserDetailsService Implement classes to implement UserDetailsService Interface

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description
 */

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Resource
    private UserInfoService userInfoService;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        /**
         * 1/ adopt userName  Get userInfo Information 
         * 2/ adopt User(UserDetails) return details.
         */
        // adopt userName Get user information 
        UserInfo userInfo = userInfoService.getUserInfoByUsername(userName);
        if(userInfo == null) {
            throw new UsernameNotFoundException("not found");
        }
        // Define a list of permissions .
        List<GrantedAuthority> authorities = new ArrayList<>();
        //  The name of the resource that the user can access ( Or the permissions that users have )  Be careful : must "ROLE_" start 
        authorities.add(new SimpleGrantedAuthority("ROLE_"+ userInfo.getRole()));
        User userDetails = new User(userInfo.getUserName(),passwordEncoder.encode(userInfo.getPassword()),authorities);
        return userDetails;
    }
}
WebSecurityConfg Configuration class :
/**
 * @Author qt
 * @Date 2021/3/25
 * @Description SpringSecurity Security framework configuration 
 */
@Configuration
@EnableWebSecurity// Turn on Spring Security The function of 
//prePostEnabled Attribute decision Spring Security Whether annotations are available before interfaces @PreAuthorize,@PostAuthorize Etc , Set to true, Will intercept interfaces with these annotations 
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter {
    /**
     *  Specify the encryption method 
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        //  Use BCrypt Encrypted password 
        return new BCryptPasswordEncoder();
    }
}

For by userName The service layer for getting user information , Persistence layer and database statement are not introduced , What we use here is SSM frame , Use mybaits.

 3、 Custom form authentication login

  3.1  Catalog

  

   3.2  WebSecurityConfg Core configuration class

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description spring-security The core configuration of rights management 
 */
@Configuration
@EnableWebSecurity// Turn on Spring Security The function of 
//prePostEnabled Attribute decision Spring Security Whether annotations are available before interfaces @PreAuthorize,@PostAuthorize Etc , Set to true, Will intercept interfaces with these annotations 
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter {

    @Resource
    private AuthenticationSuccessHandler loginSuccessHandler; // Authentication success result processor 
    @Resource
    private AuthenticationFailureHandler loginFailureHandler; // Authentication failure result processor 

    //http Request interception configuration 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();// Start operation iframe Nested pages 

        http//1、 Configure permission Authentication 
            .authorizeRequests()
                // Configure not to intercept routes 
                .antMatchers("/500").permitAll()
                .antMatchers("/403").permitAll()
                .antMatchers("/404").permitAll()
                .antMatchers("/login").permitAll()
                .anyRequest() // Any other request 
                .authenticated() // All need identity authentication 
                .and()
             //2、 Login configuration form authentication method 
            .formLogin()
                .loginPage("/login")// Custom login page url
                .usernameParameter("username")// Set login account parameters , Consistent with form parameters 
                .passwordParameter("password")// Set login password parameters , Consistent with form parameters 
                //  tell Spring Security Process the submitted credentials when sending the specified path , By default , Redirect the user back to the page the user came from . login form form in action The address of , That is, the path to handle authentication requests ,
                //  Just keep the form action and HttpSecurity Internally configured loginProcessingUrl It's OK to be consistent , You don't have to deal with it yourself , It doesn't pass the request to Spring MVC And your controller , So we don't have to write another one ourselves /user/login The controller interface of 
                .loginProcessingUrl("/user/login")// Configure the default login entry 
                .defaultSuccessUrl("/index")// The default jump page path after successful login 
                .failureUrl("/login?error=true")
                .successHandler(loginSuccessHandler)// Use a custom successful results processor 
                .failureHandler(loginFailureHandler)// Use custom failed result processor 
                .and()
            //3、 Cancellation 
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .permitAll()
                .and()
            //4、session management 
            .sessionManagement()
                .invalidSessionUrl("/login") // After the failure, jump to the landing page 
                // Single user login , If there is a login , The same user logs in somewhere else to take the previous one off the line 
                //.maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy())
                // Single user login , If there is a login , The same user cannot log in anywhere else 
                //.maximumSessions(1).maxSessionsPreventsLogin(true) ;
                .and()
            //5、 Disable Cross Station csrf Attack defense 
            .csrf()
                .disable();
    }
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        // Configuration static files do not require authentication 
        web.ignoring().antMatchers("/static/**");
    }

    /**
     *  Specify the encryption method 
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        //  Use BCrypt Encrypted password 
        return new BCryptPasswordEncoder();
    }
}

Step on the pit 1: Login page interface /login And login authentication interface /user/login, Here's what I've been wrong about before , Here are the pictures on the Internet

Step on the pit 2:springboot To configure spring security Static resources cannot be accessed

security Configuration of : In the class WebSecurityConfig Inherit WebSecurityConfigurerAdapter, This class is what we are configuring security When , To our request url And some authentication configuration of permission rules . I won't be specific , Here is the problem of static resources .

In this class, we will override some methods , There is one way , You can configure static resources for us without authentication .

@Override
    public void configure(WebSecurity web) throws Exception {
        // Configuration static files do not require authentication 
        web.ignoring().antMatchers("/static/**");
    }

The reference to the page is as follows :

 <link rel="stylesheet" th:href="@{static/layui/css/layui.css}">

Then we start the project : notice css It didn't work

  It's just through spring security Configuration is not enough , We need to rewrite it addResourceHandlers Method to map static resources , This method should be familiar , We go through springboot This will be used when adding interceptors .

Write a class WebMvcConfig Inherit WebMvcConfigurationSupport, Be careful spring boot2 Version and 1 The version is different ,spring boot1 Version inherited WebMvcConfigurerAdapter stay spring boot2 It's out of date in the version

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     *  Configuring static resources 
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }
}

Now restart the project :css The file has been referenced successfully .

 3.3  ErrorPageConfig  Configuration error page

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description  Configuration error page  403 404 500   Apply to  SpringBoot 2.x
 */
@Configuration
public class ErrorPageConfig {

    @Bean
    public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
        WebServerFactoryCustomizer<ConfigurableWebServerFactory> webCustomizer = new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
            @Override
            public void customize(ConfigurableWebServerFactory factory) {
                ErrorPage[] errorPages = new ErrorPage[] {
                        new ErrorPage(HttpStatus.FORBIDDEN, "/403"),
                        new ErrorPage(HttpStatus.NOT_FOUND, "/404"),
                        new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"),
                };
                factory.addErrorPages(errorPages);
            }
        };
        return webCustomizer;
    }
}

 3.4 MainController controller

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description  Main controller 
 */
@Controller
public class MainController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/login")
    public String loginPage(){
        System.out.println("login page");
        return "login";
    }
    @GetMapping("/index")
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public String index(){
        System.out.println("index page");
        return "index";
    }


    @GetMapping("/admin")
    @PreAuthorize("hasAnyRole('ADMIN')")
    public String printAdmin(){
        System.out.println("hello admin");
        return "admin";
    }

    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public String printUser(){
        System.out.println("hello user");
        return "user";
    }

    /**
     *  Page not found 
     */
    @GetMapping("/404")
    public String notFoundPage() {
        return "/error/404";
    }
    /**
     *  unauthorized 
     */
    @GetMapping("/403")
    public String accessError() {
        return "/error/403";
    }
    /**
     *  Server error 
     */
    @GetMapping("/500")
    public String internalError() {
        return "/error/500";
    }
}

3.5 UserInfoController  User controller

/**
 * @Author qt
 * @Date 2021/3/25
 * @Description
 */
@Controller
@RequestMapping("/user")
public class UserInfoController {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private UserInfoService userInfoService;

    @GetMapping("/getUserInfo")
    @ResponseBody
    public User getUserInfo(@RequestParam String username){
        return userInfoService.getUserInfoByUsername(username);
    }
}

SMM The rest of the frame is omitted , Not here .

3.6 CustomAccessDecisionManager Custom permission decision Manager

/**
 * @Author qt
 * @Date 2021/3/31
 * @Description  Custom permission decision Manager 
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    /**
     * @Author: qt
     * @Description:  Take the current user's permission and this request url Compare the required permissions , Decide whether to release 
     * auth  Contains current user information , Include permissions , Before UserDetailsService User objects stored at login time 
     * object  Namely FilterInvocation object , You can get request etc. web resources .
     * configAttributes  Permission required for this access . That's the last step  MyFilterInvocationSecurityMetadataSource  The list of permissions obtained by query and check in 
     **/
    @Override
    public void decide(Authentication auth, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        while (iterator.hasNext()) {
            if (auth == null) {
                throw new AccessDeniedException(" Current access does not have permission ");
            }
            ConfigAttribute ca = iterator.next();
            // Permissions required for the current request 
            String needRole = ca.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (auth instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException(" Not logged in ");
                } else
                    return;
            }
            // Permissions of the current user 
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException(" Insufficient authority !");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

3.7 CustomLogoutSuccessHandler Log off processing

/**
 * @Author qt
 * @Date 2021/3/31
 * @Description  Log off processing 
 */
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println(" Logout successful !");
        // Here is the logic of your successful login 
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(" Logout successful !");
    }
}

3.8 LoginFailureHandler Login failure handling

/**
 * @Author qt
 * @Date 2021/3/24
 * @Description  Login failure handling 
 */
@Component("loginFailureHandler")
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info(" Login failed ");
        this.saveException(request, exception);
        this.getRedirectStrategy().sendRedirect(request, response, "/login?error=true");
    }
}

 3.9 LoginSuccessHandler  Login successfully processed

/** @Author qt 
* @Date 2021/3/24 * @Description Login successfully processed
*/ @Component("loginSuccessHandler") public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Resource private ObjectMapper objectMapper; private RequestCache requestCache; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // Get all the parameters passed from the front end to the back end Enumeration enu = request.getParameterNames(); while (enu.hasMoreElements()) { String paraName = (String) enu.nextElement(); System.out.println(" Parameters - " + paraName + " : " + request.getParameter(paraName)); } logger.info(" Login authentication successful "); // Here is the logic of your successful login , You can verify other information , Such as verification code .
response.setContentType("application/json;charset=UTF-8"); JSONObject resultObj = new JSONObject(); resultObj.put("code", HttpStatus.OK.value()); resultObj.put("msg"," Login successful "); resultObj.put("authentication",objectMapper.writeValueAsString(authentication)); response.getWriter().write(resultObj.toString()); this.getRedirectStrategy().sendRedirect(request, response, "/index");// Redirect } }

3.10 login.html The login page

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> Sign in </title>
    <link rel="stylesheet" type="text/css" th:href="@{static/layui/css/layui.css}">
</head>
<body>
<form method="POST" th:action="@{/user/login}">
    <div>
         user name :<input type="text" name="username" id="username">
    </div>
    <div>
         password :<input type="password" name="password" id="password">
    </div>
    <div>
         <button type="submit"> Log in now </button>
    </div>
    <!--  The following is to display the prompt information such as authentication failure (th:if="" Be sure to write  )-->
    <span style="color: red;" th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
</form>
</body>
</html>

 3.11 Effect picture

Login failed

 

  Login successful

 4、 Customize ajax Request authentication login

I prefer to use ajax How to login and authenticate , This is more flexible .

   4.1  Catalog

   4.2、 Compared with the change of form login authentication

  LoginFailureHandler  Login failure handling

/**
 * @Author qt
 * @Date 2021/3/24
 * @Description  Login failure handling 
 */
@Component("loginFailureHandler")
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info(" Login failed ");
        response.setContentType("application/json;charset=UTF-8");
        // Here is the logic of your login failure , Can add verification code verification, etc 
        String errorInfo = "";
        if (exception instanceof BadCredentialsException ||
                exception instanceof UsernameNotFoundException) {
            errorInfo = " Wrong account name or password !";
        } else if (exception instanceof LockedException) {
            errorInfo = " The account is locked , Please contact the Administrator !";
        } else if (exception instanceof CredentialsExpiredException) {
            errorInfo = " Password expired , Please contact the Administrator !";
        } else if (exception instanceof AccountExpiredException) {
            errorInfo = " Account expired , Please contact the Administrator !";
        } else if (exception instanceof DisabledException) {
            errorInfo = " Account is disabled , Please contact the Administrator !";
        } else {
            errorInfo = " Login failed !";
        }
        logger.info(" Login failure reason :" + errorInfo);
        //ajax Request authentication method 
        JSONObject resultObj = new JSONObject();
        resultObj.put("code", HttpStatus.UNAUTHORIZED.value());
        resultObj.put("msg",errorInfo);
        resultObj.put("exception",objectMapper.writeValueAsString(exception));
        response.getWriter().write(resultObj.toString());

        // Form authentication 
        //this.saveException(request, exception);
        //this.getRedirectStrategy().sendRedirect(request, response, "/login?error=true");
    }
}
LoginSuccessHandler  Login successfully processed 
/**
 * @Author qt
 * @Date 2021/3/24
 * @Description  Login successfully processed 
 */
@Component("loginSuccessHandler")
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        response.setContentType("application/json;charset=UTF-8");
        //  Get all the parameters passed from the front end to the back end 
          Enumeration enu = request.getParameterNames();
          while (enu.hasMoreElements()) {
              String paraName = (String) enu.nextElement(); System.out.println(" Parameters - " + paraName + " : " + request.getParameter(paraName));
          }
        logger.info(" Login authentication successful ");
        // Here is the logic of your successful login , Can add verification code verification, etc 

        //ajax Request authentication method 
        JSONObject resultObj = new JSONObject();
        resultObj.put("code", HttpStatus.OK.value());
        resultObj.put("msg"," Login successful ");
        resultObj.put("authentication",objectMapper.writeValueAsString(authentication));
        response.getWriter().write(resultObj.toString());

        // Form authentication 
        //this.getRedirectStrategy().sendRedirect(request, response, "/index");// Redirect 
    }
}

login.html The login page

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title> Sign in </title>
    <link rel="stylesheet" type="text/css" th:href="@{static/layui/css/layui.css}">
</head>
<body>
<form method="POST" action="">
    <div>
         user name :<input type="text" name="username" id="username">
    </div>
    <div>
         password :<input type="password" name="password" id="password">
    </div>
    <div>
        <input type="button" name="login" id="login" th:value=" Log in now " onclick="mylogin()">
    </div>
</form>

<script type="text/javascript" charset="utf-8" th:src="@{static/jquery/jquery-3.5.1.min.js}"></script>
<script type="text/javascript" charset="utf-8" th:src="@{static/layui/layui.js}"></script>
<script th:inline="javascript" type="text/javascript">
    layui.use(['form','jquery','layedit', 'laydate'], function () {
        var $ = layui.jquery,
            form = layui.form,
            layer = layui.layer;
    });
    function mylogin() {
        var username = $("#username").val();
        var password = $("#password").val();
        console.log("username:" + username + "password:" + password);
        var index = layer.load(1);
        $.ajax({
            type: "POST",
            dataType: "json",
            url: "user/login",
            data: {
                "username": username,
                "password": password
                // Can add verification code parameters, etc , Background login processing LoginSuccessHandler These parameters are passed in 
            },
            success: function (data) {
                layer.close(index);
                console.log("data===>:" + JSON.stringify(data));
                if (data.code == 200) { // Login successful 
                    window.location.href = "index";
                } else {
                    layer.msg(data.msg, {
                        icon: 2,
                        time: 3000 //2 Seconds off ( If you don't configure , The default is 3 second )
                    });
                }
            },
            error: function () {
                layer.close(index);
                layer.msg(" Data request exception !", {
                    icon: 2,
                    time: 2000 //2 Seconds off ( If you don't configure , The default is 3 second )
                });
            }
        });
    }
</script>
</body>
</html>

4.3  Demo picture

Login failed

  Login successful

  Finally, add a little one I wrote demo, It's also integrated security frame , Use springboot + ssm Back end framework + maven Dependency package management + thmeleaf template engine + pear-admin-layui Front end frame, etc .

 demo Demo address :http://www.qnto.top/springfashionsys/login

 demo Only the data analysis page is set with permissions , Only admin To access .

Reprint need to add links , Sorting is not easy to .

summary : Practice is the only criterion for testing truth , Close test available .

  Reference link :

 https://blog.csdn.net/qq_40298902/article/details/106433192

 https://www.e-learn.cn/topic/3143567

 https://blog.csdn.net/qq_20108595/article/details/89647419

 http://www.spring4all.com/article/428

 https://blog.csdn.net/tanleijin/article/details/100698486

 https://blog.csdn.net/zhaoxichen_10/article/details/88713799

 https://blog.csdn.net/hanxiaotongtong/article/details/103095906

 https://www.jb51.net/article/140429.htm

 https://www.jianshu.com/p/29d10ad22531

 https://blog.csdn.net/weixin_39588542/article/details/110507502

 https://blog.csdn.net/sinat_33151213/article/details/89931819

版权声明
本文为[QianTLL]所创,转载请带上原文链接,感谢
https://cdmana.com/2021/04/20210408114553591d.html

Scroll to Top