接上 进阶-使用Spring Security3.2搭建LDAP认证授权和Remember-me(1)
使用javaconfig,只需要生成两个类,就可以完成XML配置下的3个步骤。这两个类非别是:
继承于org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter的一个子类。
继承于org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer的子类。
原理如下:
SpringServletContainerInitializer实现了servlet 3中的一个规范接口javax.servlet.ServletContainerInitializer. 一旦实现了这个接口,当web container启动时,就会自动加载SpringServletContainerInitializer. 而SpringServletContainerInitializer会调用AbstractSecurityWebApplicationInitializer类。以上的步骤完成了相当于SpringSecurityFilterChain的配置。
接下来需要配置SpringSecurity,AbstractSecurityWebApplicationInitializer会为Spring指定配置文件,但这个配置文件不是XML形式 ,而是java形式。而java形式的配置则为WebSecurityConfigurerAdapter的子类。
如下面的小例子:
//注意java annotation的使用。 @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //用于配置Authentication,比如LDAP, Database连接,以及用户和角色的查询方法。 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception {} //用于配置URL的保护形式,和login页面。 @Override public void configure(HttpSecurity http) throws Exception {} //用于配置类似防火墙,放行某些URL。 @Override public void configure() {WebSecurity web} } public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityWebApplicationInitializer() { //注册Spring的配置文件。 super(SecurityConfig.class); } }
在3.2中,WebSecurityConfigurerAdapter使用三个configure方法,用于配置authentication, authorization和web security. 我们可以声明一个WebSecurityConfigurerAdapter,也可以声明多个.下面用我们项目中的实例来讲解这些内容。
回到firstWeb项目,我们需要生成两个类。可以把这两个类放在包com.mycompany.my.security下面。
MultiHttpSecurityConfig.java
@Configuration @EnableWebSecurity public class MultiHttpSecurityConfig { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:389/dc=mycompany,dc=com"); contextSource.setUserDn("cn=admin,dc=mycompany,dc=com"); contextSource.setPassword("admin"); contextSource.afterPropertiesSet(); BindAuthenticator authenticator = new BindAuthenticator(contextSource); authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator( contextSource, "ou=groups"); populator.setGroupRoleAttribute("cn"); populator.setGroupSearchFilter("uniqueMember={0}"); AuthenticationProvider authProvider = new LdapAuthenticationProvider( authenticator, populator); auth.authenticationProvider(authProvider); } @Configuration @Order(1) public static class IndexSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/index.jsp").anonymous(); } } @Configuration @Order(2) public static class HtmlSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/html/**") .authorizeRequests() .antMatchers("/html/submit.jsp").hasRole("BLACK") .antMatchers("/html/forbidden.html").authenticated() .and().formLogin() .loginPage("/html/login.jsp") .loginProcessingUrl("/html/login") .defaultSuccessUrl("/index.jsp") .permitAll() .and().logout().logoutUrl("/html/logout") .and().exceptionHandling().accessDeniedPage("/html/403.jsp"); } @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/html/forbidden.html"); } } @Configuration @Order(3) public static class AjaxSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .antMatcher("/ajax/**") .authorizeRequests().anyRequest().hasRole("RED") .and() .httpBasic(); } } }
SecurityWebApplicationInitializer.java
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityWebApplicationInitializer() { super(MultiHttpSecurityConfig.class); } }
在上面的代码中,我配置了三个service config. 他们使用共同的authentication, 关于authentication的代码都在configureGlobal方法中。LDAP还是使用我之前配置的OpenLDAP.
三个service config分别的解释:
IndexSecurityConfig, 配置index.jsp可以被所有人访问。看似多余,因为不声明也可以被所有的人访问。 但是这样可以对index.jsp开启所有的Filter,比如CSRFFilter。index.jsp中有Form,所以,需要开启CSRF。
@Configuration @Order(1) public static class IndexSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/index.jsp").anonymous(); } }
HtmlSecurityConfig, 配置html路径下的FBA。 这里将重点讲解。讲解放到code的注释中。注意,一切/xxx开始的路径,均是指相对于context path。比如/html/login, 它真正的url应该是/context/html/login.
@Configuration //声明这是一个SpringSecurity config @Order(2) //声明这个config使用的顺序。Spring会按照顺序进行匹配,一旦匹配,则越过后面。 public static class HtmlSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/html/**") //后面的配置适用于${context}/html/下的所有路径。 .authorizeRequests() //对下面的几个URL进行授权 //${context}/html/submit.jsp只能Black role才可访问 //${context}/html/forbidden.html认证过的人才可访问。 .antMatchers("/html/submit.jsp").hasRole("BLACK") .antMatchers("/html/forbidden.html").authenticated() .and().formLogin() //配置FBA .loginPage("/html/login.jsp") //指定登陆页面,如果未认证访问保护资源, //则跳转到此页面。 .loginProcessingUrl("/html/login") //此为login Form提交的URL. //类似于j_security_check .defaultSuccessUrl("/index.jsp") //登陆成功以后转到哪一个页面。 //如果因为Get访问受保护资源而跳转到 //login页面,登陆成功后会转到受保护的资源 //默认登陆失败url为login page?error //也可以通过方法指定。 .permitAll() //让login.jsp人人可访问,否则会导致递归跳转。 .and().logout().logoutUrl("/html/logout") //配置logout的处理URL. //可以配置logout成功后跳转的页面 //如果没有配置,则跳转到loginpage?logout .and().exceptionHandling() //配置exception处理页面。 .accessDeniedPage("/html/403.jsp"); } //配置放行的路径。放行/html/forbidden.html, 即使上面声明其为保护资源。 @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/html/forbidden.html"); } }
AjaxSecurityConfig,则是配置ajax目录下面的Basic Authentication. 由于看了上一个配置的详解,这个配置就简单多了。不多说。
到此为止。FBA和BA均已配置完毕。将firstWeb打包,部署到tomcat上,查看每个路径是否符合我们的配置。
javaconfig默认会开启CSRF,如果想关闭,可以调用http.csrf().disable(). 如果使用XML配置,默认是关闭CSRF,需要CSRF则需要声明<CSRF/>.
在开启CSRF以后,网站的任何FORM POST都必须带有CSRF的token,如果缺失token的话,则无法提交FORM。在每个FORM中,我们可以使用下面一段代码来带上CSRF的token.
<input type="hidden" name="<c:out value="${_csrf.parameterName}"/>" value="<c:out value="${_csrf.token}"/>"/>
试想,如果一个URL在GET的时候,没有经过CSRF filter的话,就不会有_csrf.token产生,这就是为什么我在上面要对不需要保护的index.jsp也声明了一个javaconfig的原因,这使得index.jsp会受到SpringSecurityFilterChain的过滤。
如果用户没有登陆,比如他在写博客。当他写完,提交FORM到受保护的页面时,需要FBA认证。在SpringSecurity中,如果没有经过特殊处理,FORM提交会转到login页面,当用户登陆成功以后,会跳转到默认页面,之前填写的FORM会丢失掉。 这会令人抓狂。
一般的处理方式有2种:
在用户填写完FORM以后,点击提交按钮之时,浏览器会先使用AJAX向服务器询问用户是否有权限,如果没有,则在当前页面动态的生成一个Warning message,告诉用户还没有登陆。
在用户填写完FORM,点击按钮后,同样还是AJAX询问服务器是否登录,如果没有,则弹出一个AJAX方式的用户登陆dialog,用户在dialog中输入账户,登陆成功后,重新提交FORM.
在本实验中,index.jsp想submit.jsp提交FORM就存在未登陆的情况,感兴趣的同学可以根据上面两种方式,改进其登陆方式。我就懒的去写了。
Remember Me的理论很简单。服务器端将用户名,加密过后的密码,和一个过期时间打包在一起,生成一个base64 token,写入客户端的cookie中。当用户下次访问的时候,可以从cookie中读取remember me的cookie,通过cookie,可以找到用户的名字和加密过的密码。系统通过UserDetailsService访问用户的所有信息,将cookie中的信息和UserDetailsService拿到的用户信息进行比对,其中密码进行加密后的比对,比对成功,则完成自动登录。
Spring Security 3中的remember me功能提供了默认实现。但它也存在几个要求:
在login Form中写入<input type="checkbox" name="remember-me">
AuthenticationManager必须实现了UserDetailsService. 或为RememberMeAuthenticationProvider指定UserDetailsService.
XML或javaconfig中配置remember me.
接下来,我将开始配置Remember me. Token信息存储数据库就不配置了,Spring自带机制。Remember me将记住账户14天。
在login Form中添加代码
<input type="checkbox" name="remember-me"></td><td>Don't ask for my password for two weeks</td>
在类HttpServiceConfig的方法configure(HttpSecurity http)中,对http的配置最后一行添加一段新的代码。
@Configuration @Order(2) public static class HtmlSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http....... .and().exceptionHandling().accessDeniedPage("/html/403.jsp") .and().rememberMe().tokenValiditySeconds(14*24*60*60); }
为AuthManager添加UserDetailsService。修改之前configureGlobal方法中的代码,添加UserDetailsService部分。
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { ...... LdapAuthenticationProvider authProvider = new LdapAuthenticationProvider( authenticator, populator); FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch("ou=people","(uid={0})",contextSource); LdapUserDetailsService userDetailsService = new LdapUserDetailsService(userSearch, populator); //auth.authenticationProvider(authProvider); //Will use DaoAuthenticationProvider. auth.userDetailsService(userDetailsService); }
到此remember me就成功了。重新部署,测试一下吧。
请看下一篇文章。