首页 > 代码库 > Spring Security

Spring Security

Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。Spring Security提供了完整的安全性解决方案。它能够在Web请求级别和方法调用级别处理身份认证和授权。

Spring Security从两个角度来解决安全性问题。它使用Servlet规范中的Filter保护Web请求并限制URL级别的访问。Spring Security还能够使用Spring AOP保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。

 

Spring Security 3.2 被分成了11个模块

ACL(access control list)  支持通过访问控制列表为域对象提供安全性

切面(Aspects)  一个很小的模块,当使用Spring Security注解时,会使用基于AspectJ的切面,而不是使用标准的Spring AOP

CAS客户端(CAS Client)  提供与Jasig的中心认证服务进行集成功能

配置(configuration)  包含通过XML和Java配置Spring Security的功能支持

核心(Core)  提供Spring Security基本库

加密(Cryptography)  提供了加密和密码编码的功能

LDAP  支持基于LDAP进行认证

OpenID  支持使用OpenID进行集中式认证

Remoting  提供了对Spring Remoting的支持

标签库(tag library)  Spring Security的JSP标签库

Web  提供了Spring Security基于Filter的Web安全性支持

 

应用程序的类路径下至少要包含Core和Configuration这两个模块。

Spring Security通过一些列Servlet Filter来提供各种安全性功能。DelegatingFilterProxy是一个特殊的Servlet Filter。它将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring应用上下文中。

在web.xml中配置Servlet和Filter

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

通过java方式配置

public class SecurityWebInitializer extends AbstractSecurityWebApplicatuonInitializer{}

AbstractSecurityWebApplicationInitializer实现了WebApplicattionInitialzier,Spring会发现它,并在Web容器中注册DelegatingFilterProxy。

不管我们用web.xml还是通过AbstractSecurityWebApplicationInitializer的子类来配置DelegatingFilterProxy,它都会拦截发往应用中的请求,并将请求委托给ID为springSecurityFilterChain的bean。

springSecurityFilterChain本身是另一个特殊的Filter,他也被称为FilterChainProxy。它可以链接任意一个或多个其他的Filter。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityAdapeter{
}

@EnableWebSecurity注解将会启用Web安全功能,但实际上并没有什么用。Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中。

@EnableWebMvcSecurity注解配置了一个Spring MVC参数解析器(argument resolver),这样的话处理器方法能够通过带有@AuthenicationPrincipal注解的参数获得认证用户的principal。它同时还配置了一个bean,在使用Spring报表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造(cross-site request forger, CSRF)token输入域。

 

通过重载WebSecurityConfigurerAdapter中的一个或多个方法(configure)来指定Web安全细节

configure(WebSecurity)  通过重载,配置Spring Security的Filter链

configure(HttpSecurity)  通过重载,配置如何通过拦截器保护请求

configure(AuthenticationManagerBuilder)  通过重载,配置user-detail服务

 

拦截请求:重写protected void configure(HttpSecurity http) throws Exception{}

@Override
protected void configure(HttpSecurity http)  throws Exception{
    http.authorizeRequests().anyRequest().authenticated()
        .and().formLogin().and().httpBasic();
}

上段代码为Spring Security的默认配置。通过调用AuthorizeRequests()和anyRequest().authenticated()就会要求所有进入应用的HTTP请求都要进行认证,同时配置Spring Security支持基于表单的登陆以及HTTP Basic方式的认证

