spring boot 之 spring security 配置

Spring Security简介

以前项目都是用shiro,可是时过境迁,spring security变得愈来愈流行。spring security的前身是Acegi, acegi 我也玩过,那都是5年的事情了! 现在spring security已经发布了不少个版本,已经到了5.x.x 了。其新功能也增长了很多, 咱们来看看吧!html

spring security实际上是独立于 spring boot 的。即便是 spring security 的注解, 也跟boot 关系不大, 那都是他们自带的。可是我这里仍是把他归类为boot,由于我是使用boot来作测试的。 java

 spring security 配置很是灵活,可是正是这种灵活性,是依赖于其底层须要强大的设计,和良好的API 支持, 基本是把复杂性包装了在底层。 spring security提供了所谓的流式API, 也就是能够经过点(.)符号,连续的进行配置。固然,这里的流式API跟java8的流式API仍是不一样的。git

若是咱们运行官方example,咱们发现,挺好的啊, 原来 spring security 这么灵活易用啊! 灵活是没错的,可是是否易用就看状况了, 对熟悉的人,固然易用。对于新手,其实到处是坑! 由于不熟悉的话,基本上只能复制,可是一改动那么就发现各类问题。github

官方示例

怎么配置就很少说了, 网上大把资料。对于security的form 登陆的配置,官方标准关键部分是这样的:web

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // @formatter:off
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
    // @formatter:on

    // @formatter:off
    @Autowired
    public void configureGlobal(
            AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER"));
    }
    // @formatter:on
}

thymeleaf 模板:正则表达式

<html xmlns:th="http://www.thymeleaf.org">
    <head th:include="layout :: head(title=~{::title},links=~{})">
        <title>Please Login</title>
    </head>
    <body th:include="layout :: body" th:with="content=~{::content}">
        <div th:fragment="content">
            <form name="f" th:action="@{/login}" method="post">
                <fieldset>
                    <legend>Please Login</legend>
                    <div th:if="${param.error}" class="alert alert-error">Invalid
                        username and password.</div>
                    <div th:if="${param.logout}" class="alert alert-success">You
                        have been logged out.</div>
                    <label for="username">Username</label> <input type="text"
                        id="username" name="username" /> <label for="password">Password</label>
                    <input type="password" id="password" name="password" /> <label
                        for="remember-me">Remember Me?</label> <input type="checkbox"
                        id="remember-me" name="remember-me" />
                    <div class="form-actions">
                        <button type="submit" class="btn">Log in</button>
                    </div>
                </fieldset>
            </form>
        </div>
    </body>
</html>

 官方github还有不少示例,咱们能够都拉下来看看。但我本文的意图是解释下 spring security的如何配置,以及其各类坑。spring

 

关键概念和API

配置的关键莫过于HttpSecurity ,WebSecurity 和AuthenticationManagerBuilder。关键中的关键是HttpSecurity , 其关键api 有:数据库

servletApi 配置SecurityContextHolderAwareRequestFilter
anonymous 匿名登陆控制
cors 增长CorsFilter,提供 跨域资源共享( CORS )机制。它容许 Web 应用服务器进行跨域访问控制。 这个和 crsf 长得有些像
logout 登出配置
openidLogin 增长OpenIDAuthenticationFilter ,配置外部openid 服务器
addFilterBefore
addFilter
mvcMatcher
exceptionHandling 增长ExceptionTranslationFilter,对认证受权等异常进行处理
formLogin form登入认证配置
sessionManagement 配置session 管理
antMatcher
requiresChannel 增长ChannelProcessingFilter过滤器,也就是安全通道,和https 相关
requestMatcher
userDetailsService 设置用户详情数据
setSharedObject 全局共享数据配置
httpBasic httpBasic基础认证
portMapper 端口映射
authorizeRequests
authenticationProvider 设置authentication提供者
securityContext
rememberMe 增长RememberMeAuthenticationProvider过滤器配置
csrf 增长CsrfFilter过滤器,防止csrf攻击
requestMatchers
regexMatcher
headers 增长HeaderWriterFilter过滤器, 其余它并非拦截过滤做用。而是将一些请求头写到response 响应头中
requestCache 增长RequestCacheAwareFilter过滤器,对request 进行缓存处理
addFilterAt
addFilterAfter
x509 增长X509AuthenticationFilter过滤器,提供x509 认证支持:从X509证书中提取用户名等
jee 增长J2eePreAuthenticatedProcessingFilter过滤器,提供j2ee 认证支持apache

 (大体说明下关键的api,而忽略简单api)api

