Spring Security核心类

核心类简介

Authentication

Authentication 是一个接口,用来表示用户认证信息的,在用户登陆认证以前相关信息会封装为一个 Authentication 具体实现类的对象,在登陆认证成功以后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,而后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。spring

SecurityContextHolder

SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。默认状况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中咱们能够从 ThreadLocal 中获取到当前的 SecurityContext。由于线程池的缘由,若是咱们每次在请求完成后都将 ThreadLocal 进行清除的话,那么咱们把 SecurityContext 存放在 ThreadLocal 中仍是比较安全的。这些工做 Spring Security 已经自动为咱们作了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。数据库

SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法内部逻辑基本上都是经过 SecurityContextHolder 持有的 SecurityContextHolderStrategy 来实现的,如 getContext()、setContext()、clearContext()等。而默认使用的 strategy 就是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy。另外,Spring Security 还提供了两种类型的 strategy 实现,GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一个 SecurityContext,如 C/S 结构的客户端;后者使用 InheritableThreadLocal 来存放 SecurityContext,即子线程可使用父线程中存放的变量。数组

通常而言,咱们使用默认的 strategy 就能够了,可是若是要改变默认的 strategy,Spring Security 为咱们提供了两种方法,这两种方式都是经过改变 strategyName 来实现的。SecurityContextHolder 中为三种不一样类型的 strategy 分别命名为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL。第一种方式是经过 SecurityContextHolder 的静态方法 setStrategyName() 来指定须要使用的 strategy;第二种方式是经过系统属性进行指定,其中属性名默认为 “spring.security.strategy”,属性值为对应 strategy 的名称。缓存

Spring Security 使用一个 Authentication 对象来描述当前用户的相关信息。SecurityContextHolder 中持有的是当前用户的 SecurityContext,而 SecurityContext 持有的是表明当前用户相关信息的 Authentication 的引用。这个 Authentication 对象不须要咱们本身去建立,在与系统交互的过程当中,Spring Security 会自动为咱们建立相应的 Authentication 对象,而后赋值给当前的 SecurityContext。可是每每咱们须要在程序中获取当前用户的相关信息,好比最多见的是获取当前登陆用户的用户名。在程序的任何地方,经过以下方式咱们能够获取到当前用户的用户名。安全

   public String getCurrentUsername() {
      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      if (principal instanceof UserDetails) {
         return ((UserDetails) principal).getUsername();
      }
      if (principal instanceof Principal) {
         return ((Principal) principal).getName();
      }
      return String.valueOf(principal);
   }

 

经过 Authentication.getPrincipal() 能够获取到表明当前用户的信息,这个对象一般是 UserDetails 的实例。获取当前用户的用户名是一种比较常见的需求,关于上述代码其实 Spring Security 在 Authentication 中的实现类中已经为咱们作了相关实现,因此获取当前用户的用户名最简单的方式应当以下。ide

 public String getCurrentUsername() {
      return SecurityContextHolder.getContext().getAuthentication().getName();
   }

 

此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,若是对应的 SecurityContext 不存在,则 Spring Security 将为咱们创建一个空的 SecurityContext 并进行返回。测试

AuthenticationManager 和 AuthenticationProvider

AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个表明认证请求的 Authentication 对象做为参数,若是认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。spa

Authentication authenticate(Authentication authentication) throws AuthenticationException;

 

在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,并且它不直接本身处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,而后会依次使用每个 AuthenticationProvider 进行认证,若是有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,以后的 AuthenticationProvider 将再也不继续认证。而后直接以该 AuthenticationProvider 的认证结果做为 ProviderManager 的认证结果。若是全部的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。校验认证请求最经常使用的方法是根据请求的用户名加载对应的 UserDetails,而后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证经过。Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails,UserDetailsService 将在下节讲解。在认证成功之后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中。线程

当咱们在使用 NameSpace 时, authentication-manager 元素的使用会使 Spring Security 在内部建立一个 ProviderManager,而后能够经过 authentication-provider 元素往其中添加 AuthenticationProvider。当定义 authentication-provider 元素时,若是没有经过 ref 属性指定关联哪一个 AuthenticationProvider,Spring Security 默认就会使用 DaoAuthenticationProvider。使用了 NameSpace 后咱们就不要再声明 ProviderManager 了。code

<security:authentication-manager alias="authenticationManager">
      <security:authentication-provider
         user-service-ref="userDetailsService"/>
   </security:authentication-manager>

 

若是咱们没有使用 NameSpace,那么咱们就应该在 ApplicationContext 中声明一个 ProviderManager。

认证成功后清除凭证

默认状况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码。因此若是你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么之后你再利用缓存的信息去认证将会失败,由于它已经不存在密码这样的凭证信息了。因此在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一块儿缓存。

UserDetailsService

经过 Authentication.getPrincipal() 的返回类型是 Object,但不少状况下其返回的实际上是一个 UserDetails 的实例。UserDetails 是 Spring Security 中一个核心的接口。其中定义了一些能够获取用户名、密码、权限等与认证相关的信息的方法。Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,咱们若是要使用 UserDetails 时也能够直接使用该类。在 Spring Security 内部不少地方须要使用用户信息的时候基本上都是使用的 UserDetails,好比在登陆认证的时候。登陆认证的时候 Spring Security 会经过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证经过后会将该 UserDetails 赋给认证经过的 Authentication 的 principal,而后再把该 Authentication 存入到 SecurityContext 中。以后若是须要使用用户信息的时候就是经过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。

