Spring Security安全配置

使用Spring Boot与Spring MVC进行Web开发时,如果项目引入spring-boot- starter-security依赖启动器,MVC Security 安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。其中,SecurityAutoConfiguration会导入并自动化配置SpringBootWebSecurityConfiguration用于启动Web安全管理,UserDetailsServiceAutoConfiguration则用于配置用户身份信息。

通过自定义WebSecurityConfigurerAdapter 类型的Bean组件,可以完全关闭Security 提供的Web应用默认安全配置,但是不会关闭UserDetailsService用户信息自动配置类。如果要关闭UserDetailsService默认用户信息配置,可以自定义UserDetailsService、AuthenticationProvider或AuthenticationManager类型的Bean组件。另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件覆盖默认访问规则。Spring Boot 提供了非常多方便的方法,可用于覆盖请求映射和静态资源的访问规则。

下面我们通过Spring Security API查看WebSecurityConfigurerAdapter的主要方法,具体所示:

configure(AuthenticationManagerBuilder auth):定制用户认证管理器来实现用户认证

configure(HttpSecurity http):定制基于HTTP请求的用户访问控制

从上面可以看到,WebSecurityConfigurerAdapter类中包含两个非常重要的配置方法,分别是configure(AuthenticationManagerBuilder auth)和configure(HttpSecurity http)。Configure(AuthenticationManagerBuilder auth)方法用于构建认证管理器,configure(HttpSecurity http)方法用于对基于HTTP的请求进行请求访问控制。

一、自定义用户认证:

通过自定义WebSecurityConfigurerAdapter类型的Bean组件,并重写configure(AuthenticationManagerBuilder auth)方法,可以自定义用户认证。针对自定义用户认证,Spring Security提供了多种自定义认证方式, 包括有: In-Memory Authentication (内存 身份认证)、JDBCAuthentication ( JDBC身份认证)、LDAP Authentication ( LDAP身份认证)、AuthenticationProvider (身份认证提供商)和UserDetailsService (身份详情服务)。 下面我们选取其中3个比较有代表性的方式讲解如何实现自定义用户认证。

二、自定义用户授权管理:

当一个系统建立之后,通常需要适当地做一些权限控制,使得不同用户具有不同的权限操作系统。例如,一般的项目中都会做一些简单的登录控制,只有特定用户才能登录访问。接下来我们针对Web应用中常见的自定义用户授权管理进行介绍。

自定义用户访问控制:

实际生产中,网站访问多是基于HTTP请求的,上面MVC Security 安全配置介绍时,我们已经分析出通过重写WebSecurityConfigurerAdapter类的configure(HttpSecurity http)方法可以对基于HTTP的请求访问进行控制。下面我们通过对configure(HttpSecurity http)方法剖析,分析自定义用户访问控制的实现过程。

configure(HttpSecurity http)方法的参数类型是HttpSecurity类,HttpSecurity类提供了Http请求的限制以及权限、Session 管理配置、CSRF跨站请求问题等方法,具体如下所示。

HttpSecurity 类的主要方法及说明:

authorizeRequests():开启基于HtpSereleRequest请求访问的限制

formLogin():开启基于表单的用户登录

httpBasic():开启基于HTTP请求的Basic认证登录

logout():开启退出登录的支持

sessionManagement():开启Session管理配置

rememberMe():开启记住我功能

csrf():配置CSRF跨站请求伪造防护功能

此处重点讲解用户访问控制,这里先对authorizeRequests()方法的返回值做进一步查看,其中涉及用户访问控制的主要方法及说明如下所示。

antMatchers(java.lang.String .. antPatterns):开启Ant风格的路径匹配

mvcMatchers(java.lang. String ... patterns):开启MVC风格的路径匹配(与Ant风格类似)

regexMatchers(java.lang String... regexPatterns):开启正则表达式的路径匹配

and():功能连接符

anyRequest():匹配任何请求

rememberMe():开启记住我功能

access(String attribute):匹配给定的SpEL表达式计算结果是否为true

hasAnyRole(String .. roles):匹配用户是否有参数中的任意角色

hasRole(String role):匹配用户是否有某一个角色

hasAnyAuthority(String .. authorities):匹配用户是否有参数中的任意权限

hasAuthority(String authority):匹配用户是否有某一个权限

authenticated():匹配已经登录认证的用户

fullyAuthenticated():匹配完整登录认证的用户(非rememberMe登录用户)

haslpAddress(String ipaddressExpression):匹配某IP地址的访问请求

permitAll():无条件对请求进行放行

上面列举了用户请求访问的常用方法,如果想了解更多方法可以通过查看API文档。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 授权
        http.authorizeRequests()
                .antMatchers(
                        "/user/setting",
                        "/user/upload",
                        "/discuss/add",
                        "/comment/add/**",
                        "/letter/**",
                        "/notice/**",
                        "/like",
                        "/follow",
                        "/unfollow"
                )
                .hasAnyAuthority(
                        "AUTHORITY_USER",
                        "AUTHORITY_ADMIN",
                        "AUTHORITY_MODERATOR"
                )
                .antMatchers(
                        "/discuss/top",
                        "/discuss/wonderful"
                )
                .hasRole("AUTHORITY_USER")
                .anyRequest().permitAll()
                .and()
                .formLogin();
    }
}

自定义用户登录:

formLogin()是开启基于表单的用户登录,所以下面就围绕formLogin()这个方法来探索并讲解自定义登录的具体实现。

用户登录相关的主要方法及说明如下所示。

loginPage(String loginPage):用户登录页面跳转路径,默认为get请求的/login

successForwardUrl(String forwardUrl):用户登录成功后的重定向地址

successHandler(AuthenticationSuccessHandler successHandler):用户登录成功后的处理

defaultSuccessUrl(String defaultSuccessUrl):用户直接登录后默认跳转地址

failureForwardUrl(String forwardUrl):用户登录失败后的重定向地址

failureUrl(String authenticationFailureUrl):用户登录失败后的跳转地址,默认为/login?error

failureHandler(AuthenticationF ailureHandler authenticationF ailureHandler):用户登录失败后的错误处理

usernameParameter(String usernameParameter):登录用户的用户名参数,默认为username

passwordParameter(String passwordParameter):登录用户的密码参数,默认为password

loginProcessingUrl(String loginProcessingUr):登录表单提交的路径,默认为post请求的login

permitAIl():无条件对请求进行放行

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name").passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");
    }
}

自定义用户退出:

自定义用户退出主要考虑退出后的会话如何管理以及跳转到哪个页面。HttpSecurity 类的logout()方法用来处理用户退出,它默认处理路径为“logout”的Post类型请求,同时也会清除Session和“Remember Me”(记住我)等任何默认用户配置。下面我们就围绕logout)这个方法来探索并讲解自定义用户退出的具体实现。

logout()方法中涉及用户退出的主要方法及说明如下所示。

logoutUrI(String logoutUrl):用户退出处理控制URL,默认为post请求的logout

logoutSuccessUrl(String logoutSuccessUrI) :用户退出成功后的重定向地址

logoutSuccessHandler(LogoutSuccessHandle logoutSuccessHandler):用户退出成功后的处理器设置

deleteCookies(String ... cookieNamesToClear):用户退出后删除指定Cookie

invalidateHttpSession(boolean invalidateHttpSession):用户退出后是否立即清除Session (默认为true )

clearAuthentication(boolean clearAuthentication):用户退出后是否立即清除Authentication用户认证信息(默认为true)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户退出控制
        http.logout()
                .logoutUrlUrl("/mylogout")
                .logoutSuccessUrl("/");
    }
}

记住我功能:

在实际开发中,有些项目为了用户登录方便还会提供记住我( Remember-Me )功能。如果用户登录时勾选了记住我选项,那么在一段有效时间内,会默认自动登录,并允许访问相关页面,这就免去了重复登录操作的麻烦。Spring Security提供了用户登录控制的同时,当然也提供了对应的记住我功能,前面介绍的HttpSecurity类的主要方法rememberMe()就是Spring Security 用来处理记住我功能的。下面我们围绕rememberMe()这个方法来探索并讲解记住我功能的具体实现。

rememberMe()记住我功能相关涉及记住我的主要方法及说明如下所示。

rememberMeParameter(String rememberMeParameter):指示在登录时记住用户的HTTP参数

key(String key):记住我认证生成的Token令牌标识

tokenValiditySeconds(int tokenValiditySeconds):记住我Token令牌有效期,单位为s (秒)

tokenRepository(PersistentTokenRepository tokenRepository):指定要使用的PersistentTokenRepository,用来配置持久化Token令牌

alwaysRemember(boolean alwaysRemember):是否应该始终创建记住我Cookie,默认为false

clearAuthentication(boolean clearAuthentication):是否设置Cookie为安全的,如果设置为true,则必须通过HTTPS进行连接请求

需要说明的是,Spring Security 针对记住我功能提供了两种实现:一种是简单地使用加密来保证基于Cookie中Token的安全;另一种是通过数据库或其他持久化机制来保存生成的Token。下面我们分别对这两种记住我功能的实现进行讲解并演示说明。

基于简单加密Token 的方式:

基于简单加密Token的方式实现记住我功能非常简单,当用户选择记住我并成功登录后,Spring Security将会生成一个Cookie并发送给客户端浏览器。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 定制Remember-Me记住我功能
        http.rememberMe()
                .rememberMeParameter("rememberme")
                .tokenValiditySeconds(200);
    }
}

基于持久化Token的方式:

持久化Token的方式与简单加密Token的方式在实现Remember -Me功能上大体相同,都是在用户选择[记住我]并成功登录后,将生成的Token存入Cookie中并发送到客户端浏览器,在下次用户通过同一客户端访问系统时,系统将直接从客户端Cookie中读取Token进行认证。两者的主要区别在于:基于简单加密Token的方式,生成的Token将在客户端保存一段时间,如果用户不退出登录,或者不修改密码,那么在Cookie失效之前,任何人都可以无限制地使用该Token进行自动登录;而基于持久化Token的方式采用如下实现逻辑。

  1. 用户选择[记住我]成功登录后,Security会把username、随机产生的序列号、生成的Token进行持久化存储(例如一个数据表中),同时将它们的组合生成一个 Cookie发送给客户端浏览器。
  2. 当用户再次访问系统时,首先检查客户端携带的Cookie,如果对应Cookie 中包含的username、序列号和Token与数据库中保存的一致,则通过验证并自动登录,同时系统将重新生成一个新的Token替换数据库中旧的Token,并将新的Cookie再次发送给客户端。
  3. 如果Cookie中的Token不匹配,则很有可能是用户的Cookie被盗用了。由于盗用者使用初次生成的Token进行登录时会生成一个新的 Token,所以当用户在不知情时再次登录就会出现Token不匹配的情况,这时就需要重新登录,并生成新的Token和Cookie。同时Spring Security就可以发现Cookie可能被盗用的情况,它将删除数据库中与当前用户相关的所有Token记录,这样盗用者使用原有的Cookie将不能再次登录。
  4. 如果用户访问系统时没有携带Cookie, 或者包含的username和序列号与数据库中保存的不-致,那么将会引导用户到登录页面。

从以上实现逻辑可以看出,持久化Token的方式比简单加密Token的方式相对更加安全。使用简单加密Token的方式,一旦用户的Cookie被盗用,在Token有效期内,盗用者可以无限制地自动登录进行恶意操作,直到用户本人发现并修改密码才会避免这种问题;而使用持久化Token的方式相对安全,用户每登录一次都会生成新的Token和Cookie,但也给盗用者留下了在用户进行第2次登录前进行恶意操作的机会,只有在用户进行第2次登录并更新Token和Cookie时,才会避免这种问题。因此,总体来讲,对于安全性要求很高的应用,不推荐使用Remember -Me功能。

CSRF防护功能:

CSRF (Cross-site request forgery,跨站请求伪造),也被称为“One Click Attack”(一键攻击)或者”Session Riding” (会话控制),通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。与传统的XSS攻击( Cross-site Scriping,跨站脚本攻击)相比,CSRF攻击更加难以防范,被认为比XSS更具危险性。CSRF攻击可以在受害者毫不知情的情况下以受害者的名义伪造请求发送给攻击页面,从而在用户未授权的情况下执行在权限保护之下的操作。

例如,一个用户Tom登录银行站点服务器准备进行转账操作,在此用户信息有效期内,Tom

被诱导查看了一个黑客恶意网站,该网站就会获取到Tom登录后的浏览器与银行网站之间尚未过期的Session信息,而Tom浏览器的Cookie中含有Tom银行账户的认证信息,此时黑客就会伪装成Tom认证后的合法用户对银行账户进行非法操作。

在讨论如何抵御CSRF攻击之前,先要明确CSRF攻击的对象,也就是要保护的对象。从上

面的例子可知,CSRF 攻击是黑客借助受害者的Cookie骗取服务器的信任,但是黑客并不能获取Cookie,也看不到Cookie 的具体内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客无法进行解析。黑客所能做的就是伪造正常用户给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。因此,针对CSRF攻击要保护的对象是那些可以直接产生数据变化的服务,而对于读取数据的服务,可以不进行CSRF保护。例如,银行转账操作会改变账号金额,需要进行CSRF保护。获取银行卡等级信息是读取操作,不会改变数据,可以不需要保护。在业界目前防御CSRF攻击主要有以下3种策略。

(1)验证HTTP Referer字段。

(2)在请求地址中添加Token并验证。

(3)在HTTP头中自定义属性并验证。

Spring Security安全框架提供了CSRF防御相关方法,具体如下所示。

disable():关闭Security默认开启的CSRF防御功能

csrfTokenRepository(CsrfTokenRepositor csrfTokenRepository):指定要使用的CsrfTokenRepository ( Token令牌持久化仓库)。默认是由LazyCsrfTokenRepository包装的HttpSessionCsrfTokenRepository

rgurerProtetionatchereqestMatcher requireCSsrfProtectionMatcher):指定针对什么类型的请求应用CSRF防护功能。默认设置是忽略GET、HEAD、TRACE和OPTIONS请求,而处理并防御其他所有请求

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 可以关闭Spring Security默认开启的CSRF防护功能
        http.csrf().disable();;
    }
}