其中 antMatcher requestMatchers regexMatcher 功能是相似的。 默认httpsecurity 拦截全部的请求, 若是配置了这个以后,那么以后拦截指定的 url, 它和authorizeRequests 返回的ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry 提供的antMatcher 等类似方法的做用是不太同样的, 必定不能搞混! 这个配置呢,经常用于配置多个 httpSecurity,具体参见官方文档。

 

可见,关键在于使用 HttpSecurity 这个API, 我找到一份中文说明,可是又迷失在了茫茫的网络之中了。

 

它提供了不少的接口,简单说一下个人理解:

 

authorizeRequests 返回一个ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry  而后咱们就能够进行对各类url 的进行权限配置。 注意, 它是配置权限的。requestMatchers 配置一个request Mather数组,参数为RequestMatcher 对象,其match 规则自定义。

antMatchers 配置一个request Mather 的 string数组,参数为 ant 路径格式, 直接匹配url。

mvcMatchers 同上,可是用于匹配 @RequestMapping 的value

regexMatchers 同上,可是为正则表达式

anyRequest 匹配任意url,无参。 

上面的各个url 匹配方法都还有一个重载的对一个request method 参数的方法。 注意 这些url 是有顺序的,这个顺序就是他们出现的顺序,必定不要搞错。因此anyRequest 最好是配置到最后面,不然就容易踩坑了。

配置url 匹配规则的同时, 咱们就能够配置其权限,常见的有:

hasAnyRole 是否有(参数数组中的)任一角色
hasRole 是否有某个角色
hasAuthority 是否有某个权限
hasAnyAuthority 是否有(参数数组中的)任一权限
hasIpAddress ip是否匹配参数
permitAll 容许全部状况,即至关于没作任何security限制
denyAll 拒绝全部状况。 这状况比较奇怪, 若是拒绝全部状况的话, 那的存在有什么意义?
anonymous 能够以匿名身份登陆
authenticated 必需要进行身份验证
fullyAuthenticated 进行严格身份验证,即不能使用缓存/cookie之类的
rememberMe 能够cookie 登陆?

这些个权限,其实还好理解,暂很少说。后文再分析。

 

开始登入 login 

spring security提供了不少种登陆的方式, 常见的有 基于Authentication请求头的httpBasic, 基于表单 的 formLogin。 前者是比较少用的,我主要分析下formLogin,formLogin 是提供了一个 FormLoginConfigurer ,其能够配置的部分为:

loginPage 若是是表单登陆,那么至少要一个loginpage 吧,可是这个也不是必须的,若是不配置这个,那么系统自动生成一个。
usernameParameter 
passwordParameter
failureForwardUrl 登陆失败后就回forward到参数指定的url, 这个url
successForwardUrl 登陆成功后forward到参数指定的url 

父类AbstractAuthenticationFilterConfigurer提供的配置的:

permitAll 容许loginPage, loginProcessingUrl, failureUrl 被任何状况访问到

defaultSuccessUrl 登陆成功后默认的url

loginProcessingUrl form 表单应该提交 security框架能够处理的url, 默认是/login
failureHandler 登陆失败处理器
successHandler 登陆成功处理器
failureUrl 登陆失败系统转向的url ,默认是
this.loginPage + "?error"。这个有些坑,由于他默认是没有权限的! 咱们必须给它额外配置一个适当的登陆权限。不然是跳转不过去的。由于会跳转到登陆页面

authenticationDetailsSource

上面的配置大部分是不能重复配置的。固然,也许咱们能够设置多个相同配置,可是其实只有最后一个生效。除此以外,部分功能相近的配置会有覆盖效果。好比 若是配置了failureHandler ,那么 failureUrl 配置就失效了。 successHandler 也是这样。failureHandler 和 failureUrl 是 last config win。

上面的配置都是能够不用配置的,由于他们都有默认值,具体哪些默认值就不说了。须要注意的是 successForwardUrl,若是不配置,那么登陆成功后默认就跳转到 orgiin url , 也就是被跳转至loginPage 前 咱们尝试访问的那个 url。

另外, defaultSuccessUrl的意思有些难以理解,我至今有些疑问,它和successForwardUrl 的区别是? 

 

