spring security 核心 --authentication

什么是Spring Security验证?

  1. 提示用户输入用户名和密码进行登录。

  2. 该系统 (成功) 验证该用户名的密码正确。

  3. 获取该用户的环境信息 (他们的角色列表等).

  4. 为用户建立安全的环境。

  5. 用户进行,可能执行一些操作,这是潜在的保护的访问控制机制,检查所需权限,对当前的安全的环境信息的操作。

前三个项目构成的验证过程,所以我们将看看这些是如何发生在Spring Security中的。

  1. 用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken (一个Authentication接口的实例, 我们之前看到的).

  2. 令牌传递到AuthenticationManager实例进行验证。

  3. 该AuthenticationManager完全填充Authentication实例返回成功验证。

  4. 安全环境是通过调用 SecurityContextHolder.getContext().setAuthentication(…​), 传递到返回的验证对象建立的。

从这一点上来看,用户被认为是被验证的。spring security 验证的经典例子

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

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");
}
}

直接设置SecurityContextHolder的内容
事实上,Spring Security不介意你如何把Authentication对象包含在SecurityContextHolder内。唯一的关键要求是SecurityContextHolder包含Authentication在AbstractSecurityInterceptor之前(我们会看到更多的版本)需要用户授权操作。
你可以(很多用户都这样做)写一个自己的过滤器或MVC控制器来提供验证系统的交互,这些都不是基于Spring Security的。比如,你也许使用容器管理认证,从ThreadLocal或JNDI里获得当前用户信息。或者,你的公司可能有一个遗留系统,它是一个企业标准,你不能控制它。这种情况下,很容易让Spring Security工作,也能提供验证能力。你所需要的就是写一个过滤器(或等价物)从指定位置读取第三方用户信息,把它放到SecurityContextHolder里。在这种情况下,你还需要考虑的事情通常是由内置的认证基础设施自动照顾。


spring security 支持很多种的认证模式,这些验证绝大多数都是由第三方提供,或由相关的标准组织,如互联网工程任务组开发。并且spring security 也提供自己的一组认证功能。
从这些大量的认证模式中抽象封装就有了spring security的认证模块

常见的身份验证有:

  1. HTTP BASIC 认证头 (基于 IETF RFC-based 标准)
  2. HTTP Digest 认证头 ( IETF RFC-based 标准)
  3. HTTP X.509 客户端证书交换 ( IETF RFC-based 标准)
  4. LDAP (一个非常常见的方法来跨平台认证需要, 尤其是在大型环境)
  5. Form-based authentication (用于简单的用户界面)
  6. OpenID 认证
  7. Authentication based on pre-established request headers (such as Computer Associates Siteminder) 根据预先建立的请求有进行验证
  8. JA-SIG Central Authentication Service (CAS,一个开源的SSO系统 )
  9. Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (Spring远程协议)
  10. Automatic "remember-me" authentication (你可以勾选一个框以避免预定的时间段再认证)
  11. Anonymous authentication (让每一个未经验证的访问自动假设为一个特定的安全标识)
  12. Run-as authentication (在一个访问应该使用不同的安全标识时非常有用)
  13. Java Authentication and Authorization Service (JAAS)
  14. JEE container autentication (所以如果愿你以可以任然使用容器管理的认证)

身份验证的一些理解
首先,http basic 和http digest ,http 的基本和摘要两种认证模式,这两种模式是http 协议规范里面的两种认证机制,浏览器对这两种机制都会有一个很好的支持。
基本认证模式

基本认证模式
客户向服务器发送请求,服务器返回401(未授权),要求认证。401消息的头里面带了挑战信息。realm用以区分要不同认证的部分。客户端收到401后,将用户名密码和挑战信息用BASE64加密形成证书,发送回服务器认证。语法如下:
challenge = "Basic" realm
credentials = "Basic" basic-credentials
示例:
认证头: WWW-Authenticate: Basic realm="zhouhh@mydomain.com"
证书:Authorization: Basic QsdfgWGHffuIcaNlc2FtZQ== 【虎.无名,格式如Authorization:Basic base64(username:password)。。。但是没定义如何处理realm信息,简单处理,可以针对每个realm分别有一组user:pass信息。进一步,可以走md5摘要,但这些已经超出标准,估计不被浏览器支持。

摘要模式和基本模式差不多,这两个模式的核心都是认证头和证书,只是摘要要复杂一些,并且摘要模式是一个md5 摘要,而basic 只是用base64 编码了一下,basic 的使用需要配合https 协议,要不然基本就是明文传输。

