• Spring Security 自定义资源认证规则


    自定义资源认证规则

    在这里插入图片描述

    自定义资源权限规则

    • /index公共资源
    • /hello受保护资源

    在项目中添加如下配置就可以实现对资源权限规则设定:

    创建一个配置类,继承WebSecurityConfigurerAdapter,重写其中的configure(HttpSecurity http)方法,最终在类上使用@Configuration.

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/index")
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    # 说明
    - permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
    - anyRequest().authenticated()代表所有请求,必须认证之后才能访问
    - formLogin() 代表开启表单认证
    ## 注意:放行资源必须放在所有认证请求之前!
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.自定义登录界面

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    .mvcMatchers("/loginHtml").permitAll()  //请求路径,即Controller路径,放行自定义登录界面
                    .mvcMatchers("/index").permitAll()   //请求路径/index放行
                    .anyRequest().authenticated()   //其他的任何请求都要进行认证
                    .and()
                    .formLogin() //表单验证
                    .loginPage("/loginHtml") //默认登录页面,也是通过请求进行跳转至默认登录页面
                    .loginProcessingUrl("/doLogin")    //指定发送过来的/doLogin请求被捕获,进行权限验证。
                    .usernameParameter("uname") //默认接收username参数,修改为uname
                    .passwordParameter("passwd") //默认接收password参数,修改为passwd
    //                .successForwardUrl("/index")   //认证成功 forward跳转路径 地址栏不变  每次都默认跳转到/index
                    .defaultSuccessUrl("/index",true)   //认证成功之后的跳转  重定向
                    // 如果之前保存了请求但是被拦截,在拦截之后先跳转到被保存的请求,如果没有,则跳转到指定的请求。
                    // 如果第二个参数设置为true,则无论如何,只要认证成功,不管有没有保存的请求直接跳转到指定的请求。
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    在这里插入图片描述

    需要注意的是

    • 登录表单method必须为post。

    • antion的请求路径与配置类中的loginProcessingUrl()一致。

    • 用户名密码的参数也需要与配置类中的usernameParameter()、passwordParameter()一致。

    • successForwardUrl、defaultSuccessUrl这个两个方法都可以实现成功之后跳转

      • successForwardUrl默认使用forward跳转 注意:不会跳转到之前请求路径
      • defaultSuccessUrl默认使用redirect跳转 注意:如果之前请求路径,会优先跳转之前请求路径,可以传入第二个参数进行修改。

    2.自定义登录成功处理

    有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。只需要给前端返回一个JSON通知登录成功还是失败与否。这个时候可以通过自定义AuthenticationSuccessHandler实现

    public interface AuthenticationSuccessHandler {
    	/**
    	 * Called when a user has been successfully authenticated.
    	 * @param request the request which caused the successful authentication
    	 * @param response the response
    	 * @param authentication the Authentication object which was created during
    	 * the authentication process.
    	 */
    	void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    			Authentication authentication) throws IOException, ServletException;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    根据接口的描述信息,也可以得知登录成功会自动回调这个方法,进一步查看它的默认类型,发现successForwardUrl、defaultSuccessUrl也是由它的子类实现的。

    在这里插入图片描述

    • 自定义AuthenticationSuccessHandler实现
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String,Object> result = new HashMap<String,Object>();
            result.put("msg","登录成功");
            result.put("status",200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 配置AuthenticationSuccessHandler
    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    //...
                    .and()
                    .formLogin() //表单验证
    				//...
    				//自定义登录成功处理
                    .successHandler(new MyAuthenticationSuccessHandler())
                    .failureForwardUrl("/loginHtml")
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    3.显示登录失败信息

    为了能够更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security在登录失败之后会将异常信息存储到request,session作用域中key为SPRING_SECURITY_LAST_EXCEPTION命名属性中。

    源码可以参考:SimpleUrlAuthenticationFailureHandler

    • 显示异常信息

    在这里插入图片描述

    • 配置
    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
                    //...
                    .and()
                    .formLogin() //表单验证
    				//...
                  //.failureForwardUrl("/loginHtml") //认证失败之后的forward跳转    forward  -->异常信息存放在request
                    .failureUrl("/loginHtml")  //  默认  认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    4.自定义登录失败处理

    和自定义登录成功处理一样,Spring Security同样为前后端分离开发提供了登录失败的处理,这个类就是AuthenticationFailureHandler。

    源码:

    public interface AuthenticationFailureHandler {
    
    	/**
    	 * Called when an authentication attempt fails.
    	 * @param request the request during which the authentication attempt occurred.
    	 * @param response the response.
    	 * @param exception the exception which was thrown to reject the authentication
    	 * request.
    	 */
    	void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
    			AuthenticationException exception) throws IOException, ServletException;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    根据接口的描述信息,也可以得知登录失败会自动回调这个方法,进一步查看它的默认实现,发现failureUrl、failureForwardUrl也是由它的子类实现的。

    在这里插入图片描述

    • 自定义AuthenticationFailureHandler实现
    /*
    * 自定义登录失败解决方案
    * */
    public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            Map<String,Object> result = new HashMap<>();
            result.put("msg","登录失败:"+exception.getMessage());
            result.put("status",500);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    配置AuthenticationFailureHandler

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
    				//...
                    .and()
                    .formLogin() //表单验证
    				//...
                   .successHandler(new MyAuthenticationSuccessHandler()) //用来自定义认证成功之后处理 前后端分离解决方案
    //             .failureForwardUrl("/loginHtml") //认证失败之后的forward跳转    forward  -->异常信息存放在request
    //             .failureUrl("/loginHtml")  //  默认  认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
                  .failureHandler(new MyAuthenticationFailureHandler()) //用来自定义认证失败之后处理  前后端分离解决方案
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    5.注销登录

    Spring Security中提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。

    • 开启注销登录 默认开启

      @Configuration
      public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.authorizeHttpRequests()
      				//...
                      .and()
                      .formLogin() //表单验证
      				//...
                      .and()
                      .logout()   //开启注销登录  默认
                      .logoutUrl("/logout")  //指定注销登录 url  默认 默认请求方式:GET
                      .invalidateHttpSession(true) //默认 会话失效
                      .clearAuthentication(true)  //默认 清楚认证标记
                      .logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
                      .and()
                      .csrf().disable();   //禁止 csrf 跨域请求保护
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 通过logout()方法注销配置
      • logoutUrl指定退出登录请求地址,默认是GET请求,路径为/logout
      • invalidateHttpSession退出时是否是session失效,默认值为true
      • clearAuthentication退出时是否清楚认证信息,默认值为true
      • logoutSuccessUrl退出登录时跳转地址
    • 配置多个注销登录请求

      如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:

      @Configuration
      public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.authorizeHttpRequests()
      				//...
                      .and()
                      .formLogin() //表单验证
      				//...
                      .and()
                      .logout()   //开启注销登录  默认
                      .logoutRequestMatcher(new OrRequestMatcher(
                              new AntPathRequestMatcher("/aa","GET"),
                              new AntPathRequestMatcher("/bb","POST")
                      ))      //
                      .invalidateHttpSession(true) //默认 会话失效
                      .clearAuthentication(true)  //默认 清楚认证标记
                      .logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
                      .and()
                      .csrf().disable();   //禁止 csrf 跨域请求保护
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
    • 前后端分离注销登录配置

    如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义LogoutSuccessHandler实现来返回内容注销之后信息:

    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            Map<String,Object> result = new HashMap<String,Object>();
            result.put("msg","注销成功");
            result.put("status",200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(result);
            response.getWriter().println(s);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    配置

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
    				//...
                    .and()
                    .formLogin() //表单验证
    				//...
                    .and()
                    .logout()   //开启注销登录  默认
                    .logoutRequestMatcher(new OrRequestMatcher(
                            new AntPathRequestMatcher("/aa","GET"),
                            new AntPathRequestMatcher("/bb","POST")
                    ))      //
                    .invalidateHttpSession(true) //默认 会话失效
                    .clearAuthentication(true)  //默认 清楚认证标记
                    .logoutSuccessHandler(new MyLogoutSuccessHandler())  //注销登录成功之后处理
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    6.登录用户数据获取

    6.1.SecurityContextHolder

    ​ Spring Security会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolder中。

    ​ SecurityContextHolder中的数据保存默认是通过ThreadLocal来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。以后每当有请求到来时,Spring Security就会先从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。

    实际那个SecurityContextHolder中存储的是SecurityContext,在SecurityContext中存储是Authentication。

    在这里插入图片描述

    这种设计是典型的策略设计模式:

    public class SecurityContextHolder {
    
    	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    
    	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    
    	public static final String MODE_GLOBAL = "MODE_GLOBAL";
    
    	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
    
    	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    
    	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
    
    	private static SecurityContextHolderStrategy strategy;
    
    	//...
    
    	private static void initializeStrategy() {
    		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
    			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
    					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
    			return;
    		}
    		if (!StringUtils.hasText(strategyName)) {
    			// Set default
    			strategyName = MODE_THREADLOCAL;
    		}
    		if (strategyName.equals(MODE_THREADLOCAL)) {
    			strategy = new ThreadLocalSecurityContextHolderStrategy();
    			return;
    		}
    		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
    			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
    			return;
    		}
    		if (strategyName.equals(MODE_GLOBAL)) {
    			strategy = new GlobalSecurityContextHolderStrategy();
    			return;
    		}
    		// Try to load a custom strategy
    		try {
    			Class<?> clazz = Class.forName(strategyName);
    			Constructor<?> customStrategy = clazz.getConstructor();
    			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
    		}
    		catch (Exception ex) {
    			ReflectionUtils.handleReflectionException(ex);
    		}
    	}
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    1. MODE THREADLOCAL:这种存放策略是将SecurityContext存放在ThreadLocal中,Threadlocal的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter到达Servlet,都是由一个线程来处理。这也是SecurityContextHolder的默认存储测录额,这种存储策略以为着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到
    2. MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中ue能够获取到登录用户数据,那么可以使用这种存储模式。
    3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在JavaWeb开发中,这种模式很少使用到。

    6.2.SecurityContextHolderStrategy

    通过SecurityContextHolder可以得知,SecurityContextHolderStrategy接口用来定义存储策略方法

    public interface SecurityContextHolderStrategy {
    
    	void clearContext();
    
    	SecurityContext getContext();
    
    	void setContext(SecurityContext context);
    
    	SecurityContext createEmptyContext();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    接口中一共定义了四个方法:

    • clearContext:该方法用来清除存储的SecurityContext对象。
    • getContext:该方法用来获取存储的SecurityContext对象。
    • setContext:该方法用来设置存储的SecurityContext对象。
    • create Empty Context:该方法则用来创建一个空的SecurityContext对象。

    在这里插入图片描述

    从上面可以看出每一个实现类对应一种策略的实现。

    在这里插入图片描述

    6.3.代码中获取认证之后用户数据

    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello(){
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            User user = (User)authentication.getPrincipal();
            System.out.println("身份信息:"+user.getUsername());
            System.out.println("权限信息:"+authentication.getAuthorities());
    
            new Thread(()->{
                Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
                System.out.println("子线程:"+authentication1);
    
            }).start();
            System.out.println("hello security");
            return "hello spring security";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    6.4.多线程情况下获取用户数据

    从源码中可以知道,该策略可以从系统参数中获取,因此覆盖系统中的SYSTEM_PROPERTY参数即可。

    在这里插入图片描述

    -Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
    
    • 1

    在这里插入图片描述

    因此在启动项目是设置系统参数即可。

    在这里插入图片描述

    6.5.页面上获取用户信息

    • 引入依赖
            <dependency>
                <groupId>org.thymeleaf.extrasgroupId>
                <artifactId>thymeleaf-extras-springsecurity5artifactId>
                <version>3.0.4.RELEASEversion>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 页面加入命名空间
    <html lang="en" xmlns:th="https://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    
    • 1
    • 2
    • 页面中使用
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    7.自定义认证数据源

    7.1.认证流程分析

    在这里插入图片描述

    • 发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter拦截
    • UsernamePasswordAuthenticationFilterattemptAuthentication方法中将请求中用户名和密码,封装为Authentication对象,并交给AuthenticationManager进行认证
    • 认证成功,将认证信息存储到SecurityContextHolder以及调用RememberMe记住我的操作,并回调AuthenticationSuccessHandler处理
    • 认证失败,清除SecurityContextHolder以及记住我中的信息,回调AuthenticationFailureHandler处理

    7.2.三者关系

    AuthenticationManager是认真的核心类,但实际上在底层真正认证时还离不开ProviderManager以及AuthenticationProvider。他们三者关系是怎么样的呢?

    • AuthenticationManager是一个认证管理器,它定义了Spring Security过滤器要执行认证操作。
    • ProviderManager是AuthenticationManager接口的实现类。Spring Security认证时默认使用就是ProviderManager。
    • AuthenticationProvider就是针对不同的身份类型执行的具体的身份认证。

    AuthenticationManager与ProviderManager

    在这里插入图片描述

    ProviderManager是AuthenticationManager的唯一实现,也是Spring Security默认使用实现。从这里不难看出默认情况下

    AuthenticationManager就是一个ProviderManager。

    ProviderManager与AuthenticationProvider

    https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

    在这里插入图片描述

    ​ 在Spring Security中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe认证、手机号码动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider来提供。

    ​ 多个AuthenticationProvider将组成一个列表,这个列表将由ProviderManager代理。换句话说,在ProviderManager中存在一个AuthenticationProvider列表,在ProviderManager中遍历列表中的每一个AuthenticationProvider去执行身份认证,最终得到认证结果。

    ​ ProviderManager本身也可以再配置一个AuthenticationManager作为parent,这样当ProviderManager认证失败之后,就可以进入到parent中再次进行认证。理论上来说,ProviderManager的parent可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager来扮演parent的角色,也就是ProviderManager是ProviderManager的parent。

    ​ ProviderManager本身也可以有多个,多个ProviderManager共用同一个parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级,然后,父级是一种全局资源,作为所有提供者的后备资源。

    在这里插入图片描述

    弄清楚认证原理之后我们来看下具体认证时数据源的获取。默认情况下,AuthenticationProvider是由DaoAuthenticationProvider类来实现认证的,在DaoAuthenticationProvider认证时又通过UserDetailsService完成数据源的校验。他们之间调用关系如下:

    在这里插入图片描述

    总结:AuthenticationManager是认证管理器,在Spring Security中有全局AuthenticationManager,也可以有AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManager进行实现。每一个ProviderManager中都代理一个AuthenticationProvier的列表,列表中每一个实现代表一种身份的认证方式。认证时底层数据源需要调用UserDetailService来实现。

    7.3.配置全局AuthenticationManager

    https://spring.io/guides/topical/spring-security-architecture

    • 默认的全局AuthenticationManager
        //方法名字自定义
    	@Autowired
        public void initialize(AuthenticationManagerBuilder builder) throws Exception {
           System.out.println("springboot 默认配置:" + builder);
          InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
            userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
            builder.userDetailsService(userDetailsService);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
        //方法名字自定义(上述为一个示例)
    	@Autowired
        public void initialize(AuthenticationManagerBuilder builder) throws Exception {
    		//builder...
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总结:1.默认自动配置全局AuthenticationManager 默认找当前项目中是否存在自定义UserDetailService实例,自动将当前项目UserDetailService实例设置为数据源

    2.默认自动配置创建全局AuthenticationManager在工厂中使用时直接在代码中注入即可。

    Springboot对security进行自动配置时自动在工厂中创建一个全局AuthenticationManager。

    • 自定义全局AuthenticationManager
    //    自定义AuthenticationManager
        @Override
        public void configure(AuthenticationManagerBuilder builder) throws Exception {
            System.out.println("自定义AuthenticationManager:" + builder);
            InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
            userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
            builder.userDetailsService(userDetailsService);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    //    自定义AuthenticationManager(上述为一个实例)
        @Override
        public void configure(AuthenticationManagerBuilder builder) throws Exception {
    		//...
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总结:1.一旦通过configure方法自定义AuthenticationManager实现 就会将工厂中自动配置Authentication进行覆盖

    2.一旦通过configure方法自定义AuthenticationManager实现 需要在实现中指定认证数据源对象UserDetailService实例

    3.一旦通过configure方法自定义AuthenticationManager实现 这种方式创建AuthenticationManager对象,工厂内部创建一个本地AuthenticationManager对象,不允许在其他自定义组件中进行注入。

    • 用来在工厂中暴露自定义AuthenticationManager实例
    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Bean
        public UserDetailsService userDetailsService(){
            InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
            userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
            return userDetailsService;
        }
    
    
    
    //    自定义AuthenticationManager  并没有在工厂中暴露出来
        @Override
        public void configure(AuthenticationManagerBuilder builder) throws Exception {
            System.out.println("自定义AuthenticationManager:" + builder);
            builder.userDetailsService(userDetailsService());
        }
    
        //作用:用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    7.4.自定义数据库数据源

    设计表结构

    -- 用户表
    CREATE TABLE `user`
    (
    	`id` int(11) NOT NULL AUTO_INCREMENT,
    	`username` varchar(32) DEFAULT NULL,
    	`password` varchar(255) DEFAULT NULL,
    	`enable` tinyint(1) DEFAULT NULL,
    	`accountNonExpired` tinyint(1) DEFAULT NULL,
    	`accountNonLocked` tinyint(1) DEFAULT NULL,
    	`credentialsNonExpired` tinyint(1) DEFAULT NULL,
    	PRIMARY KEY(`id`)
    )ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
    
    -- 角色表
    CREATE TABLE `role`
    (
    	`id` int(11) NOT NULL AUTO_INCREMENT,
    	`name` varchar(32) DEFAULT NULL,
    	`name_zh` varchar(32) DEFAULT NULL,
    	PRIMARY KEY(`id`)
    )ENGINE=InnoDB Auto_INCREMENT=4 DEFAULT CHARSET=utf8;
    
    -- 用户角色关系表
    CREATE TABLE `user_role`
    (
    	`id` int(11) NOT NULL AUTO_INCREMENT,
    	`uid` int(11) DEFAULT NULL,
    	`rid` int(11) DEFAULT NULL,
    	PRIMARY KEY(`id`),
    	KEY `uid`(`uid`),
    	KEY `rid`(`rid`)
    )ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    插入测试数据

    -- 插入用户数据
    BEGIN;
    	INSERT INTO `user`
    	values(1,'root','{noop}123',1,1,1,1);
    	INSERT INTO `user`
    	values(2,'admin','{noop}123',1,1,1,1);
    	INSERT INTO `user`
    	values(3,'blr','{noop}123',1,1,1,1);
    COMMIT;
    -- 插入角色数据
    BEGIN;
    	INSERT INTO `role`
    	values(1,'ROLE_product','商品管理员');
    	INSERT INTO `role`
    	values(2,'ROLE_admin','系统管理员');
    	INSERT INTO `role`
    	values(3,'ROLE_user','用户管理员');
    COMMIT;
    -- 插入用户角色数据
    BEGIN
    	INSERT INTO `user_role`
    	values(1,1,1);
    	INSERT INTO `user_role`
    	values(2,1,2);
    	INSERT INTO `user_role`
    	values(3,2,2);
    	INSERT INTO `user_role`
    	values(4,3,3);
    COMMIT;
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    引入依赖

            
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>1.2.8version>
            dependency>
    
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>5.1.38version>
            dependency>
    
            
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>2.2.0version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    配置springboot配置文件

    #设置thymeleaf 缓存
    spring.thymeleaf.cache=false
    #配置数据源
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=root
    
    #mybatis配置
    #注意mapper目录必须使用"/"
    mybatis.mapper-locations=classpath:com/zkt/mapper/*.xml
    mybatis.type-aliases-package=com.zkt.entity
    
    #日志处理  为了展示mysql运行 sql语句
    logging.level.com.zkt=debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    创建entity

    ​ user对象

    public class User implements UserDetails {
    
        private Integer id;
        private String username;
        private String password;
        private Boolean enabled; //账户是否激活
        private Boolean accountNonExpired; //账号是否过期
        private Boolean accountNonLocked; //账户是否被锁定
        private Boolean credentialsNonExpired;  //密码是否过期
        private List<Role> roles = new ArrayList<>();   //关系属性 用来存储当前用户所有角色信息
    
    
    
        //返回权限信息
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
    
            Set<SimpleGrantedAuthority> authorities = new HashSet<>();
            roles.forEach(role->{
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
                authorities.add(simpleGrantedAuthority);
    
            });
    
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public void setEnabled(Boolean enabled) {
            this.enabled = enabled;
        }
    
        public void setAccountNonExpired(Boolean accountNonExpired) {
            this.accountNonExpired = accountNonExpired;
        }
    
        public void setAccountNonLocked(Boolean accountNonLocked) {
            this.accountNonLocked = accountNonLocked;
        }
    
        public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
            this.credentialsNonExpired = credentialsNonExpired;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
        public Integer getId() {
            return id;
        }
    
        public List<Role> getRoles() {
            return roles;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    role对象

    public class Role {
        private Integer id;
        private String name;
        private String nameZh;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getNameZh() {
            return nameZh;
        }
    
        public void setNameZh(String nameZh) {
            this.nameZh = nameZh;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    编写Dao类

    @Mapper
    public interface UserDao {
    
        //提供根据用户名返回用户方法
        User loadUserByUsername(String username);
    
        //提供根据用户id查询用户角色信息方法
        List<Role> getRolesByUid(Integer uid);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    mapper

    
    DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.zkt.dao.UserDao">
        
        
        <select id="loadUserByUsername" resultType="User">
            select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpired
            from user
            where username = #{username}
        select>
    
    
        <select id="getRolesByUid" resultType="Role">
            select r.id,r.name,r.name_zh from role r, user_role ur
            where r.id = ur.rid
            and ur.rid = #{uid}
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    自定义UserDetailsService

    @Component
    public class MyUserDetailService implements UserDetailsService {
    
    
        private final UserDao userDao;
        //dao ==>springboot+mybatis
        @Autowired
        public MyUserDetailService(UserDao userDao){
            this.userDao = userDao;
        }
    
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //        1.查询用户
            User user = userDao.loadUserByUsername(username);
            if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确~");
    //        2.查询权限信息
            List<Role> roles = userDao.getRolesByUid(user.getId());
            user.setRoles(roles);
            return user;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    配置authenticationManager使用自定义UserDetailService

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        private final MyUserDetailService myUserDetailService;
    
        @Autowired
        public WebSecurityConfigurer(MyUserDetailService myUserDetailService) {
            this.myUserDetailService = myUserDetailService;
        }
    
    
    //    自定义AuthenticationManager  并没有在工厂中暴露出来
        @Override
        public void configure(AuthenticationManagerBuilder builder) throws Exception {
            System.out.println("自定义AuthenticationManager:" + builder);
    //        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
    //        userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
            builder.userDetailsService(myUserDetailService);
        }
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    		//...
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    测试

    使用账号 root 密码123进行登录 登录成功。

  • 相关阅读:
    安防监控视频AI智能分析网关:人流量统计算法的应用场景汇总
    网络编程的学习
    docker和Helm Chart的基本命令和操作
    Win32API操作文件
    【11.14】Codeforces 刷题
    【Python 实战基础】如何绘制饼状图分析商品库存
    平衡小车调车保姆式教程
    自动 ARIMA 超参数搜索
    JMeter 4.0 如何获取cookie
    C++ 关联式容器map+set
  • 原文地址:https://blog.csdn.net/Littewood/article/details/125892937