Spring Security-基本配置与核心组件概览
前言
最近在跟着一本书写一个Spring项目,结果快被那个登录模块折磨死了……照着一大堆代码写了下来也不知道到底在干什么。于是需要花点时间好好学习一下Spring Security。
然而网上关于Spring Security的内容相当少。更不用提能说清楚的了。拜托讲人话有这么难嘛??好不容易找到了一个比较详细的教程。在内容的详细程度和解释的通俗程度上都很棒。
内容主要摘自:
芋道 Spring Boot 安全框架 Spring Security 入门
概述
Authentication & Authorization
以打飞机举例子:
- 【认证】你要登机,你需要出示你的 passport 和 ticket,passport 是为了证明你张三确实是你张三,这就是 authentication。
- 【授权】而机票是为了证明你张三确实买了票可以上飞机,这就是 authorization。
以论坛举例子:
- 【认证】你要登录论坛,输入用户名张三,密码 1234,密码正确,证明你张三确实是张三,这就是 authentication。
- 【授权】再一 check 用户张三是个版主,所以有权限加精删别人帖,这就是 authorization
FROM Spring Security 官网:
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序。Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring Security的真正强大之处,在于它很容易扩展以满足定制需求。
核心组件
这一节主要介绍一些在 Spring Security 中常见且核心的 Java 类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。
SecurityContextHolder
SecurityContextHolder 用于存储安全上下文(security context)的信息:
当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限… 这些都被保存在 SecurityContextHolder 中。
SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在 web 场景下使用 Spring Security。
(都用上Spring Boot了当然是web项目了啊,难不成还要用swing编程?)
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
}
else {
String username = principal.toString();
}
getAuthentication()返回了认证信息, getPrincipal() 返回了身份信息,UserDetails 便是 Spring 对身份信息封装的一个接口。
Authentication
先看看这个接口的源码长什么样:
package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
Collection<? extends GrantedAuthority> getAuthorities(); // <2>
Object getCredentials();// <2>
Object getDetails();// <2>
Object getPrincipal();// <2>
boolean isAuthenticated();// <2>
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。可以见得,Authentication 在 spring security 中是最高级别的身份 / 认证的抽象。
还记得上一节中,authentication.getPrincipal() 返回了一个 Object,我们将 Principal 强转成了 Spring Security 中最常用的 UserDetails,这在 Spring Security 中非常常见,接口返回 Object,使用 instanceof 判断类型,强转成对应的具体实现类。接口详细解读如下:
getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。getDetails(),细节信息,web 应用中的实现接口通常为WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。getPrincipal(),最重要的身份信息!!!大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。UserDetails 接口将会在下面的小节重点介绍。
Spring Security 是如何完成身份认证的?
- 用户名和密码被过滤器获取到,封装成
Authentication, 通常情况下是UsernamePasswordAuthenticationToken这个实现类。 AuthenticationManager身份管理器负责验证这个Authentication- 认证成功后,
AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。 SecurityContextHolder安全上下文容器将第 3 步填充了信息的Authentication,通过 SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到其中。
为了方便理解,这里采用一段类似于伪代码的程序来描述整个流程:
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
break;
}catch(AuthenticationException e) {
System.out.println("Authentication failed:" + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains:" + SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");}
}
实际使用中,整个流程更加复杂。但核心思想几乎与上面一致。
AuthenticationManager
初次接触 Spring Security 的朋友相信会被 AuthenticationManager,ProviderManager ,AuthenticationProvider … 这么多相似的 Spring 认证类搞得晕头转向(没错我就是这样),但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点。因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,甚至,可能允许用户使用指纹登录,所以说 AuthenticationManager 一般不直接认证。
AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个List<AuthenticationProvider> 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
也就是说,核心的认证入口始终只有一个:AuthenticationManager。
不同的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登录则对应了三个 AuthenticationProvider。这样一来四不四就好理解多了?
熟悉 shiro 的朋友可以把 AuthenticationProvider 理解成 Realm。在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。
只保留了关键认证部分的 ProviderManager 源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个 AuthenticationProvider 列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有 Authentication 信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 移除密码
((CredentialsContainer) result).eraseCredentials();
}
// 发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
// 执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
ProviderManager 中的 List,会依照次序去认证,认证成功则立即返回,若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
DaoAuthenticationProvider
AuthenticationProvider 最最最常用的一个实现便是 DaoAuthenticationProvider。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。
在 Spring Security 中。提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法便是 retrieveUser。
虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作用,返回一个 UserDetails。还需要完成 UsernamePasswordAuthenticationToken 和 UserDetails 密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的,如果这个 void 方法没有抛异常,则认为比对成功。比对密码的过程,用到了 PasswordEncoder 和 SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
如果你已经被这些概念搞得晕头转向了,不妨这么理解 DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails 与 UserDetailsService
上面不断提到了 UserDetails 这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和 Authentication 接口很类似,比如它们都拥有 username,authorities,区分他们也是本文的重点内容之一。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Authentication 的 getCredentials()与 UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。
Authentication 中的 getAuthorities()实际是由 UserDetails 的 getAuthorities() 传递而形成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 之后被填充的。
UserDetailsService 和 AuthenticationProvider 两者的职责常常被人们搞混,关于他们的问题在文档的 FAQ 和 issues 中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现 UserDetailsService,通常这更加灵活。
简单配置
和其他的模块一样,使用Spring Security首先需要引入依赖。
在项目的 pom.xml中引入:
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Spring Security 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在 application.yml 中,添加 Spring Security 配置,如下:
spring:
# Spring Security 配置项,对应 SecurityProperties 配置类
security:
# 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
user:
name: user # 账号
password: user # 密码
roles: ADMIN # 拥有角色
默认情况下,Spring Boot UserDetailsServiceAutoConfiguration 自动化配置类,会创建一个内存级别的 InMemoryUserDetailsManager Bean 对象,提供认证的用户信息。
- 这里,我们添加了
spring.security.user配置项,UserDetailsServiceAutoConfiguration 会基于配置的信息创建一个用户 User 在内存中。 - 如果,我们未添加
spring.security.user配置项,UserDetailsServiceAutoConfiguration 会自动创建一个用户名为"user",密码为 UUID 随机的用户 User 在内存中。
Spring Security启用后,只要启动Spring Boot应用,不管进入什么页面都会被拦截在登录页面。
因为我们没有自定义登录界面,所以默认会使用 DefaultLoginPageGeneratingFilter 类,使用默认的界面。
输入我们配置的「user/user」账号,进行登录。登录完成后,因为 Spring Security 会记录被拦截的访问地址,所以浏览器自动动跳转 http://127.0.0.1:8080/admin/demo 接口。

浙公网安备 33010602011771号