一般咱们须要在应用中获取当前用户的其它信息,如 Email、电话等。这时存放在 Authentication 的 principal 中只包含有认证相关信息的 UserDetails 对象可能就不能知足咱们的要求了。这时咱们能够实现本身的 UserDetails,在该实现类中咱们能够定义一些获取用户其它信息的方法,这样未来咱们就能够直接从当前 SecurityContext 的 Authentication 的 principal 中获取这些信息了。上文已经提到了 UserDetails 是经过 UserDetailsService 的 loadUserByUsername() 方法进行加载的。UserDetailsService 也是一个接口,咱们也须要实现本身的 UserDetailsService 来加载咱们自定义的 UserDetails 信息。而后把它指定给 AuthenticationProvider 便可。以下是一个配置 UserDetailsService 的示例。

<!-- 用于认证的 AuthenticationManager -->
   <security:authentication-manager alias="authenticationManager">
      <security:authentication-provider
         user-service-ref="userDetailsService" />
   </security:authentication-manager>

   <bean id="userDetailsService"
      class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
      <property name="dataSource" ref="dataSource" />
   </bean>

 

上述代码中咱们使用的 JdbcDaoImpl 是 Spring Security 为咱们提供的 UserDetailsService 的实现,另外 Spring Security 还为咱们提供了 UserDetailsService 另一个实现,InMemoryDaoImpl。

其做用是从数据库中加载 UserDetails 信息。其中已经定义好了加载相关信息的默认脚本,这些脚本也能够经过 JdbcDaoImpl 的相关属性进行指定。关于 JdbcDaoImpl 使用方式会在讲解 AuthenticationProvider 的时候作一个相对详细一点的介绍。

JdbcDaoImpl

JdbcDaoImpl 容许咱们从数据库来加载 UserDetails,其底层使用的是 Spring 的 JdbcTemplate 进行操做,因此咱们须要给其指定一个数据源。此外,咱们须要经过 usersByUsernameQuery 属性指定经过 username 查询用户信息的 SQL 语句;经过 authoritiesByUsernameQuery 属性指定经过 username 查询用户所拥有的权限的 SQL 语句;若是咱们经过设置 JdbcDaoImpl 的 enableGroups 为 true 启用了用户组权限的支持,则咱们还须要经过 groupAuthoritiesByUsernameQuery 属性指定根据 username 查询用户组权限的 SQL 语句。当这些信息都没有指定时,将使用默认的 SQL 语句,默认的 SQL 语句以下所示。

select username, password, enabled from users where username=? -- 根据 username 查询用户信息
select username, authority from authorities where username=? -- 根据 username 查询用户权限信息
select g.id, g.group_name, ga.authority from groups g, groups_members gm, groups_authorities ga where gm.username=? and g.id=ga.group_id and g.id=gm.group_id -- 根据 username 查询用户组权限

 

 

使用默认的 SQL 语句进行查询时意味着咱们对应的数据库中应该有对应的表和表结构,Spring Security 为咱们提供的默认表的建立脚本以下。

create table users(
      username varchar_ignorecase(50) not null primary key,
      password varchar_ignorecase(50) not null,
      enabled boolean not null);

create table authorities (
      username varchar_ignorecase(50) not null,
      authority varchar_ignorecase(50) not null,
      constraint fk_authorities_users foreign key(username) references users(username));
      create unique index ix_auth_username on authorities (username,authority);

create table groups (
  id bigint generated by default as identity(start with 0) primary key,
  group_name varchar_ignorecase(50) notnull);

create table group_authorities (
  group_id bigint notnull,
  authority varchar(50) notnull,
  constraint fk_group_authorities_group foreign key(group_id) references groups(id));

create table group_members (
  id bigint generated by default as identity(start with 0) primary key,
  username varchar(50) notnull,
  group_id bigint notnull,
  constraint fk_group_members_group foreign key(group_id) references groups(id));

 

 

此外,使用 jdbc-user-service 元素时在底层 Spring Security 默认使用的就是 JdbcDaoImpl。

<security:authentication-manager alias="authenticationManager">
      <security:authentication-provider>
         <!-- 基于 Jdbc 的 UserDetailsService 实现,JdbcDaoImpl -->
         <security:jdbc-user-service data-source-ref="dataSource"/>
      </security:authentication-provider>
   </security:authentication-manager>

 

InMemoryDaoImpl

InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中。使用 NameSpace 时,使用 user-service 元素 Spring Security 底层使用的 UserDetailsService 就是 InMemoryDaoImpl。此时,咱们能够简单的使用 user 元素来定义一个 UserDetails。

   <security:user-service>
      <security:user name="user" password="user" authorities="ROLE_USER"/>
   </security:user-service>

如上配置表示咱们定义了一个用户 user,其对应的密码为 user,拥有 ROLE_USER 的权限。此外,user-service 还支持经过 properties 文件来指定用户信息,如:

<security:user-service properties="/WEB-INF/config/users.properties"/>

 

其中属性文件应遵循以下格式:

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

因此,对应上面的配置文件,咱们的 users.properties 文件的内容应该以下所示:

#username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
user=user,ROLE_USER

GrantedAuthority

Authentication 的 getAuthorities() 能够返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。其返回值是一个 GrantedAuthority 类型的数组,每个 GrantedAuthority 对象表明赋予给当前用户的一种权限。GrantedAuthority 是一个接口,其一般是经过 UserDetailsService 进行加载,而后赋予给 UserDetails 的。

GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,若是对应权限不能用字符串表示,则应当返回 null。

Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的全部 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。