开始登出 logout

登出比较简单点,咱们使用 httpSecurity提供的 logout()方法便可。它返回一个LogoutConfigurer ,主要的配置有:

addLogoutHandler 增长登出处理器,它和logoutSuccessHandler的区别是它不能forward或redirect request, 可是logoutSuccessHandler能够并且是应该的。
clearAuthentication 
invalidateHttpSession 
logoutUrl 登出url , 默认是/logout, 它能够是一个ant path url 
logoutRequestMatcher 登出url matcher。这个比较有意思,它让咱们能够灵活配置logoutUrl 。 若是说logoutUrl 只是一个ant path url 的话,那么它就能够是多个RequestMatcher。
logoutSuccessUrl 登出成功后跳转的 url
permitAll 容许  logoutSuccessUrl logoutUrl  和logoutRequestMatcher 
deleteCookies 删除cookie
logoutSuccessHandler 登出成功处理器,设置后会把logoutSuccessUrl  置为null
defaultLogoutSuccessHandlerFor 只有logoutSuccessHandler为null 的时候,它才会生效。
permitAll 容许全部

 

其中defaultLogoutSuccessHandlerFor  是比较难理解的,它的定义是这样的:

    public LogoutConfigurer<H> defaultLogoutSuccessHandlerFor(LogoutSuccessHandler handler, RequestMatcher preferredMatcher) {
        Assert.notNull(handler, "handler cannot be null");
        Assert.notNull(preferredMatcher, "preferredMatcher cannot be null");
        this.defaultLogoutSuccessHandlerMappings.put(preferredMatcher, handler);
        return this;
    }

其实就是被添加到了一个map,它能够配置屡次。 我理解是,它应该是和logoutRequestMatcher 配合使用的。 logoutRequestMatcher能够配置多个logout url, defaultLogoutSuccessHandlerFor 恰好能够对那些url 再次作个匹配, 匹配成功后执行对应的LogoutSuccessHandler。若是匹配不到,那么就直接 SimpleUrlLogoutSuccessHandler 直接 sendRedirect 到 logoutSuccessUrl 

 clearAuthentication invalidateHttpSession 我暂时不太理解。 测试过, 没有达到预期, 多是理解错误。

 

记住我 remember-me

 咱们经常须要作登陆入口给提供一个“记住我"的checkbox,以方便用户下次登陆,将用户名直接显示在登陆框,密码显示在密码框,而后咱们能够再也不输入用户密码了! 可是, security的remember-me 功能好像不是这个做用, 我也是醉了!

具体用法是,

1 先在form 表单增长以下内容:

<input type="checkbox" name="remember-me" />

注意,这里的name ,必须是remember-me,而不能是rememberMe,或其余之类的。 不然就不会生效!

2 而后配置一个 UserDetailsService,配置 uds 有多种方式, 以下:

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            UserDetails userDetails = User.withUsername("admin").password("admin").roles("user").build();
            UserDetailsService userDetailsService = new InMemoryUserDetailsManager(Collections.singleton(userDetails));// 简单起见,这里使用内存方式
            auth.userDetailsService(userDetailsService);
        }

另外,咱们还能够经过@Bean方式来配置。 至于为何须要一个uds, 那是由于, security须要调用 它的 loadUserByUsername 方法, 而后返回一个Authentication , 而后就能够不用输入用户密码了。 

3 而后httpsecurity的最后:

                    .and()
                    .rememberMe() // 这个做用是配置一个 RememberMeAuthenticationFilter ,必须

 

官方的说法是:

Remember-me or persistent-login authentication refers to web sites being able to remember the identity of a principal between sessions. This is typically accomplished by sending a cookie to the browser, with the cookie being detected during future sessions and causing automated login to take place. Spring Security provides the necessary hooks for these operations to take place, and has two concrete remember-me implementations. One uses hashing to preserve the security of cookie-based tokens and the other uses a database or other persistent storage mechanism to store the generated tokens.

Note that both implementations require a UserDetailsService. If you are using an authentication provider which doesn’t use a UserDetailsService (for example, the LDAP provider) then it won’t work unless you also have a UserDetailsService bean in your application context.

