浅谈 SpringSecurity使用方式——WebSecurityConfigurerAdapter的三大方法(二)

上一章节我们简单介绍了SpringSecurity使用方式及自动配置原理,这一节我们会着重阐述SpringSecurity的配置,并且会基于配置类WebSecurityConfigurerAdapter的三个方法的使用方式及原理向大家介绍

2.4、基于配置类WebSecurityConfigurerAdapter

创建 WebSecurityConfig配置类,继承 WebSecurityConfigurerAdapter抽象类,实现 Spring Security在 Web 场景下的自定义配置。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
     
}

思考: 是否需要添加@EnableWebSecurity注解?

同样的,我们通过spring-boot-starter-security导入了spring-security依赖,在@EnableAutoConfiguration加载组件的时候默认到spring-boot-autoconfigure下的META-INF/spring.factories下去寻找SecurityAutoConfiguration

SecurityAutoConfiguration

在这里插入图片描述

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Spring Security.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Madhura Bhave
 * @since 1.0.0
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({
    
     SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
         SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
    
    
    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher.class)
    public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
    
    
        return new DefaultAuthenticationEventPublisher(publisher);
    }

}

我们来分析一下上面这波源码

SecurityAutoConfiguration

  • @EnableConfigurationProperties(SecurityProperties.class)

    • 在2.2.1中我们埋下过一个伏笔,@ConfigurationProperties(prefix = “spring.security”)当时是没有生效的,但是在SecurityAutoConfiguration就要生效了,因为存在@EnableConfigurationProperties(SecurityProperties.class),这个注解有两个作用,一是将SecurityProperties加入到IOC容器中,二是在SecurityProperties.class中的@ConfigurationProperties(prefix = “spring.security”)生效,也就是对应在application.properties/yml绑定前缀生效
  • @Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
    SecurityDataConfiguration.class })

    • WebSecurityEnablerConfiguration.class就有一个注解@EnableWebSecurity,所以我们就不需要在继承 WebSecurityConfigurerAdapter的类去添加@EnableWebSecurity

      @Configuration(proxyBeanMethods = false)
      @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
      @ConditionalOnClass(EnableWebSecurity.class)
      @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
      @EnableWebSecurity
      class WebSecurityEnablerConfiguration {
              
              
      }
      
2.4.1、认证管理器

重写 configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager认证管理器

采用编码的方式去配置用户名密码和角色,注意第9行代码auth.inMemoryAuthentication()等同于我们使用在application.properties/yml文件中定义的用户名密码的方式是相同(前面有分析过运行原理),我们来分析一下configure(AuthenticationManagerBuilder auth) 到底可以干什么

@Configuration 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
     
    @Override 
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    {
    
     
        String password = passwordEncoder().encode("123456"); 
        auth
            // 使用基于内存的 InMemoryUserDetailsManager 
            .inMemoryAuthentication() 
            //使用 PasswordEncoder 密码编码器 
            //.passwordEncoder(passwordEncoder()) 
            // 配置用户 
            .withUser("fox").password(password).roles("admin") 
            // 配置其他用户 
            .and() 
            .withUser("fox2").password(password).roles("user"); 
    }
    @Bean 
    public PasswordEncoder passwordEncoder(){
    
     
        //return NoOpPasswordEncoder.getInstance(); 
        return new BCryptPasswordEncoder(); 
    } 
}
configure(AuthenticationManagerBuilder auth) 认证管理器

用于通过允许AuthenticationManager容易地添加来建立认证机制,也就是说用来记录账号,密码,角色信息

AuthenticationManagerBuilder.class的两个方法

  • inMemoryAuthentication() 基于内存的认证

    • public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
        throws Exception {
              
              
       return apply(new InMemoryUserDetailsManagerConfigurer<>());
      }
      
  • userDetailsService() 基于编码的认证

    • public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
        T userDetailsService) throws Exception {
              
              
       this.defaultUserDetailsService = userDetailsService;
       return apply(new DaoAuthenticationConfigurer<>(userDetailsService));
      }
      
最佳实践
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
// 让springsecurity走我们自定义的UserDetailsService,定义的密码解析
@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
		auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
// 重写springsecurity的userDetailService中的loadUserByUserName方法
@Bean
@Override
public UserDetailsService userDetailsService() {
    
    
	return username -> {
    
    
	Admin admin = adminService.getAdminByUserName(username);
	if (admin != null) {
    
    
	admin.setRoles(adminService.getRoles(admin.getId()));
	return admin;
}
	// 这是由springsecurity提供的异常处理器
	throw new UsernameNotFoundException("用户名或密码不正确!");
	};
}

@Bean
public PasswordEncoder passwordEncoder() {
    
    
	return new BCryptPasswordEncoder();
	}
}
2.4.2、网络认证器

configure(HttpSecurity http)网络认证器

允许基于选择匹配在资源级配置基于网络的安全性。以下示例将以/ admin /开头的网址限制为具有ADMIN角色的用户,并声明任何其他网址需要成功验证

