SpringSecurity基础配置

1,721 阅读9分钟

~~我正在参加「掘金·启航计划」~~

零、前言

对于web项目,项目安全一直都是重中之重,老牌的项目安全框架为shiro,但是随着spring以及spring boot的兴起,spring security也变得越来越常见,先对spring secruity进行简要介绍。

依赖引入,下文未特殊说明均使用此版本,引入的spring-security版本为5.7.1

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

一、新旧版本配置方式

1、旧版本配置

在旧版本中,我们通过继承WebSecurityConfigurerAdapter来进行配置,类似配置如下:

 /**
  * Spring Security配置
  *
  * SpringSecurity支持三种注解设置权限方式,对应EnableGlobalMethodSecurity注解的三个属性:
  *          prePostEnabled、securedEnabled、jsr250Enabled
  *      可单独开启,也可同时开启多个
  *
  */
 @Configuration
 //@EnableGlobalMethodSecurity(prePostEnabled = true)
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 ​
     @Resource
     private UserDetailsService userDetailsService;
 ​
     @Autowired
     private AuthenticationSuccessHandler successHandler;
 ​
     @Autowired
     private AuthenticationFailureHandler failureHandler;
 ​
     @Bean
     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
     }
 ​
     @Bean
     public AuthenticationProvider authenticationProvider() {
         DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
         authenticationProvider.setUserDetailsService(userDetailsService);
         authenticationProvider.setPasswordEncoder(passwordEncoder());
         // 配置UserNotFoundException正常抛出,而不是被BadCredentialsException替换
         authenticationProvider.setHideUserNotFoundExceptions(false);
         return authenticationProvider;
     }
 ​
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
         // 直接装配userDetailsService
         auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
 ​
         // 手动装配 AuthenticationProvider
 //        auth.authenticationProvider(authenticationProvider());
     }
 ​
     // 把默认的角色前缀`ROLE_`修改为`AA`
     @Bean
     public GrantedAuthorityDefaults grantedAuthorityDefaults() {
         return new GrantedAuthorityDefaults("AA");
     }
     
     @Override
     public void configure(WebSecurity web) throws Exception {
         web.ignoring().antMatchers("/", "/index", "/error.html");
     }
 ​
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.csrf().disable();
         http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
         http.authorizeRequests()
                 .mvcMatchers("/t/admin").hasRole("ADMIN")
                 .antMatchers("/t/USER").hasAuthority("ROLE_USER")
                 .antMatchers("/t/read").hasRole("READ")
                 .antMatchers("/t/test1/**").permitAll()
                 .anyRequest().authenticated()
                 .and().formLogin()
                 .usernameParameter("myUsername").passwordParameter("myPassword")
                 .loginPage("/login.html").loginProcessingUrl("/login")
                 .successForwardUrl("/successForwardUrl")
                 .defaultSuccessUrl("/one")
                 .defaultSuccessUrl("/one", true)
                 .successHandler(successHandler)
                 .failureUrl("/error.html")
                 .failureHandler(failureHandler)
                 .and().rememberMe()
                 .userDetailsService(userDetailsService)
                 .tokenValiditySeconds(1200)
                 .and()
                 .sessionManagement()
                 .invalidSessionUrl("/one")
                 .maximumSessions(1)
                 .maxSessionsPreventsLogin(false)
                 .expiredUrl("/")
                 .and()
                 .and()
                 .logout().logoutSuccessUrl("/index")
         ;
     }
 }

2、新版本配置

在新版本(>=5.4)中,我们可以直接配置SecurityFilterChain,从而简化配置:

 @Configuration
 public class NewSecurityConfig {
 ​
     @Autowired
     private AuthenticationSuccessHandler successHandler;
 ​
     @Autowired
     private AuthenticationFailureHandler failureHandler;
 ​
     @Bean
     public PasswordEncoder passwordEncoder() {
         return new BCryptPasswordEncoder();
     }
 ​
     @Bean
     public GrantedAuthorityDefaults grantedAuthorityDefaults() {
         return new GrantedAuthorityDefaults("AA");
     }
 ​
     @Bean
     public WebSecurityCustomizer webSecurityCustomizer() {
         // 不推荐使用,此类方式会直接跳过认证和授权。
         return web -> web.ignoring().antMatchers("/", "/index", "/error.html");
     }
 ​
     @Bean
     public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http.csrf().disable();
         http
                 .formLogin(login -> login.successHandler(successHandler)
                         .defaultSuccessUrl("/", false)
                         .failureUrl("/error.html")
                         .failureHandler(failureHandler)
                 )
                 .authorizeRequests(authorize -> authorize
                         .mvcMatchers("/t/admin").hasRole("ADMIN")
                         .mvcMatchers("/t/USER").hasAuthority("ROLE_USER")
                         .mvcMatchers("/t/read").hasRole("READ")
                         .mvcMatchers("/t/test1/**").authenticated()
                         .antMatchers("/", "/index", "/error.html").permitAll()  // 推荐使用此方式配置白名单
                         .anyRequest().authenticated()
                 )
                 .logout(
                         logout -> logout.logoutSuccessUrl("/index")
                         .addLogoutHandler((request, response, authentication) -> {
                             System.out.println(request.getMethod());
                             System.out.println("===========LogoutHandler============");
                         })
                 )
         ;
         return http.build();
     }
 }