@Override
protected void configure(HttpSecurity http) throws Exception{
    http.authorizeRequest()
        .antMathchers("/spitters/me").authenticated() // 对spitters/me请求进行认证
        .antMatchers(HttpMethod.POST, "/spittles").authenticated()在  //对spittles的POST请求进行认证
        .anyRequest().permitAll();  //对于其他所有的请求都是允许的,不需要认证和任何的权限
}

  configure()方法中得到的HttpSecurity对象可以在多个方面配置HTTP的安全性。调用authorizeRequests()方法返回的对象的方法来配置请求级别的安全性细节。

  antMatchers()方法中设定的路径支持Ant风格的通配符。如antMatchers("/spittles/**", "/spittles/mine").authenticated();

  regexMatchers()方法则能够接受正则表达式来定义请求路径。如regexMatchers("/spitters/.*").authenticated();

  authenticated()要求在执行该请求时,必须已经登陆了应用,如果用户没有认证的话,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登陆页面。permitAll()方法允许请求没有任何的安全限制。requiresChannel()方法能为各种URL密匙声明所要求的通道。requiresSecure()表示将请求定向到HTTPS上

  e.g:  http.requiresChannel().antMatchers("/spitter/form").requiresSecure();

      http.antMatchers("/").requiresInecure();

 

Spring Security3.2起,默认会开启CSRF(cross-site request forgery)防护。它通过一个同步token的方式来实现CSRF防护的功能。它将会拦截状态变化的请求并检查CSRF token。如果请求中不包含CSRF token或token不能与服务器端的token相匹配,请求会失败,并抛出CsrfException异常。

 

 

 

access(String)  如果给定的SpEL表达式计算结果为true,就允许访问

anonymous()  允许匿名用户访问

authenticated()  允许认证过的用户访问

denyAll()  无条件拒绝所有饭个万宁

fullyAuthenticated()  如果用户是完整证认证的话(不是通过remember-me功能认证的),就允许访问

hasAnyAuthority(String...)  如果用户具备给定权限中的某一个的话,就允许访问

hasAnyRole(String...)  如果用户具备给定角色中的某一个的话,就允许访问

hasAuthhority(String)  如果用户具备给定权限的话,就允许访问

hasIpAddress(String)  如果请求来自给定IP地址的话,就允许访问

hasRole(String)  如果用户具备给定角色的话,就允许访问

not()  对其他访问方法的结果求反

permitAll()  无条件允许访问

rememberMe()  如果用户是通过Remember-me功能认证的,就允许访问

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
            .antMatchers("/spitter/ych").hasRole("SPITTER")
            .antMatchers(HttpMethod.POST, "/spittles").hasRole("SPITTER")
            .anyRequest().permitAll();
    }

Spring Security扩展了SpEL如下:

authentication  用户的认证对象

denyAll  结果始终为false

hasAnyRole(list of roles)  如果用户被授予了列表中任意的指定角色,结果为true

hasRole(role)  如果用户被受哦与了指定的角色,结果为true

hasIpAddress(IP Address)  如果请求来自指定IP,结果为true

isAnonymous()  如果当前用户为匿名用户,结果为true

isAuthenticated()  如果当前用户进行认证的话,结果为true

idFullyAuthenticated()  如果当前用户进行了完整认证的话,结果为true

isRememerMe()  如果当前用户是用过Remember-me自动认证的,结果为true

permitALL  始终为true

principal  用户的principal对象

  e.g: antMathcers("/spitter/me").access("hasRole(‘ROLE_SPITTER‘) and hasIpAddress(‘192.168.1.1‘)")

 

 

用户存储:重写protected void configure(AuthenticationManagerBuilder auth) throws Exception{}

  使用基于内存的用户存储@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.inMemoryAuthentication().withUser("user").password("password").roles("USER")
        .and().withUser("admin").password("password").roles("USER", "ADMIN");
// auth.inMemoryAuthentication().withUser("user").password("password").authorities("ROLE_USER")
//    .and().withUser("admin").password("password").authorities("ROLE_ADMIN"); }

    通过调用auth.inMemoryAuthentication()启动内存用户存储。withUser()方法为内存用户存储添加新的用户,它返回UserDetailsManagerConfigurer.UserDetailsBuilder。roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个"ROLE_"前缀,并将其作为权限授予给用户。

                配置用户详细信息的方法

  accountExpired(boolean)  定义账号是否已经过期

  accountLocked(boolean)  定义账户是否已经锁定

  and()  用来连接配置

  authorities(GrantedAuthority...)  授予某个用户一项或多项权限 

  authorities(List<? extends GratedAuthority>)  授予某个用户一项或多项权限

  authorities(String...)  授予某个用户一项或多项权限

  credentialsExpired(boolean)  定义凭证是否已经过期

  disabled(boolean)  定义账号是否已被禁用

  password(String)  定义用户密码

  roles(String...)  授予某个用户一项或多项角色

  

  基于数据库表进行认证