也就是对角色的权限——所能访问的路径做出限制

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeUrls()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
}

configure(HttpSecurity http)到底能做什么

springsecurity为我们提供了大量可定制的权限配置,我着重列出了以下几个方法的使用方式以及最佳实践

  • LogoutConfigurer logout()
  • CorsConfigurer cors()
  • SessionManagementConfigurer sessionManagement()
  • HttpSecurity authorizeRequests()
  • HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
  • ExceptionHandlingConfigurer exceptionHandling()
logout
  • 作用:销毁HttpSession对象,清除认证数据,设置logout成功跳转路径

  • Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即可。默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout ;当然我们也可以自定义跳转路径

    public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
    		extends AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
          
          
    
    	private List<LogoutHandler> logoutHandlers = new ArrayList<>();
    
    	private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
    
    	private String logoutSuccessUrl = "/login?logout";
    
    	private LogoutSuccessHandler logoutSuccessHandler;
    
    	private String logoutUrl = "/logout";
        
        // 自定义跳转路径
        public LogoutConfigurer<H> logoutUrl(String logoutUrl) {
          
          
    		this.logoutRequestMatcher = null;
    		this.logoutUrl = logoutUrl;
    		return this;
    	}
        // 跳转成功
        public LogoutConfigurer<H> logoutSuccessUrl(String logoutSuccessUrl) {
          
          
    		this.customLogoutSuccess = true;
    		this.logoutSuccessUrl = logoutSuccessUrl;
    		return this;
    	}
    }
    
  • 自定义退出跳转路径

    • http.logout()
          .logoutUrl("/logout") 
          .logoutSuccessUrl("/login.html");
      
    • LogoutSuccessHandler

      退出成功处理器,实现 LogoutSuccessHandler 接口 ,可以自定义退出成功处理逻辑。

      http.
          logoutSuccessHandler(new MyLogoutSuccessHandler());
      
      public LogoutSuccessHandler MyLogoutSuccessHandler() implements LogoutSuccessHandler{
              
              
          @Override
          pubilc void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{
              
              
              // 自定义内容
          }
      }
      
      public interface LogoutSuccessHandler {
              
              
        void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException;
      }
      
    • 最佳实践:现在大多数都流行前后端分离,包括职责的分离,以前有很多需要通过后端逻辑去完成的,现在很大一部分都交给了前端去完成,比如页面的跳转,前后端分离前,javaweb都是通过jsp servlet去完成页面跳转,而在前后端分离以后,后端主要接收前端发起的请求并响应对应格式的JSON数据;

      所以在前端发起的logout登出请求,页面的跳转包括前端保存在web端的缓存数据(Authorization,token认证,当前登录用户数据等)清除,都由前端逻辑去完成,而后端在使用了SpringSecruity安全框架后,只需要清除保存在SecurityContextHolder中的当前登录用户的数据,然后返回success状态码即可

      public LogoutConfigurer<H> logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
              
              
      		this.logoutSuccessUrl = null;
      		this.customLogoutSuccess = true;
      		this.logoutSuccessHandler = logoutSuccessHandler;
      		return this;
      	}
      
      private LogoutSuccessHandler getLogoutSuccessHandler() {
              
              
      		LogoutSuccessHandler handler = this.logoutSuccessHandler;
      		if (handler == null) {
              
              
      			handler = createDefaultSuccessHandler();
      		}
      		return handler;
      	}
      
      // 如果我们不自定义LogOutSuccessHandler,则会默认帮我们创建
      private LogoutSuccessHandler createDefaultSuccessHandler() {
              
              
      		SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
      		urlLogoutHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
      		if (this.defaultLogoutSuccessHandlerMappings.isEmpty()) {
              
              
      			return urlLogoutHandler;
      		}
      		DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(
      				this.defaultLogoutSuccessHandlerMappings);
      		successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
      		return successHandler;
      	}
      
CSRF
  • CSRF(Cross-site request forgery)跨站请求伪造,防止恶意伪造用户请求访问受信任站点的非法请求访问。

  • 什么是跨域?跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。

  • 为什么要禁用CSRF?

    • 从Spring Security 4开始,默认启用CSRF机制,但是与Spring Boot结合在一起,那么实现起来就比较麻烦了,尤其是采用前后端分离式的开发架构后,配置CSRF机制就更困难了
  • CSRF导致的接口访问问题

    • Could not verify the provided CSRF token because your session was not found in spring security
      

      在开发人员使用Postman调试接口时,已经通过继承WebSecurityConfigurerAdapter过滤了Rest接口拦截的机制,但出现403错误并且提示信息为“Could not verify the provided CSRF token because your session was not found in spring security”。

    • 解决方法

      @Configuration
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
              
              
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              
              
              http
                  // 使用jwt不需要csrf,关闭csrf保护功能(跨域访问)
                  .csrf().disable();
          }
      }
      