二、配置详情

从上面可以看出,不管是旧版写法还是新版写法,配置的核心都是Filter Chain的配置,下面我们来详细看下。

不管哪种方式,配置方法都接受一个HttpSecurity类型的参数,我们就是基于此来进行配置。

1、配置url请求路径权限

方式:http.authorizeHttpRequests()

通过mvcMatchers(url)antMatchers(url)匹配url,然后通过hasRole()方法指定角色或通过hasAuthority()方法指定权限,如下所示:

PS:通过hasRole()方法指定角色时,会自动加上默认的角色前缀ROLE_,如何取消、修改该角色前缀,详见后文

 http.authorizeHttpRequests()
     .mvcMatchers("/t/admin").hasRole("ADMIN")           // 访问 /t/admin 请求需要具有ADMIN角色,即ROLE_ADMIN权限
     .antMatchers("/t/USER").hasAuthority("ROLE_USER")   // 访问 /t/USER 请求需要具有ROLE_USER权限
     .antMatchers("/t/read").hasRole("READ")             // 访问 /t/read 请求需要具有ADMIN角色,即ROLE_ADMIN权限
     .antMatchers("/t/test1/**").permitAll()             // 访问 /t/test1/ 路径下的所有请求不需要认证,可以直接访问
     .anyRequest().authenticated()                       // 访问剩余的请求,都需要认证

所有的请求配置方法:

请求配置方法说明
access(String)如果给定的SpEL表达式计算结果为true,就允许访问
anonymous()允许匿名用户访问
authenticated()允许认证过的用户访问
denyAll()无条件拒绝访问
permitAll()无条件允许访问
hasAuthority(String)如果用户具备给定权限的话,就允许访问
hasAnyAuthority(String...)如果用户具备给定权限中的某一个的话,就允许访问
hasRole(String)如果用户具备给定角色的话,就允许访问
hasAnyRole(String...)如果用户具备给定角色中的某一个的话,就允许访问

2、认证配置

我们都知道,当我们引入spring security依赖后,什么都没有配置,但是访问请求时,就会跳转登录页,这是为什么呢?

这是因为spring security的默认配置导致的,这个默认配置启用了表单配置,所有就跳转到了登录页。

默认配置等价于如下的显示配置:

 http.authorizeHttpRequests()
     .anyReqest().authenticated()    // 配置所有请求都需要认证,即登录
     .and().formLogin()              // 配置通过表单认证
     .and().httpBasic()              // 配置基础认证
     ;

很明显,默认配置太简单,不符合我们的要求。

2.1 自定义表单登录:

 http.formLogin()
     .usernameParameter("myUsername").passwordParameter("myPassword")    // 自定义请求参数的用户名和密码的名称
     .loginPage("/login.html").loginProcessingUrl("/login")  // 设置自定义登录页面和登录接口
     .successForwardUrl("/successForwardUrl")    // 登录成功转发,因为login为post请求,这里重定向的请求也必须为post,否则报错
     .defaultSuccessUrl("/one")  // 默认的登录成功页面,为重定向操作,对请求无要求
     .defaultSuccessUrl("/one", true)
     .successHandler(successHandler) 
     .failureUrl("/error.html")  //设置登录失败错误页面
     .failureHandler(failureHandler) 
     ;

