~~我正在参加「掘金·启航计划」~~
零、前言
对于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)
;
说明:
usernameParameter()
、passwordParameter()
:用来自定义登录请求参数的用户名和密码的名称,默认的用户名和密码时username和passwordloginPage()
:用来自定义登录页面loginProcessingUrl()
:用来定义登录接口,默认为/login
successForwardUrl()
:用来定义登录成功后的请求转发地址。由于是请求转发,且登录接口(这里时/login
)为post
请求,故该方法配置的接口也必须为post
请求defaultSuccessUrl()
:用来定义登录成功后的请求地址,功能上和successForwardUrl()
一样,都是定义登录成功后的请求,但是该方法是重定向,故这里配置的请求可以是任何类型的请求successHandler()
:用来定义登录成功后的操作逻辑failureUrl()
:用来定义登录失败后的请求地址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()
时,最后一个配置生效。
我们来看先几个方法的源码:
-
successForwardUrl()
方法:很明显,该方法就是构造一个ForwardAuthenticationSuccessHandler
对象,然后调用successHandler()
方法public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) { successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl)); return this; }
-
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); }
-
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有效期,单位:秒,默认两周
;
说明:
-
userDetailsService()
:指定remember-me
时使用的UserDetailsService
。如果未指定则使用{@link AuthenticationManagerBuilder#defaultUserDetailsService}
的值- 如果
configure(AuthenticationManagerBuilder auth)
方法中通过auth.userDetailsService()
进行配置,则可以不显示指定, - 若是通过
auth.authenticationProvider()
手动注入UserDetailsService
,则这里必须指定,否则报错;因为此方式不设置{@link AuthenticationManagerBuilder#defaultUserDetailsService}
的值,defaultUserDetailsService
为null,使用时就报错:UserDetailsService is required.
- 如果
-
tokenValiditySeconds()
:指定token有效期,即RemberMe的有效期,单位:秒,默认两周
4、Session会话配置
http.sessionManagement()
.invalidSessionUrl("/one") // 会话过期跳转url,可通过yml文件或properties文件配置过期时间
.maximumSessions(1) // 最大会话数,即同时一个账号能同时登录几次
.maxSessionsPreventsLogin(false) // 是否允许账号再次登录,默认为false
.expiredUrl("/") // 设置用户被挤下线后,导致会话过期跳转的url
;
说明:
invalidSessionUrl()
:会话过期跳转url,可通过yml文件或properties文件配置过期时间maximumSessions()
:最大会话数,即同时一个账号能同时登录几次maxSessionsPreventsLogin()
:是否允许账号再次登录,默认为falseexpiredUrl()
:设置用户被挤下线后,导致会话过期跳转的url
5、Logout退出配置
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/index")
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
;
说明:
logoutUrl()
:退出登录接口,默认为/logout
logoutSuccessUrl()
:退出登录成功接口,默认为/login?logout
logoutSuccessHandler()
:退出成功操作,该配置会使logoutSuccessUrl()
配置失效。和配置先后无关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、区别
authorizeRequests()
和authorizeHttpRequests()
方法分别对应两种Security Filter
,前者对应FilterSecurityInterceptor
,后者对应AuthorizationFilter
。- 当使用
authorizeRequests()
时,spring security将FilterSecurityInterceptor
视为Secyrity Filters
放入SecurityFilterChain
中 - 当使用
authorizeHttpRequests()
时,spring security将AuthorizationFilter
视为Secyrity Filters
放入SecurityFilterChain
中
AuthorizationFilter
正在逐步取代FilterSecurityInterceptor
,故spring security官方推荐使用authorizeHttpRequests()
进行请求权限配置。不过为了保持兼容,默认的Secyrity Filters
还是FilterSecurityInterceptor
2、放上spring security官网上两者的认证流程图:
图一:FilterSecurityInterceptor
过滤器认证流程
图二:AuthorizationFilter
过滤器认证流程