零、前言
对于 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 和 password
loginPage()
:用来自定义登录页面
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()
:是否允许账号再次登录,默认为 false
expiredUrl()
:设置用户被挤下线后,导致会话过期跳转的 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
过滤器认证流程