说明:

  1. usernameParameter()passwordParameter():用来自定义登录请求参数的用户名和密码的名称,默认的用户名和密码时username和password
  2. loginPage():用来自定义登录页面
  3. loginProcessingUrl():用来定义登录接口,默认为/login
  4. successForwardUrl():用来定义登录成功后的请求转发地址。由于是请求转发,且登录接口(这里时/login)为post请求,故该方法配置的接口也必须为post请求
  5. defaultSuccessUrl():用来定义登录成功后的请求地址,功能上和successForwardUrl()一样,都是定义登录成功后的请求,但是该方法是重定向,故这里配置的请求可以是任何类型的请求
  6. successHandler():用来定义登录成功后的操作逻辑
  7. failureUrl():用来定义登录失败后的请求地址
  8. failureHandler():用来定义登录失败后的操作逻辑

2.2 successForwardUrl()defaultSuccessUrl()的区别

1、successForwardUrl(url):无论何种情况(直接访问登录页面登录,还是访问指定页面跳转登录页面登录),登录成功后都会转发到设置的页面(请求方式必须与login的请求方式一样,为POST)

2、defaultSuccessUrl(url):只有直接访问登录页面,登录成功后才重定向到设置的页面,如果访问的是指定页面,因为没登录而重定向到登录页面,那么登录成功后会重定向到访问的页面

3、defaultSuccessUrl(url, true):第二个参数设置为true,表示总是使用该url,在功能上与successForwardUrl一样,唯一的区别是这里是重定向而不是转发,故url的请求方式不用于login的请求方式相同,为POST

2.3 successForwardUrl()defaultSuccessUrl()successHandler()的配置优先级

结论:后面的结置优先级高于前面配置的,也就是说后面的配置会覆盖前面的配置

也就是说,当同时定义successForwardUrl()defaultSuccessUrl()successHandler()时,最后一个配置生效。

我们来看先几个方法的源码:

  1. successForwardUrl()方法:很明显,该方法就是构造一个ForwardAuthenticationSuccessHandler对象,然后调用successHandler()方法

     public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
         successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
         return this;
     }
    
  2. defaultSuccessUrl()方法:该方法也是先构造SavedRequestAwareAuthenticationSuccessHandler对象,然后调用successHandler()方法

     public final T defaultSuccessUrl(String defaultSuccessUrl) {
         return defaultSuccessUrl(defaultSuccessUrl, false);
     }
     ​
     public final T defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) {
         SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();
         handler.setDefaultTargetUrl(defaultSuccessUrl);
         handler.setAlwaysUseDefaultTargetUrl(alwaysUse);
         this.defaultSuccessHandler = handler;
         return successHandler(handler);
     }
    
  3. successHandler()方法:设置successHandler变量

 public final T successHandler(AuthenticationSuccessHandler successHandler) {
     this.successHandler = successHandler;
     return getSelf();
 }

三个方法源码一看就很明了了,三者都是通过构造AuthenticationSuccessHandler对象来实现相关功能,因此后声明的AuthenticationSuccessHandler会覆盖掉前面声明的,故只有最后一个配置才是有效的

同理,failureUrl()failureHandler()的优先级也如此。

3、RememberMe配置

 http.rememberMe()
     .userDetailsService(userDetailsService)
     .tokenValiditySeconds(1200) // 指定token有效期,单位:秒,默认两周
     ;

