Spring Security - 核心类简介 - 获得登录用户的相关信息(转)

核心类简介

目录

  1. Authentication
  2. SecurityContextHolder
  3. AuthenticationManager和AuthenticationProvider
    3.1 认证成功后清除凭证
  4. UserDetailsService
    4.1 JdbcDaoImpl
    4.2 InMemoryDaoImpl
  5. GrantedAuthority

1. Authentication

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

2. 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。但是往往我们需要在程序中获取当前用户的相关信息,比如最常见的是获取当前登录用户的用户名。在程序的任何地方,通过如下方式我们可以获取到当前用户的用户名。

publicString getCurrentUsername() {
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    if(principalinstanceofUserDetails) {
        return((UserDetails) principal).getUsername();
    }
    if(principalinstanceofPrincipal) {
        return((Principal) principal).getName();
    }
    returnString.valueOf(principal);
}

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

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

此外,调用SecurityContextHolder.getContext()获取SecurityContext时,如果对应的SecurityContext不存在,则Spring Security将为我们建立一个空的SecurityContext并进行返回。

3. AuthenticationManager和AuthenticationProvider

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

public 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了。

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

如果我们没有使用NameSpace,那么我们就应该在ApplicationContext中声明一个ProviderManager。

3.1 认证成功后清除凭证

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

<!-- 认证管理器 -->
<authentication-manager alias="authenticationManager" erase-credentials="false">
      <authentication-provider user-service-ref="cachingUserDetailsService" />
</authentication-manager>

<!-- 可以缓存 UserDetails 的 UserDetailsService -->
<beans:bean id="cachingUserDetailsService" class="org.springframework.security.config.authentication.CachingUserDetailsService">
      <!-- 真正加载 UserDetails 的 UserDetailsService -->
      <beans:constructor-arg ref="userDetailsService"/>
      <!-- 缓存 UserDetails 的 UserCache -->
      <beans:property name="userCache">
            <beans:bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache">
                  <!-- 用于真正缓存的 Ehcache 对象 -->
                  <beans:property name="cache" ref="ehcache4UserDetails"/>
            </beans:bean>
      </beans:property>
</beans:bean>

<!-- 将使用默认的 CacheManager 创建一个名为 ehcache4UserDetails 的 Ehcache 对象 -->
<beans:bean id="ehcache4UserDetails" class="org.springframework.cache.ehcache.EhCacheFactoryBean"/>

<!-- 从数据库加载 UserDetails 的 UserDetailsService -->
<beans:bean id="userDetailsService" class="org.spring.security.userdetails.UserDetailsServiceCustom" />

4. 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的时候做一个相对详细一点的介绍。

4.1 JdbcDaoImpl

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

-- 根据username查询用户信息
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 

使用默认的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) referencesusers(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>

4.2 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

5. GrantedAuthority

Authentication的getAuthorities()可以返回当前Authentication对象拥有的权限,即当前用户拥有的权限。其返回值是一个GrantedAuthority类型的数组,每一个GrantedAuthority对象代表赋予给当前用户的一种权限。GrantedAuthority是一个接口,其通常是通过UserDetailsService进行加载,然后赋予给UserDetails的。

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

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

  • (注:本文是基于Spring Security3.1.6所写)

(注:原文地址:http://elim.iteye.com/blog/2155786

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容