Spring Framework 为开发 Java 应用程序提供了全面的基础架构支持。它包含了一些不错的功能,如 "依赖注入",以及一些现成的模块:html
这些模块能够大大减小应用程序的开发时间。例如,在 Java Web 开发的早期,咱们须要编写大量样板代码以将记录插入数据源。可是,经过使用 Spring JDBC 模块的 JDBCTemplate,咱们能够仅经过少许配置将其简化为几行代码。java
阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问个人我的博客 —— 全栈修仙之路
Spring Boot 是基于 Spring Framework,它为你的 Spring 应用程序提供了自动装配特性,它的设计目标是让你尽量快的上手应用程序的开发。如下是 Spring Boot 所拥有的一些特性:spring
Spring Security 是一个可以为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组能够在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减小了为企业系统安全控制编写大量重复代码的工做。数据库
Spring Security 拥有如下特性:编程
Spring、Spring Boot 和 Spring Security 三者的关系以下图所示:安全
目前 Spring Security 5 支持与如下技术进行集成:架构
在进入 Spring Security 正题以前,咱们先来了解一下它的总体架构:框架
最基本的对象是 SecurityContextHolder
,它是咱们存储当前应用程序安全上下文的详细信息,其中包括当前使用应用程序的主体的详细信息。如当前操做的用户是谁,该用户是否已经被认证,他拥有哪些角色权限等。ide
默认状况下,SecurityContextHolder
使用 ThreadLocal
来存储这些详细信息,这意味着 Security Context 始终可用于同一执行线程中的方法,即便 Security Context 未做为这些方法的参数显式传递。spring-boot
由于身份信息与当前执行线程已绑定,因此可使用如下代码块在应用程序中获取当前已验证用户的用户名:
Object principal = SecurityContextHolder.getContext() .getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
调用 getContext()
返回的对象是 SecurityContext
接口的一个实例,对应 SecurityContext
接口定义以下:
// org/springframework/security/core/context/SecurityContext.java public interface SecurityContext extends Serializable { Authentication getAuthentication(); void setAuthentication(Authentication authentication); }
在 SecurityContext 接口中定义了 getAuthentication 和 setAuthentication 两个抽象方法,当调用 getAuthentication 方法后会返回一个 Authentication 类型的对象,这里的 Authentication 也是一个接口,它的定义以下:
// org/springframework/security/core/Authentication.java public interface Authentication extends Principal, Serializable { // 权限信息列表,默认是GrantedAuthority接口的一些实现类,一般是表明权限信息的一系列字符串。 Collection<? extends GrantedAuthority> getAuthorities(); // 密码信息,用户输入的密码字符串,在认证事后一般会被移除,用于保障安全。 Object getCredentials(); Object getDetails(); // 最重要的身份信息,大部分状况下返回的是UserDetails接口的实现类,也是框架中的经常使用接口之一。 Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
以上的 Authentication 接口是 spring-security-core jar 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中,由此可知 Authentication 是 spring security 中核心的接口。经过这个 Authentication 接口的实现类,咱们能够获得用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息等。
下面咱们来简单总结一下 SecurityContextHolder,SecurityContext 和 Authentication 这个三个对象之间的关系,SecurityContextHolder 用来保存 SecurityContext (安全上下文对象),经过调用 SecurityContext 对象中的方法,如 getAuthentication 方法,咱们能够方便地获取 Authentication 对象,利用该对象咱们能够进一步获取已认证用户的详细信息。
SecurityContextHolder,SecurityContext 和 Authentication 的详细定义以下所示:
让咱们考虑一个每一个人都熟悉的标准身份验证方案:
前三项构成了身份验证进程,所以咱们将在 Spring Security 中查看这些内容。
UsernamePasswordAuthenticationToken
的实例中(咱们以前看到的Authentication
接口的实例)。AuthenticationManager
的实例以进行验证。AuthenticationManager
在成功验证时返回彻底填充的 Authentication
实例。SecurityContextHolder.getContext().setAuthentication(…)
建立的,传入返回的身份验证 Authentication 对象。了解完上述的身份验证流程,咱们来看一个简单的示例:
AuthenticationManager 接口:
public interface AuthenticationManager { // 对传入的authentication对象进行认证 Authentication authenticate(Authentication authentication) throws AuthenticationException; }
SampleAuthenticationManager 类:
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"); } }
AuthenticationExample 类:
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 { // 使用用户输入的name和password建立request对象,这里的UsernamePasswordAuthenticationToken // 是前面提到的Authentication接口的实现类 Authentication request = new UsernamePasswordAuthenticationToken(name, password); // 使用SampleAuthenticationManager实例,对request进行认证操做 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()); } }
在以上代码中,咱们实现的 AuthenticationManager
将验证用户名和密码相同的任何用户。它为每一个用户分配一个角色。上面代码的验证过程是这样的:
Please enter your username: semlinker Please enter your password: 12345 Authentication failed: Bad Credentials Please enter your username: semlinker Please enter your password: semlinker Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: semlinker; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,由于在实际需求中,咱们可能会容许用户使用用户名 + 密码登陆,同时容许用户使用邮箱 + 密码,手机号码 + 密码登陆,甚至,可能容许用户使用指纹登陆,因此要求认证系统要支持多种认证方式。
Spring Security 中 AuthenticationManager 接口的默认实现是 ProviderManager,但它自己并不直接处理身份验证请求,它会委托给已配置的 AuthenticationProvider 列表,每一个列表依次被查询以查看它是否能够执行身份验证。每一个 Provider 验证程序将抛出异常或返回一个彻底填充的 Authentication 对象。
也就是说,Spring Security 中核心的认证入口始终只有一个:AuthenticationManager,不一样的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登陆则对应了三个 AuthenticationProvider。
下面咱们来看一下 ProviderManager 的核心源码:
// spring-security-core-5.2.0.RELEASE-sources.jar // org/springframework/security/authentication/ProviderManager.java 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; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); // 遍历providers列表,判断是否支持当前authentication对象的认证方式 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } try { // 执行provider的认证方式并获取返回结果 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } // 若当前ProviderManager没法完成认证操做,且其包含父级认证器,则容许转交给父级认证器尝试进行认证 if (result == null && parent != null) { try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // 完成认证,从authentication对象中移除私密数据 ((CredentialsContainer) result).eraseCredentials(); } // 若父级AuthenticationManager认证成功,则派发AuthenticationSuccessEvent事件 if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // 未认证成功,抛出ProviderNotFoundException异常 if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } if (parentException == null) { prepareException(lastException, authentication); } throw lastException; } }
在 ProviderManager 进行认证的过程当中,会遍历 providers 列表,判断是否支持当前 authentication 对象的认证方式,若支持该认证方式时,就会调用所匹配 provider(AuthenticationProvider)对象的 authenticate 方法进行认证操做。若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,若是全部认证器都没法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException
异常。
在 Spring Security 中较经常使用的 AuthenticationProvider 是 DaoAuthenticationProvider,这也是 Spring Security 最先支持的 AuthenticationProvider 之一。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。DaoAuthenticationProvider 类的内部结构以下:
在实际项目中,最多见的认证方式是使用用户名和密码。用户在登陆表单中提交了用户名和密码,而对于已注册的用户,在数据库中已保存了正确的用户名和密码,认证即是负责比对同一个用户名,提交的密码和数据库中所保存的密码是否相同即是了。
在 Spring Security 中,对于使用用户名和密码进行认证的场景,用户在登陆表单中提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法就是 retrieveUser,虽然有两个参数,可是 retrieveUser 只有第一个参数起主要做用,返回一个 UserDetails。retrieveUser 方法的具体实现以下:
// spring-security-core-5.2.0.RELEASE-sources.jar // org/springframework/security/authentication/dao/DaoAuthenticationProvider.java protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
在 DaoAuthenticationProvider 类的 retrieveUser 方法中,会以传入的 username 做为参数,调用 UserDetailsService 对象的 loadUserByUsername 方法加载用户。
在 DaoAuthenticationProvider 类中 retrieveUser 方法签名是这样的:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { }
该方法返回 UserDetails 对象,这里的 UserDetails 也是一个接口,它的定义以下:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
顾名思义,UserDetails 表示详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。前面咱们也介绍了一个 Authentication 接口,它与 UserDetails 接口的定义以下:
虽然 Authentication 与 UserDetails 很相似,但它们之间是有区别的。Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 须要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这二者进行比对。
此外 Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而造成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是通过了 provider (AuthenticationProvider) 认证以后被填充的。
大多数身份验证提供程序都利用了 UserDetails
和 UserDetailsService
接口。UserDetailsService 接口的定义以下:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
在 UserDetailsService 接口中,只有一个 loadUserByUsername 方法,用于经过 username 来加载匹配的用户。当找不到 username 对应用户时,会抛出 UsernameNotFoundException 异常。UserDetailsService 和 AuthenticationProvider 二者的职责经常被人们搞混,记住一点便可,UserDetailsService 只负责从特定的地方(一般是数据库)加载用户信息,仅此而已。
UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,固然你也能够本身实现 UserDetailsService。
前面咱们已经介绍了 Spring Security 的核心组件(SecurityContextHolder,SecurityContext 和 Authentication)和核心服务(AuthenticationManager,ProviderManager 和 AuthenticationProvider),最后咱们再来回顾一下 Spring Security 总体架构: