Spring Security简介
之前项目都是用shiro,但是时过境迁,spring security变得越来越流行。spring security的前身是Acegi, acegi 我也玩过,那都是5年的事情了! 如今spring security已经发布了很多个版本,已经到了5.x.x 了。其新功能也增加了不少, 我们来看看吧!
spring security其实是独立于 spring boot 的。即使是 spring security 的注解, 也跟boot 关系不大, 那都是他们自带的。但是我这里还是把他归类为boot,因为我是使用boot来做测试的。
spring security 配置非常灵活,但是正是这种灵活性,是依赖于其底层需要强大的设计,和良好的API 支持, 基本是把复杂性包装了在底层。 spring security提供了所谓的流式API, 也就是可以通过点(.)符号,连续的进行配置。当然,这里的流式API跟java8的流式API还是不同的。
如果我们运行官方example,我们发现,挺好的啊, 原来 spring security 这么灵活易用啊! 灵活是没错的,但是是否易用就看情况了, 对熟悉的人,当然易用。对于新手,其实处处是坑! 因为不熟悉的话,基本上只能复制,但是一改动那么就发现各种问题。
官方示例
怎么配置就不多说了, 网上大把资料。对于security的form 登录的配置,官方标准关键部分是这样的:
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的如何配置,以及其各种坑。
关键概念和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 认证支持
(大致说明下关键的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