sessionManagement
  • 会话控制

    我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

    机制 描述
    always 如果session不存在总是需要创建
    ifRequired 如果需要就创建一个session(默认)登录时
    never Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那
    么Spring Security将会使用它
    stateless Spring Security将绝对不会创建session,无状态架构适用于REST API
  • 最佳实践:前后端分离我们一般会使用无状态的会话协议 ,基于token的鉴权机制类似于http协议是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。所以我们要禁用session

    @Override
    protected void configure(HttpSecurity http) throws Exception {
          
          
        // 使用jwt不需要csrf
        http.// 基于token不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    
authorizeRequests

请求授权

  • 在使用spring Security时,需要注意authorizeRequests的顺序,如果是以下这种情况的话:

     @Override
        protected void configure(HttpSecurity http) throws Exception {
          
          
         http.authorizeRequests()
                 .anyRequest().authenticated()
                 .antMatchers("/hello").permitAll();
        }
    

    由于是按照从上往下顺序依次执行,如上所示,当我们访问/hello时,会发现此时仍然需要登录!

    所以我们往往会把.anyRequest().authenticated()放在最后

     @Override
        protected void configure(HttpSecurity http) throws Exception {
          
          
         http.authorizeRequests()
                 .antMatchers("/hello").hasRole("USER")
                 .antMatchers("/hi").hasRole("ADMIN")
        		.antMatchers("/sayHello").permitAll()
                 .anyRequest().authenticated();
        }
    

    如上就表示。访问/hello接口需要USER角色,访问/hi需要ADMIN角色,访问其他接口需要认证

addFilterBefore

顾名思义就是在指定过滤器之前添加过滤器

@Override
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) {
    
    
 return addFilterAtOffsetOf(filter, -1, beforeFilter);
}

private HttpSecurity addFilterAtOffsetOf(Filter filter, int offset, Class<? extends Filter> registeredFilter) {
    
    
		int order = this.filterOrders.getOrder(registeredFilter) + offset;
		this.filters.add(new OrderedFilter(filter, order));
		return this;
	}
  • 我们可以来分析一下上面这段源码,addFilterBefore(参数a,参数b),参数a是我们要添加到过滤器链中的目标过滤器,参数b是用于确定参数a在过滤器中的定位,a将放置在b之前执行,在通过调用addFilterAtOffsetOf(a,-1,b),this.filterOrders.getOrder(registeredFilter) -1,通过过滤器b获取执行顺序在-1即为过滤器a 的在过滤器链中的执行顺序,并添加到过滤器链中

  • 最佳实践

    由于我们前面通过sessionManagement关闭了session,使用无状态的会话协议的token进行认证,在使用springsecurity安全框架后,用户提交用户名密码后首先会被UsernamePasswordAuthenticationFilter所拦截,但是我们前后端分离后,在前端发出请求的请求头都会携带一个token(Authentication)的kv键值对,我们需要先对这个token进行一个验证,验证后在放行

    // 添加jwt 登录授权过滤器
    http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    
exceptionHandling

用来添加自定义未授权和未登录结果返回

// 添加自定义为授权和未登录结果返回
http.exceptionHandling()
        .accessDeniedHandler(restfulAccessDeniedHandler)
        .authenticationEntryPoint(restAuthorizationEntryPoint);
  • 未授权

    /**
     * 当访问接口没有权限时,自定义返回结果
     */
    @Component
    public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
          
          
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
          
          
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json");
            PrintWriter out = httpServletResponse.getWriter();
            RespBean bean = RespBean.error("权限不足,请联系管理员!");
            bean.setCode(403);
            out.write(new ObjectMapper().writeValueAsString(bean));
            out.flush();
            out.close();
        }
    }
    
  • 未登录

    /**
     * 当未登录或者token失效访问接口时自定义的返回接口
     */
    @Component
    @Slf4j
    public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
          
          
    
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
          
          
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json");
            PrintWriter out = httpServletResponse.getWriter();
            log.info("未登录,请先进行登录");
            RespBean bean = RespBean.error("未登录,请先进行登录");
            bean.setCode(401);
            out.write(new ObjectMapper().writeValueAsString(bean));
            out.flush();
            out.close();
        }
    }
    
2.4.3、全局安全性

configure(WebSecurity)用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。一般用于配置全局的某些通用事物,例如静态资源等

有的时候我们在放行的路径、静态资源太多,可以在configure((HttpSecurity http)中直接拦截所有路径

http
    .authorizeRequests()
    .anyRequest()
    .authenticated();

使用configure(WebSecurity web)放行所有免认证就可以访问的路径,比如注册业务,登录业务,退出业务,静态资源等等

 // 不走拦截链去放行路径
    @Override
    public void configure(WebSecurity web) throws Exception {
    
    
        // 放行静态资源
        web.ignoring().antMatchers(
                "/login",
                "/logout",
                "/css/**",
                "/js/**",
                "/index.html",
                "/favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/captcha",
                "/ws/**"
        );
    }

本人知识浅薄,如果有哪里解析有误,欢迎指教

猜你喜欢

转载自blog.csdn.net/weixin_46195957/article/details/120221655