@Autowired
DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder) throws Exception{
    auth.jdbbcAuthentication().dataSource(dataSource);
}

上段代码是默认的最少配置,它对我们的数据库模式有一些要求,它预期存在某些存储用户数据的表。下面的代码来自于Spring Security的内部:

  public static final String DEF_USERS_BY_USERNAME_QUERY = "select username, password, enabled from users where userame = ?";

  public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username, authority from authorities where username = ?";

  public static final String DEF_GROUP+AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name. ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group+id and g.id = gm.group_id "; 

  可以通过usersByUsernameQuery()和authoritiesByUsernameQuery()来自定义查询

  

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery("select username, password, true from Spitter where username = ?")
        .authorititiesByUsernameQuery("select username, ‘ROLE_USER‘ from Spitter where username = ?")
.password(new StandardPasswordEncoder("53cr3t")); }

  将默认的SQL查询替换为自定义的设计时,很重要的一点是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限查询会选取零行或多行数据,每行数据都会包含群组ID、群组名称以及权限。passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。Spring security的加密模块包括了三个实现:BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。

 

  基于LDAP进行认证

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.jdbcAuthentication().userSearchFilter("{uid={0}}").groupSearchFilter("member={0}");
}

  userSearchFilter()和groupSearchFilter()用来为基础LDAP查询提供过滤条件,它们分别用于搜寻用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP层级结构的跟开始。用户可以通过userSearchBase()和groupSearchBase()方法来更改查找用户的基础查询和组指定的基础查询。

@Override
protected void configure(AuthenticationMangagerBuilder auth) throws Exception{
    auth.ldapAuthentication().userSearchBase("ou=people").userSearchFilter("(uid={0})")
        .groupSearchBase("ou=groups").groupSearchFilter("member={0}")
        .passwordCompare();
}

  默认情况下,在登录表单中提供的密码将会与用户的LDAP条目中的userPassword属性进行对比,如果密码被保存在不同的属性中们可以通过passwordAttribute()方法来声明密码属性的名称。

  contextSource()方法会返回一个ContextSourceBuilder对象,这个对象提供url()方法来指定LDAP服务器的地址。同时,ContextSourceBuilder还提供root()方法调用Spring Security内嵌的LDAP服务器。当LDAP服务器启动时,它会尝试在类路径下寻找LDIF文件来加载数据。LDIF(LDAP Data Interchange Format)是以文本文件展现LDAP数据的标准方式。每条记录可以有一行或多行,每项包含一个名值对。记录之间通过空行进行分割。若不想Spring从整个根路径下搜索LDIF文件的话,可以通过调用ldif()方法来明确指定加载哪个LDIF文件。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
   auth.ldapAuthentication().userSearchBase("ou=people").userSearchFilter("(uid={0})")
         .groupSearchBase("ou=groups").groupSearchFilter("member={0}")
         .contextSource().root("dc=habuma,dc=com").ldif("classpath:users.lidf");
}

 

  配置自定义的用户服务

    我们需要实现UserDetailsService接口并重载loadUserByUsername()方法,然后在congifure中调用。

@Autowired
private SpitterRepository repository;

@Override 
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(new SpitterUserService(repository));
}
public class SpitterUserService implements UserDetailsService{

    private final SpitterRepository repository;

    public SpitterUserService(SpittreRepository repository){
        this.repository = repository;
    }

