进阶-使用Spring Security3.2搭建LDAP认证授权和Remember-me(2)

接上 进阶-使用Spring Security3.2搭建LDAP认证授权和Remember-me(1) 

javaconfig

使用javaconfig,只需要生成两个类,就可以完成XML配置下的3个步骤。这两个类非别是:

  1. 继承于org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter的一个子类。 

  2. 继承于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分别的解释:

  1. 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();
            }
        }
  2. 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");
            }
        }
  3. AjaxSecurityConfig,则是配置ajax目录下面的Basic Authentication. 由于看了上一个配置的详解,这个配置就简单多了。不多说。

到此为止。FBA和BA均已配置完毕。将firstWeb打包,部署到tomcat上,查看每个路径是否符合我们的配置。

CSRF配置

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 POST

如果用户没有登陆,比如他在写博客。当他写完,提交FORM到受保护的页面时,需要FBA认证。在SpringSecurity中,如果没有经过特殊处理,FORM提交会转到login页面,当用户登陆成功以后,会跳转到默认页面,之前填写的FORM会丢失掉。 这会令人抓狂。

一般的处理方式有2种:

  1. 在用户填写完FORM以后,点击提交按钮之时,浏览器会先使用AJAX向服务器询问用户是否有权限,如果没有,则在当前页面动态的生成一个Warning message,告诉用户还没有登陆。

  2. 在用户填写完FORM,点击按钮后,同样还是AJAX询问服务器是否登录,如果没有,则弹出一个AJAX方式的用户登陆dialog,用户在dialog中输入账户,登陆成功后,重新提交FORM.

在本实验中,index.jsp想submit.jsp提交FORM就存在未登陆的情况,感兴趣的同学可以根据上面两种方式,改进其登陆方式。我就懒的去写了。

Remember Me功能

Remember Me的理论很简单。服务器端将用户名,加密过后的密码,和一个过期时间打包在一起,生成一个base64 token,写入客户端的cookie中。当用户下次访问的时候,可以从cookie中读取remember me的cookie,通过cookie,可以找到用户的名字和加密过的密码。系统通过UserDetailsService访问用户的所有信息,将cookie中的信息和UserDetailsService拿到的用户信息进行比对,其中密码进行加密后的比对,比对成功,则完成自动登录。

Spring Security 3中的remember me功能提供了默认实现。但它也存在几个要求:

  1. 在login Form中写入<input type="checkbox" name="remember-me">

  2. AuthenticationManager必须实现了UserDetailsService. 或为RememberMeAuthenticationProvider指定UserDetailsService.

  3. XML或javaconfig中配置remember me.

接下来,我将开始配置Remember me. Token信息存储数据库就不配置了,Spring自带机制。Remember me将记住账户14天。

  1. 在login Form中添加代码

    <input type="checkbox" name="remember-me"></td><td>Don't ask for my password for two weeks</td>
  2. 在类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);
            }
  3. 为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);
    	}
  4. 到此remember me就成功了。重新部署,测试一下吧。

集群与SSO

请看下一篇文章。

你可能感兴趣的:(进阶-使用Spring Security3.2搭建LDAP认证授权和Remember-me(2))