个人理解, form登陆的时候,若是咱们提供了一个名叫remember-me 的参数,并且若是配置了RememberMeAuthenticationFilter , 那么这个filter 就会尝试自动登录autoLogin, 。具体来讲, 若是登陆成功,那么AbstractAuthenticationProcessingFilter 会调用RememberMeServices 的loginSuccess 方法, 而后 将successfulAuthentication相关信息组装成一个 cookie ,写到浏览器。cookie的名字是 remember-me, 默认和那个form 的参数名字是同样的, 它的时间默认是 14天。 而后, 咱们下次登陆的时候, security 先从 request中获取名为 remember-me 的cookie, 而后decode 它 为一个数组, 提取user name, 而后经过UserDetailsService  获取用户信息, 获取以后在比对下数组的第二部分是否一致。比对的时候,还有些麻烦。若是是 TokenBasedRememberMeServices ,那么须要先获取UserDetails,而后从新计算 expectedTokenSignature ; 若是是PersistentTokenBasedRememberMeServices, 它须要一个PersistentTokenRepository, 有两个实现,要么是 从内存: InMemoryTokenRepositoryImpl (默认) 或者 数据库 JdbcTokenRepositoryImpl 中读取。 若是是jdbc ,那么表是 必须persistent_logins 。 

 

坑爹的是, 若是咱们经过logout  注销。那么这个cookie 就被删除了。 因而乎, 我试过,这个remember-me的做用仅限于 不logout  而后关闭浏览器, 而后确实能够不用输入用户名密码。可是, 除此以外的做用不大... 

 

此处参考:

http://www.cnblogs.com/yjmyzz/p/remember-me-sample-in-spring-security3.html

http://www.cnblogs.com/fenglan/p/5913324.html

若是没有配置 UserDetailsService,那么:

java.lang.IllegalStateException: UserDetailsService is required.
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:455) ~[spring-security-config-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.onLoginSuccess(TokenBasedRememberMeServices.java:182) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.loginSuccess(AbstractRememberMeServices.java:294) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:318) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:240) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    

 

另外, 调试登陆的时候, 咱们能够发现大体有哪些过滤器:

originalChain = {ApplicationFilterChain@6975} 
 filters = {ApplicationFilterConfig[10]@7357} 
  0 = {ApplicationFilterConfig@7359} "ApplicationFilterConfig[name=metricsFilter, filterClass=org.springframework.boot.actuate.autoconfigure.MetricsFilter]"
  1 = {ApplicationFilterConfig@7360} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.filter.OrderedCharacterEncodingFilter]"
  2 = {ApplicationFilterConfig@7361} "ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter]"
  3 = {ApplicationFilterConfig@7362} "ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter]"
  4 = {ApplicationFilterConfig@7363} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.filter.OrderedRequestContextFilter]"
  5 = {ApplicationFilterConfig@7364} "ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]"
  6 = {ApplicationFilterConfig@7365} "ApplicationFilterConfig[name=webRequestLoggingFilter, filterClass=org.springframework.boot.actuate.trace.WebRequestTraceFilter]"
  7 = {ApplicationFilterConfig@7366} "ApplicationFilterConfig[name=oauth2ClientContextFilter, filterClass=org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter]"
  8 = {ApplicationFilterConfig@7367} "ApplicationFilterConfig[name=applicationContextIdFilter, filterClass=org.springframework.boot.web.filter.ApplicationContextHeaderFilter]"
  9 = {ApplicationFilterConfig@7368} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"
 pos = 6
 n = 10
 servlet = {DispatcherServlet@7358} 
 servletSupportsAsync = true
additionalFilters = {ArrayList@7045}  size = 13
 0 = {WebAsyncManagerIntegrationFilter@6972} 
 1 = {SecurityContextPersistenceFilter@6971} 
 2 = {HeaderWriterFilter@6970} 
 3 = {CsrfFilter@6958} 
 4 = {LogoutFilter@6957} 
 5 = {UsernamePasswordAuthenticationFilter@6956} 
 6 = {RequestCacheAwareFilter@6955} 
 7 = {SecurityContextHolderAwareRequestFilter@6954} 
 8 = {RememberMeAuthenticationFilter@6948} 
 9 = {AnonymousAuthenticationFilter@7163} 
 10 = {SessionManagementFilter@7387} 
 11 = {ExceptionTranslationFilter@7388} 
 12 = {FilterSecurityInterceptor@7389} 

 

 

 

参考:

https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/

http://www.cnblogs.com/softidea/p/6243200.html

http://www.cnblogs.com/davidwang456/p/4549344.html

相关文章
相关标签/搜索