说明:

  1. userDetailsService():指定remember-me时使用的UserDetailsService。如果未指定则使用{@link AuthenticationManagerBuilder#defaultUserDetailsService}的值

    • 如果configure(AuthenticationManagerBuilder auth)方法中通过auth.userDetailsService()进行配置,则可以不显示指定,
    • 若是通过auth.authenticationProvider()手动注入UserDetailsService,则这里必须指定,否则报错;因为此方式不设置{@link AuthenticationManagerBuilder#defaultUserDetailsService}的值,defaultUserDetailsService为null,使用时就报错:UserDetailsService is required.
  2. tokenValiditySeconds():指定token有效期,即RemberMe的有效期,单位:秒,默认两周

4、Session会话配置

 http.sessionManagement()
     .invalidSessionUrl("/one")          // 会话过期跳转url,可通过yml文件或properties文件配置过期时间
     .maximumSessions(1)                 // 最大会话数,即同时一个账号能同时登录几次
     .maxSessionsPreventsLogin(false)    // 是否允许账号再次登录,默认为false
     .expiredUrl("/")                    // 设置用户被挤下线后,导致会话过期跳转的url
     ;

说明:

  1. invalidSessionUrl():会话过期跳转url,可通过yml文件或properties文件配置过期时间
  2. maximumSessions():最大会话数,即同时一个账号能同时登录几次
  3. maxSessionsPreventsLogin():是否允许账号再次登录,默认为false
  4. expiredUrl():设置用户被挤下线后,导致会话过期跳转的url

5、Logout退出配置

 http.logout()
     .logoutUrl("/logout")
     .logoutSuccessUrl("/index")
     .logoutSuccessHandler(logoutSuccessHandler)
     .deleteCookies("JSESSIONID")
     ;

说明:

  1. logoutUrl():退出登录接口,默认为/logout
  2. logoutSuccessUrl():退出登录成功接口,默认为/login?logout
  3. logoutSuccessHandler():退出成功操作,该配置会使logoutSuccessUrl()配置失效。和配置先后无关
  4. deleteCookies():配置退出时要删除的cookie

三、扩展知识

1、删除/修改默认角色前缀

1.1 配置方法

配置方法很简单,执行定义一个GrantedAuthorityDefaults类型的Bean接口,其构造函数参数即为配置的角色前缀,如果为空串"",则删除了角色前缀,如下,将默认的角色前缀ROLE_修改为AA

 @Bean
 public GrantedAuthorityDefaults grantedAuthorityDefaults() {
     return new GrantedAuthorityDefaults("AA");
 }

1、只有在spring security 5.6.0版本及之后版本通过authorizeHttpRequests()配置请求权限时才生效

2、spring-security 5.6.0版本之前仅适用于方法级别的权限控制:

即配置类开启@EnableGlobalMethodSecurity(prePostEnabled = true),方法上添加@PreAuthorize("hasAnyRole('ADMIN')")等注解

1.2 源码分析

1)spring security 5.5.8

 <dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-config</artifactId>
     <version>5.5.8</version>
 </dependency>

hasRole()方法源码:

 private static String hasAnyRole(String... authorities) {
     String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','ROLE_");
     return "hasAnyRole('ROLE_" + anyAuthorities + "')";
 }
 ​
 private static String hasRole(String role) {
     Assert.notNull(role, "role cannot be null");
     Assert.isTrue(!role.startsWith("ROLE_"),
                   () -> "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
     return "hasRole('ROLE_" + role + "')";
 }

可以看到,该版本的ROLE_前缀是直接写死在代码中的,不允许配置

2)spring security 5.6.0

 <dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-config</artifactId>
     <version>5.6.0</version>
 </dependency>

hasRole()方法源码:

 public ExpressionInterceptUrlRegistry hasRole(String role) {
     return access(ExpressionUrlAuthorizationConfigurer
                   .hasRole(ExpressionUrlAuthorizationConfigurer.this.rolePrefix, role));
 }

可以看到,role前缀的值取的是rolePrefix的值,那我们看下rolePrefix是怎么来的?

是在ExpressionUrlAuthorizationConfigurer构造器中初始化的

 public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
     // 1、从spring上下文中获取所有GrantedAuthorityDefaults类型的bean
     String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
     // 2、如果有且只有一个该类型的bean,则使用该bean设置的rolePrefix
     if (grantedAuthorityDefaultsBeanNames.length == 1) {
         GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(grantedAuthorityDefaultsBeanNames[0],
                                                                             GrantedAuthorityDefaults.class);
         this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
     }
     // 如果没有,则使用默认的ROLE_,如果设置了多个,也不知道用哪个,也使用默认值ROLE_
     else {
         this.rolePrefix = "ROLE_";
     }
     this.REGISTRY = new ExpressionInterceptUrlRegistry(context);
 }

2、authorizeRequests()authorizeHttpRequests()方法的区别

1、区别

  1. authorizeRequests()authorizeHttpRequests()方法分别对应两种Security Filter,前者对应FilterSecurityInterceptor,后者对应AuthorizationFilter
  2. 当使用authorizeRequests()时,spring security将FilterSecurityInterceptor视为Secyrity Filters放入SecurityFilterChain
  3. 当使用authorizeHttpRequests()时,spring security将AuthorizationFilter视为Secyrity Filters放入SecurityFilterChain

AuthorizationFilter正在逐步取代FilterSecurityInterceptor,故spring security官方推荐使用authorizeHttpRequests()进行请求权限配置。不过为了保持兼容,默认的Secyrity Filters还是FilterSecurityInterceptor

2、放上spring security官网上两者的认证流程图:

图一:FilterSecurityInterceptor过滤器认证流程

image-20221008230223497.png

图二:AuthorizationFilter过滤器认证流程

image-20221008230253760.png