0%

AccessDeniedHandler

用户登录后,出现权限异常,可用该类处理

SimpleUrlAuthenticationFailureHandler

用户登录失败处理

SimpleUrlAuthenticationSuccessHandler

用户登录成功处理

AbstractAuthenticationTargetUrlRequestHandler

用户登出处理

AuthenticationEntryPoint

匿名用户和Remember me 用户,权限处理

参考资料

Spring Security-认证过程的发起(ExceptionTranslationFilter,AuthenticationEntryPoint)

认证用户

[TOC]

  如果我们使用如下的最简单的配置,那么就能无偿地得到一个登陆页面:

1
2
3
4
5
package spitter.config;
......
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{}

  实际上,在重写configure(HttpSecurity)之前,我们都能使用一个简单却功能完备的登录页。但是,一旦重写了configure(HttpSecurity)方法,就是失去了这个简单的登录界面。不过,这个功能要找回也容易。我们只需要在configure(HttpSecurity)方法中,调用formLogin(),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.and()
.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
.anyRequest().permitAll()
.and()
.requiresChannel()
.antMatchers("/spitter/form").requiresSecure();
}

  如果我们访问应用的“/login”链接或者导航到需要认证的页面,那么将会在浏览器中展现登录界面。这个界面在审美上没什么令人兴奋的,但是它却能实现所需的功能。

添加自定义的登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"></link>
</head>
<body onload='document.f.username.focus();'>
<div id="header" th:include="page :: header"></div>
<div id="content">

<a th:href="@{/spitter/register}">Register</a>


<form name='f' th:action='@{/login}' method='POST'>
<table>
<tr><td>User:</td><td>
<input type='text' name='username' value='' /></td></tr>
<tr><td>Password:</td>
<td><input type='password' name='password'/></td></tr>
<tr><td colspan='2'>
<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label></td></tr>
<tr><td colspan='2'>
<input name="submit" type="submit" value="Login"/></td></tr>
</table>
</form>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>

启用HTTP Basic认证

  如果要启用HTTP Basic认证的话,只需要在configure()方法所传入的HttpSecurity对象上调用httpBasic()即可。另外,还可以通过调用realmName()方法指定域。如下是在Spring Security中启用HTTP Basic认证的典型配置:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.loginPage("/login")
.and()
.httpBasic()
.realmName("Spittr")
.and()
...
}1234567891011

在httpBasic()方法中,并没有太多的可配置项,甚至不需要什么额外配置。HTTP Basic认证要么开启,要么关闭。

启用Remember-me功能

  站在用户的角度来讲,如果应用程序不用每次都提示他们登录是更好的。这就是为什么许多站点提供了Remember-me功能。你只要登录过一次,应用就会记住你。
  Spring Security使得为应用添加Remember-me功能变得非常容易。只需要在configure()方法所传入的HttpSecurity对象上调用rememberMe()即可。

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.loginPage("/login")
.and()
.rememberMe()
.tokenValiditySeconds(2419200)
.key("spittrKey")
...
}1234567891011

  默认情况下,这个功能是通过在Cookie中存储一个token完成的,这个token最多两周内有效。但是,在这里,我们指定这个token最多四周内有效(2,419,200秒)。存储在cookie中的token包含用户名、密码、过期时间和一个私匙——在写入cookie前都进行了MD5哈希。默认情况下,私匙的名为SpringSecured,但是这里我们将其设置为spitterKey,使他专门用于Spittr应用。
  既然Remember-me功能已经启用,我们需要有一种方式来让用户表明他们希望应用程序记住他们。为了实现这一点,登录请求必须包含一个名为remember-me的参数。在登录表单中,增加一个简单复选框就可以完成这件事:

1
2
<input id="remember-me" name="remember-me" type="checkbox"/>
<lable for="remember-me" class="inline">Remember me</label>12

退出

  其实,按照我们的配置,退出功能已经可以使用了,不需要再做其他的配置了。我们需要的只是一个使用该功能的链接。
  退出功能是通过Servlet容器的Filter实现的(默认情况下),这个Filter会拦截针对“/logout”的请求。在新版本的SpringSecurity中,出于安全的考虑(防止CSRF攻击),已经修改了LogoutFilter,使得Get方式的“/logout”请求不可用。必须以POST方式发起对该链接的请求才能生效。因此,为应用添加退出功能只需要添加如下表单即可(如下以Thymeleaf代码片段的形式进行了展现):