为了防止重放攻击,采用摘要访问认证。在客户发送请求后,收到一个401(未授权)消息,包含一个Challenge。消息里面有一个唯一的字符串:nonce,每次请求都不一样。客户将用户名密码和401消息返回的挑战一起加密后传给服务器。这样即使有窃听,他也无法通过每次认证,不能重放攻击。Http并不是一个安全的协议。其内容都是明文传输。因此不要指望Http有多安全。语法如下:
challenge = "Digest" digest-challenge
digest-challenge = 1#( realm | [ domain ] | nonce | [opaque] |[stale] | [algorithm] | [qop-options] | [auth-param] )
domain = "domain" "=" <"> URI ( 1*SP URI ) <">
URI = absoluteURI | abs_path
nonce = "nonce" "=" nonce-value
nonce-value = quoted-string
opaque = "opaque" "=" quoted-string
stale = "stale" "=" ( "true" | "false" )
algorithm = "algorithm" "=" ( "MD5" | "MD5-sess" | token )
qop-options = "qop" "=" <"> 1#qop-value <">
qop-value = "auth" | "auth-int" | token
realm:让客户知道使用哪个用户名和密码的字符串。不同的领域可能密码不一样。至少告诉用户是什么主机做认证,他可能会提示用哪个用户名登录,类似一个Email。
domain:一个URI列表,指示要保护的域。可能是一个列表。提示用户这些URI采用一样的认证。如果为空或忽略则为整个服务器。
nonce:随机字符串,每次401都不一样。跟算法有关。算法类似Base64加密:time-stamp H(time-stamp ":" ETag ":" private-key) 。time-stamp为服务器时钟,ETag为请求的Etag头。private-key为服务器知道的一个值。
opaque:服务器产生的由客户下去请求时原样返回。最好是Base64串或十六进制字符串。
auth-param:为扩展用的,现阶段忽略。
其他域请参考RFC2617。授权头语法:
credentials = "Digest" digest-response
digest-response = 1#( username | realm | nonce | digest-uri | response | [ algorithm ] | [cnonce] |
[opaque] | [message-qop] | [nonce-count] | [auth-param] )
username = "username" "=" username-value
username-value = quoted-string
digest-uri = "uri" "=" digest-uri-value
digest-uri-value = request-uri ; As specified by HTTP/1.1
message-qop = "qop" "=" qop-value
cnonce = "cnonce" "=" cnonce-value
cnonce-value = nonce-value
nonce-count = "nc" "=" nc-value
nc-value = 8LHEX
response = "response" "=" request-digest
request-digest = <"> 32LHEX <">
LHEX = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
response:加密后的密码
digest-uri:拷贝Request-Line,用于Proxy
cnonce:如果qop设置,才设置,用于双向认证,防止攻击。
nonce-count:如果服务器看到同样的计数,就是一次重放。
示例:
401响应: HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
realm="testrealm@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
再次请求:
Authorization: Digest username="Mufasa",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001,
cnonce="0a4f113b",
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"

下面是一个http basic 的事例,是转载别的博客上的:
在浏览网页时候,浏览器会弹出一个登录验证的对话框,如下图,这就是使用HTTP基本认证。


20171130134526044.png

1、 客户端发送http request 给服务器,服务器验证该用户是否已经登录验证过了,如果没有的话,
服务器会返回一个401 Unauthozied给客户端,并且在Response 的 header “WWW-Authenticate” 中添加信息。 如下


20171130134726538.png

2、:浏览器在接受到401 Unauthozied后,会弹出登录验证的对话框。用户输入用户名和密码后,
浏览器用BASE64编码后,放在Authorization header中发送给服务器。如下图:
20171130134815100.png

openId和 Oauth 很像都是用于提供第三方登录。


SecurityContextHolder, SecurityContext和Authentication 对象

最根本的对象是SecurityContextHolder。我们把当前应用程序的当前安全环境的细节存储到它里边了, 它也包含了应用当前使用的主体细节。默认情况下SecurityContextHolder使用ThreadLocal存储这些信息, 这意味着,安全环境在同一个线程执行的方法一直是有效的, 即使这个安全环境没有作为一个方法参数传递到那些方法里。这种情况下使用ThreadLocal是非常安全的,只要记得在处理完当前主体的请求以后,把这个线程清除就行了。当然,Spring Security自动帮你管理这一切了, 你就不用担心什么了。

有些程序并不适合使用ThreadLocal,因为它们处理线程的特殊方法。比如Swing客户端也许希望Java Virtual Machine里所有的线程 都使用同一个安全环境。SecurityContextHolder可以配置启动策略来指定你希望上下文怎么被存储。对于一个独立的应用程序,你会使用SecurityContextHolder.MODE_GLOBAL策略。其他程序可能也想由安全线程产生的线程也承担同样的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现。你可以通过两种方式更改默认的SecurityContextHolder.MODE_THREADLOCAL模式。第一个是设置系统属性,第二个是调用SecurityContextHolder的静态方法。大多数应用程序不需要修改默认值,但是如果你想要修改,可以看一下SecurityContextHolder的JavaDocs中的详细信息了解更多。