    @Override
    public UserDetais loadUserByUsername(String username) throws UserNameNotFoundException{
        Spitter spitter = repository.findByUsername(username);
        if(spitter != null){
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_SPITTER"));

            return new User(spitter.getUsername(), spitter.getPassword(), authorities);
        }

        throw new UsernameNotFoundException(username + " not found");
    }

}    

 

HTTP Basic认证(Http Basic Authentication)会直接通过HTTP请求本身,对要访问应用程序的用户进行认证。可以通过http.httpBasic()进行开启认证,并调用realmName("Spittr")方法来指定域

 

Spring Security提供Remember-me功能。通过http.rememberMe()来设置。默认情况下,spring会在cookie中存储一个token,这个token最多两周有效,但可以通过设置tokenValiditySecounds(millionseconds)。存储在cookie中的token包含用户名,密码,过期时间和一个私钥,写入cookie前都进行了MD5哈希。默认情况下,私钥名为SpringSecured,但可以通过.key(name)方法更改私钥名称。

退出功能是通过Servlet容器中的Filter实现的,这个Filter会拦截针对"/logout"的请求。当用户访问/logout时,这个请求会被Spring Security的Logout Filter处理。用户会推出应用,所有的remember-me token都会被清除掉。在退出完成后,用户浏览器将会重定向到/login?logout,从而允许用户进行再次登陆。若希望用户被重定向到其他的页面,可以使用http.logout().logoutSucessUrl("url")中进行设置,也可以调用.logoutUrl("url")来更改退出路径。

 

使用JSP标签库,需要在JSP中声明<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Spring Security的JSP标签库只包含了三个标签

<security:accesscontrollist>  如果用户通过访问控制列表授予了指定的权限,那么渲染该标签体中的内容

<security:authentication>  渲染当前用户认证对象的详细信息

<security:authorize>  如果用户被授予了特定的权限或SpEL表达式的结果为true,那么渲染标签体中的内容

 

使用<security:authentication>JSP标签来访问用户的认证详情

authorites:一组用于表示用户所授予权限的GrantedAuthority对象

Credentials:用于核实用户的凭证(通常是用户名和密码)

details:认证的附加信息(IP地址,证件序列号,会话ID等)

principal:用户的基本信息对象

e.g:  Hello <security:authentication property="principal.username" var="loginId" scope="request" />

 

Spring Security的<security:authorize>JSP标签能够根据用户被授予的权限有条件的渲染页面的部分内容

<sec:authorize access="hasRole(‘ROLE_SPITTER‘‘)">
    <s:url value="/spittles" var="spittle_url" />
    <sf:form modelAttribute="spittle" action="${spittle_url}">
        <sf:label path="text"><s:message code="lable.spittle" text="Enter spittle:" /></sf:label>
        <sf:textarea path="text" rows="2" cols="40" />
        <sf:errors path="text" />
        <br />
        <div class="spitItSumbitIt">
            <input type="submit" value="Spit it!"  />
        </div>
    </sf:form>
</sec:authorize>

 

Thymeleaf的安全方言提供了与Spring Security标签库相对应的属性。

1.需要在SpringTemplateEngine bean中声明SpringSecurityDialect。

@Bean
public SpringTemplateEngine templateEngine(TemplateResolver templateResolver){
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    templateEngine.addDialect(new SpringSecurityDialect());
    return templateEngine;
}

2.声明命名空间

  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">

 

常用标签:

sec:authentication  渲染认证对象的属性,类似Spring Security的<sec:authentication/>JSP标签

sec:authorize  基于表达式的计算结果,条件性的渲染内容,类似于Spring Security的<sec:authorize/>JSP标签

sec:authorize-acl  基于表达式的计算结果,条件性渲染内容,类似于Spring Security的<sec:accesscontrollist/>JSP标签

sec:authorize-expr  sec:authorize属性的别名

sec:authorize-url  基于给定URL路径相关的安全规则,条件性的渲染内容,类似于Spring Security的<sec:authorize/>JSP标签使用url属性时的场景

 

Spring Security