1
2
3
<form th:action="@{/logout}" method="POST">
<button type="submit">退出登录</button>
</form>

   提交这个表单,会发起对“/logout”的请求,这个请求会被Spring Security的LogoutFilter所处理。用户会退出应用,所有的Remember-me token都会被清楚掉。在退出完成后,用户浏览器将会重定向到“/login?logout”,从而允许用户进行再次登录。如果希望被重定向到其他的页面,如应用的首页,那么可以在configure()中进行配置:

1
2
3
4
5
6
7
8
9
10
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.formLogin()
.loginPage("/login")
.and()
.logout()
.logoutSuccessUrl("/")
...
}

除了logoutSuccessUrl()方法之外,你可能还希望重写默认的LogoutFilter拦截路径。我们可以通过调用logoutUrl()方法实现这一功能:

1
2
3
.logout()
.logoutSuccessUrl("/")
.logoutUrl("/signout")

拦截请求

[TOC]

  在任何应用中,并不是所有请求都需要同等程度地保护起来。有些请求需要认证,有些则不需要。
对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。如下代码片段展现了重载的configure(HttpSecurity)方法,它为不同的URL路径有选择地应用安全性:

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/spitters/me").authenticated()
.antMatchers(HttpMethod.POST,"/spittles").authenticated()
.anyRequest().permitAll();
}

antMatchers()方法所设定的路径支持Ant风格的通配符。如下

1
.antMatchers("/spitters/**","spittles/mine").authenticated();          //Ant风格1

antMatchers()方法所使用的路径可能会包括Ant风格的通配符,而regexMatchers()方法则能够接受正则表达式来定义请求路径。如下:

1
.regexMatchers("spitters/.*").authenticated();                       //正则表达式风格1

  除了路径选择,我们还通过authenticated()和permitAll()来定义该如何保护路径。authenticated()要求在执行该请求时,必须已经登录了应用。如果用户没有认证,Spring Security的Filter将会捕获该请求,并将用户重定向到应用的登录界面。同时permitAll()方法允许请求没有任何的安全限制。除了authenticated()和permitAll()以外,authorizeRequests()方法返回的对象还有更多的方法用于细粒度地保护请求。如下所示:

方法 描述
access(String) 如果给定的SpEL表达式结果为true,就允许访问
anonymous 允许匿名访问
authenticated 允许认证过的用户访问
denyAll 无条件拒绝所有访问
fullyAuthenticated 如果用户是完整认证的话(非Remember-me功能认证),允许访问
hasAnyAuthority 如果用户具备给定权限的某一种的话,允许访问
hasAnyRoles 如果用户具备给定角色的某一种的话,允许访问
hasAuthority 如果用户具备给定权限的话,允许访问
hasIPAddress 如果用户具备给定IP的话,允许访问
hasRole 如果用户具备给定角色的话,允许访问
not 对其他访问方法的结果求反
permitAll 无条件允许
rememberMe 如果用户是通过Remember-me功能认证的,允许访问

  我们可以将任意数量的antMatchers()、regexMatchers()和anyRequest()连接起来,以满足Web应用安全规则的需要。注意,将最不具体的路径(如anyRequest())放在最后面。如果不这样做,那不具体的路径配置将会覆盖掉更为具体的路径配置。

使用Spring表达式进行安全保护

  上面的方法虽然满足了大多数应用场景,但并不是全部。如果我们希望限制某个角色只能在星期二进行访问的话,那么就比较困难了。同时,上面的大多数方法都是一维的,如hasRole()方法和hasIpAddress()方法没办法同时限制一个请求路径。
  借助access()方法,我们可以将SpEL作为声明访问限制的一种方式。例如,如下就是使用SpEL表达式来声明具有“ROLE_SPITTER”角色才能访问“/spitter/me”URL:

1
.antMatchers("/spitters/me").access("hasRole('ROLE_SPITTER')");1

  让SpEL更强大的原因在于,hasRole()仅是Spring支持的安全相关表达式中的一种。下表列出了Spring Security支持的所有SpEL表达式。

表达式 计算结果
authentication 用户的认证对象
denyAll 结果始终为false
hasAnyRole(list of roles) 如果用户被授予列表中的任意的指定角色,结果为true
hasRole(role) 如果用户被授予指定角色,结果为true
hasIpaddress(ip) 如果用户来自指定ip,结果为true
isAnonymous 如果用户是匿名,结果为true
isAuthenticated 如果用户进行过认证,结果为true
isFullyAuthenticated 如果用户进行过完整认证(非Remember-me),结果为true
isRememberMe 如果用户进行过(Remember-me)认证,结果为true
permitAll 结果始终为true
principal 用户的principal对象

  现在,如果我们想限制“/spitter/me”URL的访问,不仅需要ROLE_SPITTER角色,还需要来自指定的IP地址,那么我们可以按照如下的方式调用access()方法:

1
2
.antMatchers("/spitter/me")
.access("hasRole('SPITTER') and hasIpAddress('127.0.0.1')");

Spring Security拦截请求的另外一种方式:强制通道的安全性

  通过HTTP发送的数据没有经过加密,黑客就有机会拦截请求并且能够看到他们想看的数据。这就是为什么敏感信息要通过HTTPS来加密发送的原因。传递到configure()方法中的HttpSecurity对象,除了具有authorizeRequests()方法以外,还有一个requiresChannel()方法,借助这个方法能够为各种URL模式声明所要求的通道(如HTTPS)。
  在注册表单中,用户会希望敏感信息(用户不希望泄露的信息,如信用卡号等)是私密的。为了保证注册表单的数据通过HTTPS传送,我们可以在配置中添加requiresChannel()方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/spitter/me").hasRole("SPITTER")
.antMatchers(HttpMethod.POST,"/spittles").hasRole("SPITTER")
.anyRequest().permitAll();
.and()
.requiresChannel()
.antMatchers("spitter/form").requiresSecure(); //需要
}

  不论何时,只要是对“/spitter/form”的请求,Spring Security都视为需要安全通道(通过调用requiresChannel()确定的)并自动将请求重定向到HTTPS上。
与之相反,有些页面并不需要通过HTTPS传送。例如,首页不包含任何敏感信息,因此并不需要通过HTTPS传送。我们可以使用requiresInsecure()代替requiresSecure()方法,将首页声明为始终通过HTTP传送:

1
.antMatchers("/").requiresInsecure();1

防止跨站请求伪造

什么是跨站请求伪造?下面是一个简单的例子:

1
2
3
4
<form method="POST" action="http://www.spittr.com/Spittles">
<input type="hidden" name="massage" value="I'm a stupid" />
<input type="submit" value="Click here to win a new car!"/>
</form>1234

  这是跨站请求伪造(cross-site request forgery,CRSF)的一个简单样例。简单来讲,入过一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来很严重的后果。
  从Spring Security3.2开始,默认就会启用CSRF攻击。
  Spring Security通过一个同步token的方式来实现CSRF防护。它会拦截状态变化的请求并检查CSRF token。如果请求不包含CSRF token,或token不能与服务器端的token相匹配,请求将会失败,并抛出CsrfException。
Spring Security已经简化了将token放到请求的属性中这一任务。

  • 使用Thymeleaf,只要标签的action属性添加了Thymeleaf命名空间前缀,那么就会自动生成一个“_csrf”隐藏域:

    <form method="POST" th:action="@{/spittles}"> ... </form>

  • 使用JSP作为页面模板的话,要做的事非常类似:

1
<input type="hidden" name="${_csrf.parameterName}"  value="${_csrf.token}" />1
  • 如果使用Spring表单绑定标签的话,标签会自动为我们添加隐藏的CSRF token标签。

来源

SpringSecurity学习笔记之四:拦截请求

配置用户存储

Spring Security非常灵活,能够基于各种数据存储来认证用户。它内置了多种常见的用户存储场景,如内存、关系型数据库以及LDAP。同时,我们也可以编写并插入自定义的用户存储实现。借助Spring Security的Java配置,我们能够很容易地配置一个或多个数据存储方案。