当前用户获取信息

我们在SecurityContextHolder内存储目前与应用程序交互的主要细节。Spring Security使用一个Authentication对象来表示这些信息。 你通常不需要创建一个自我认证的对象,但它是很常见的用户查询的Authentication对象。你可以使用以下代码块-从你的应用程序的任何部分-获得当前身份验证的用户的名称,例如:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

通过调用getContext()返回的对象是SecurityContext接口的实例。这是保存在线程本地存储中的对象。我们将在下面看到,大多数的认证机制以Spring Security返回UserDetails实例为主。

The UserDetailsService
从上面的代码片段中还可以看出一件事,就是你可以从Authentication对象中获得安全主体。这个安全主体就是一个Object。大多数情况下,可以强制转换成UserDetails对象 。 UserDetails是一个Spring Security的核心接口。它代表一个主体,是扩展的,而且是为特定程序服务的。 想一下UserDetails章节,在你自己的用户数据库和如何把Spring Security需要的数据放到SecurityContextHolder里。为了让你自己的用户数据库起作用,我们常常把UserDetails转换成你系统提供的类,这样你就可以直接调用业务相关的方法了(比如 getEmail(), getEmployeeNumber()等等)。

现在,你可能想知道,我应该什么时候提供这个UserDetails对象呢?我怎么做呢?我想你说这个东西是声明式的,我不需要写任何代码,怎么办?简单的回答是,这里有一个特殊的接口叫UserDetailsService。这个接口里的唯一的一个方法,接收String类型的用户名参数,返回UserDetails:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这是Spring Security用户加载信息的最常用的方法并且每当需对用户的信息时你会看到它使用的整个框架。

成功认证后,UserDetails用于构建存储在SecurityContextHolder(详见 以下)的Authentication对象。好消息是,我们提供了一些UserDetailsService的实现,包括一个使用内存映射(InMemoryDaoImpl)而另一个使用JDBC(JdbcDaoImpl)。大多数用户倾向于写自己的,常常放到已有的数据访问对象(DAO)上使用这些实现,表示他们的雇员,客户或其他企业应用中的用户。记住这个优势,无论你用UserDetailsService返回的什么数据都可以通过SecurityContextHolder获得,就像上面的代码片段讲的一样。

GrantedAuthority

除了主体,另一个Authentication提供的重要方法是getAuthorities()。这个方法提供了GrantedAuthority对象数组。毫无疑问,GrantedAuthority是赋予到主体的权限。这些权限通常使用角色表示,比如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。这些角色会在后面,对web验证,方法验证和领域对象验证进行配置。Spring Security的其他部分用来拦截这些权限,期望他们被表现出现。GrantedAuthority对象通常是使用UserDetailsService读取的。

通常情况下,GrantedAuthority对象是应用程序范围下的授权。它们不会特意分配给一个特定的领域对象。因此,你不能设置一个GrantedAuthority,让他有权限展示编号54的Employee对象,因为如果有成千上万的这种授权,你会很快用光内存(或者,至少,导致程序花费大量时间去验证一个用户)。当然,Spring Security被明确设计成处理常见的需求,但是你最好别因为这个目的使用项目领域模型安全功能。

Spring Security主要由以下几部分组成的:

  • SecurityContextHolder, 提供几种访问 SecurityContext的方式。

  • SecurityContext, 保存Authentication信息和请求对应的安全信息。

  • Authentication, 展示Spring Security特定的主体。

  • GrantedAuthority, 反应,在应用程序范围你,赋予主体的权限。

  • UserDetails,通过你的应用DAO,提供必要的信息,构建Authentication对象。

  • UserDetailsService, 创建一个UserDetails,传递一个 String类型的用户名(或者证书ID或其他).


The AuthenticationManager, ProviderManager and AuthenticationProvider

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    ...
    private List<AuthenticationProvider> providers = Collections.emptyList();
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication = true;

    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, null);
    }
...
    // ~ Methods
    // ========================================================================================================
    ...
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to
                // invalid account status
                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to
                // calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already
                // handled the request
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }
...
    /**
     * Copies the authentication details from a source Authentication object to a
     * destination one, provided the latter does not already have one set.
     *
     * @param source source authentication
     * @param dest the destination authentication object
     */
    private void copyDetails(Authentication source, Authentication dest) {
        if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;

            token.setDetails(source.getDetails());
        }
    }
...
}
public interface AuthenticationProvider {
    // ~ Methods
    // ========================================================================================================
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

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

推荐阅读更多精彩内容