一、使用基于内存的用户存储

重写chonfigure(AuthenticationManagerBuilder)方法可以方便地配置Spring Security对认证的支持。通过inmMemoryAuthentication()方法,我们可以启用、配置并任意填充基于内存的用户存储。实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
.and()
.withUser("admin").password("password").roles("USER","ADMIN");
}
}

withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,这个对象提供了多个进一步配置用户的方法,如上面的为用户设置密码的password()方法、授予用户多个角色权限的roles()方法。UserDetailsManagerConfigurer.UserDetailsBuilder对象的更多方法如下表:

方法 描述
accountExpired(boolean) 账号是否过去
accountLocked(boolean) 账号是否锁定
and() 用来连接配置
authorities(GrantedAuthority..) 授予用户一项或者多项权限
authorities(List<? extends GrantedAuthority>) 授予用户一项或者多项权限
authorities(String…) 授予用户一项或者多项权限
credentialsExpired(boolean) 凭证是否过期
disabled(boolean) 是否被禁用
password(String) 密码
roles(String) 授予用户一项或者多项角色

roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个“ROLE_”前缀,并将其作为权限授予给用户。如下的配置和上面的等价:

1
2
3
auth.inMemoryAuthentication()
.withUser("user").password("password").authorities("ROLE_USER").and()
.withUser("admin").password("password").authorities("ROLE_USER","ROLE_ADMIN");

二、基于数据库表进行认证

用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。所需的最少配置如下:

1
2
3
4
5
6
7
8
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource);
}

我们使用JDBCAuthentication()方法来实现一JDBC为支撑的用户存储,必须要配置的只是一个DataSource,就能访问关系型数据库了。

重写默认的用户查询功能

Spring Security内部默认的查询语句是写定的,可能在某些情况下并不适用。我们可以按照如下的方式配置自己的查询:

1
2
3
4
5
6
7
8
@Override
protected void configure(Authentication auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,true from Spitter where username=?")
.authoritiesByUsernameQuery("select username,'ROLE_USER' from Spitter where username=?");
}

在本例中,我们只重写了认证和基本权限的查询语句,但是通过调用groupAuthoritiesByUsername()方法,我们也能够将群组权限重写为自定义的查询语句。将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数

使用转码后的密码

通常数据库中的密码都会加密,那么如果我们只是按照上面那样配置的话,用户提交的明文密码和数据库的加密密码就会不匹配,从而认证失败。为了解决这个问题,我们需要借助passwordEncoder()方法指定一个密码转码器:

1
2
3
4
5
6
7
8
9
@Override
protected void configure(Authentication auth) throws Exception{
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,true from Spitter where username=?")
.authoritiesByUsernameQuery("select username,'ROLE_USER' from Spitter where username=?")
passwordEncoder(new StandardPasswordEncoder("53cr3t"));
}

passwordEncoder()方法可以接受Spring Security中PasswordEncoder接口的任意实现。Spring Security的加密模块包括三个这样的实现:BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。

三、配置自定义的用户服务

如果我们需要认证的用户存储在非关系型数据库中,如Mongo或Neo4j,那么我们需要提供一个自定义的UserDetailsService接口实现。UserDetailsService接口非常简单:

1
2
3
public interface UserDetailsService{
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}123

我们需要做的就是实现loadUserByUsername()方法,根据给定的用户名来查找用户。该方法会返回代表给定用户的UserDetails对象。当我们自定义的配置类完成后,以SpitterUserService(实现了UserDetailsSevice接口并重写了loadUserByUsername方法)为例,可以通过如下方式将其配置为用户存储:

1
2
3
4
5
6
@Autowired
SpitterRepository spitterRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(new SpitterUserSevice(spitterRepository));
}

来源

SpringSecurity学习笔记之三:配置用户存储

Spring Security结构

Spring Security3.2分为11个模块,如下表所示:

模块 描述
ACL 支持通过访问控制列表(access control list)为域对象提供安全性
切面 当使用Spring security注解时,会使用基于AspectJ的切面,而不是标准的SpringAOP
CAS客户端 提供与Jasig的中心认证服务(Central Authentication Service)进行集成的功能
配置Configuration 包含通过XML和JAVA配置SpringSecurity的功能支持
核心core 基本库
加密Cryptograph 提供加密和密码编码功能
LDAP 支持基于LDAP进行认证
OpenId 支持使用OpenId镜像集中式认证
Remoting 提供了对Spring Remoting 的支持
标签库Tag Lib Jsp标签库
Web 提供基于Filter的web安全性支持

编写简单的安全性配置

Spring Security3.2引入了新的Java配置方案,完全不在需要通过XML来配置安全性功能。如下,展现了Spring Security最简单的Java配置:

1
2
3
4
5
package spitter.config;
......
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{}

@EnableWebSecurity注解将会启动Web安全功能,但它本身并没有什么功能。Spring Security必须配置在一个实现了WebSecurityConfigurer的bean中,或者扩展WebSecurityConfigurerAdapter,扩展该类是最简单的配置方法。
这样做有以下几点好处:

  • a、该注解配置了一个Spring MVC参数解析器(argument
    resolver),这样处理器方法能够通过带有@AuthenticationPrincipal注解的参数获得认证用户的principal(或username)。
  • b、它配置了一个bean,在使用Spring表单绑定标签库来定义表单时,这个bean会自动添加一个隐藏的跨站请求伪造token输入域。

我们可以通过重载WebSecurityConfigurerAdapter中的一个或多个方法来指定Web安全的细节。例如WebSecurityConfigurerAdapter的三个configure()方法。详情如下表:

方法 描述
configure(WebSecurity) 配置Filter链
configure(HttpSecurity) 配置如何通过拦截器保护请求
configure(AuthenticationManagerBuilder) 配置userDetail服务

上面的程序清单中没有重写任何一个configure()方法,所以应用现在是被严格锁定的,没有任何人能够进入系统。为了让Spring Security满足我们应用的需求,还需要再添加一点配置。具体来讲,我们需要:

  • a、配置用户存储
  • b、指定哪些请求需要认证,哪些请求不需要认证,以及需要的权限。
  • c、提供一个自定义的登录页面,替代原来简单的默认登录页

来源

SpringSecurity学习笔记之二:SpringSecurity结构及基本配置

spring 过滤链 顺序。具体顺序可以通过 springboot 启动时 DefaultSecurityFilterChain 打印的日志查看

1
2
3
4
5
6
7
8
9
10
11
12
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.session.ConcurrentSessionFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

参考

springSecurity原理

SpringSecurity 概述

[TOC]

概述

Spring Security是一种基于Spring AOP和Servlet规范中的Filter实现的安全框架。它能够在Web请求级别和方法调用级别处理身份认证和授权。
Spring Security从两个角度来解决安全性问题。

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

本系列笔记学习的是Spring Security3.2,示例基于Java配置,并且有完整的小Demo用于巩固了练手。在后续将会涉及到的知识点有:

  • SpringSecurity结构

  • Spring Security配置

  • 配置用户存储的三种方式及示例

  • 拦截请求
    – 使用Spring表达式进行安全保护

    – 强制通道的安全性

    – 防止跨站请求伪造

  • 认证用户

    – 添加自定义登录页

    – 启用HTTP Basic认证

    – 启用Remember-me功能

    – 退出

  • 保护视图

    – 使用Spring Security的JSP标签库

    – 使用Thymeleaf的Spring Security方言

Github项目描述

在基础知识学习完成之后,动手做了一个小Demo,涵盖了上面的所有知识点,用于练习和巩固,该Demo已经分享到Github上,开发工具是IntelliJ IDEA,基于Maven依赖,克隆下来不需要任何配置即可使用,小伙伴们在学习的过程中可以参照着理解。该项目默认的登录名和密码分别是“zhou””123”。项目基于Spring+SpringMVC+SpringSecurity+Maven+Thymeleaf+JavaConfig

Github仓库地址:https://github.com/Dodozhou/SpringSecurityDemo

来源

SpringSecurity学习笔记之一:SpringSecurity概述及Github项目克隆