首页 > 代码库 > Spring Security:核心组件说明

Spring Security:核心组件说明

理解了Spring Security核心组件说明,Spring Security的后续学习和使用将不再是难题。

1、Spring Security 核心

 

SecurityContextHolder、SecurityContext、Authentication、GrantedAuthority、UserDetails

 

Authentication:代表了Spring Security中的当事人。

SecurityContext:拥有了Authentication、请求相关的信息。

SecurityContextHodler:用于获取SecurityContext。

GrantedAuthority:代表在应用程序中给当事人授予的权限。

UserDetails:用户详细信息。其实就是一个JavaBean。

UserDetailsService:UserDetails相关的业务处理。

 

这几个是Spring Security的核心,其它的API都是围绕这些API展开的,都是为它们服务的。

 

 

 

2、身份认证Authentication

 

2.1、一般的身份认证

从Spring Security核心部分,对Spring Security有一个笼统概念了,那该怎么理解上面说的呢?

 

通常情况下,我们的系统都是这样的:

1、用户输入用户名、密码登录

2、系统对用户名、密码进行验证

3、获取用户上下文信息(角色列表等等)

4、获取相关操作权限

 

对于上面说的前三条,用Spring Security来处理,就是:

1、用户名、密码组合生成一个Authentication对象(也就是UsernamePasswordAuthenticationToken对象)。

2、生成的这个token对象会传递给一个AuthenticationManager对象用于验证。

3、当成功认证后,AuthenticationManager返回一个Authentication对象。

4、接下来,就可以调用

SecurityContextHodler.getContext().setAuthentication(…)。

 

 

为了更好的理解,下面就写一个例子:

 

package com.springsecurity.java.test; import java.io.BufferedReader;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List; import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.BadCredentialsException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder; public class AuthenticationExample {   private static SimpleAuthenticationManager samgr = new SimpleAuthenticationManager();    public static void main(String[] args) {      try {// 用户输入用户名、密码:        BufferedReader in = new BufferedReader(new InputStreamReader(              System.in));        System.out.println("Please enter your username:");        String name = in.readLine();        System.out.println("Please enter your password:");        String password = in.readLine();// 接下来是系统进行身份认证的过程://1、将用户名、密码封装成一个token        Authentication token = new UsernamePasswordAuthenticationToken(              name, password);//2、将token传给AuthenticationManager进行身份认证//3、认证完毕,返回一个认证后的身份:        Authentication result = samgr.authenticate(token);// 认证后,存储到SecurityContext里 :    SecurityContextHolder.getContext().setAuthentication(result);      } catch (Exception ex) {        System.out.println("认证失败");      } // 从SecurityContext读取认证的身份:System.out.println(SecurityContextHolder.getContext()           .getAuthentication());   }} class SimpleAuthenticationManager 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");   } } 
View Code

 

2.2、Web Application中如何进行身份认证呢?

1、用户在首页上点击某个链接

2、后台处理时,先判断是否是要访问一个受保护的资源

3、如果是受保护的资源,判断用户是否登录,是否有这个资源的访问权限

4、如果用户没有登录,返回一个登录页面给用户

5、用户输入username、password,然后登录

6、接下来开始上面的身份认证过程

 

在Web Application环境下,将上述1-4过程由AuthenticationEntryPoint来处理。

 

 

如何存储已认证的用户呢?

    用户要访问另外的资源时,肯定要判断是否有该资源的访问权限,在判断是否有访问权限前,一般要用户先登录系统(要对用户的身份进行认证)的。如果用户已经成功登录,只需要判断是否有访问权即可。

    在一般的Web Application中(不使用Spring Security),我们通常会将用户信息存储到HttpSession中。

    如果在Web Application系统中,加上了Spring Security呢?

做法没什么两样,还是将这个信息存储在HttpSession中。但对于无状态的RESTful Web Service不是这样做的。

 

 

3、授权Authorization(Access Control)

 

身份认证Authentication保证了用户可访问系统。权限认证(授权保证了用户可以访问系统中的资源)。

    在用户的一次资源访问中,这两个过程是不可少的。Access Control就是决定你这个请求是否被允许,它是在身份认证之后,访问资源之前进行的。

 

用户请求-->身份认证-->授权-->资源访问-->响应

 

身份认证过程由AuthenticationManager来处理,授权决定则是由AccessDecisionManager来处理的。

void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;

 

Spring在进行授权时,充分利用了Spring的核心之一:AOP。通过前段时间关于AOP的研究,AOP在程序中一般体现为Filter或者是Interceptor,其中更多的是使用Interceptor。 

 

 

上面的decide方法需要三个参数:

·Authentication就是已经通过认证的Authentication对象。上面的学习已经可以理解。

·Object obj  是代表方法调用(MethodInvocation)或者请求处理(Action的Handler),在接下来的AbstractSecurityInterceptor中说明。

·configAttributes则是相关的特性配置。下面会有说明。

 

AbstractSecurityInterceptor

 

Security中关于授权部分使用了AOP,所以就不得不了解一下AbstractSecurityInterceptor。

 

Interceptor一般都会提供invoke方法。这个类是抽象类,没有提供,使用时用的是它的子类,在子类中提供了invoke方法。而在AccessDecisionManager的decide方法中的第二个参数,就是invoke方法的参数MethodInvocation对象。

 

ConfigAttribute

    其实就是对某个拦截器配置一些访问属性。举个例子:

某个拦截器interceptor,配置访问属性有ROLE_A, ROLE_B,如果一个用户经过认证后他有一个Authentication为ROLE_A,那么他的这个请求就会被拦截器interceptor处理。

 

再说的通俗点就是:设置某个拦截器能够处理哪些身份的用户的请求。

 

AbstractSecurityInterceptor的执行流程

 

1、查找有哪些configAttributes与当前的请求相关联。

2、提交secure object(就是前面说的MethodInvocation对象)、当前的Authentication(就是已经认证好的身份)、以及1中找到的那些configAttributes,提交给AccessDecisionManager用于授权。

3、选择性的改变用户的身份去进行验证。这是因为用户身份的多样性的需要。

4、Secure Object(methodInvocation 对象执行),也就是执行我们在action中写的Handler。

5、如果配置了AfterInvocationManager,那么AfterInvocationManager也会执行的。

 

这个流程是官方文档中说明的,已经很清楚了。为了更清新的了解这个过程,还是查看一下源码吧:

在AbstractSecurityInterceptor的子类MethodSecurityInterceptor中:

public Object invoke(MethodInvocation mi) throws Throwable {// 预先处理        InterceptorStatusToken token = super.beforeInvocation(mi);         Object result;        try {// 真实方法调用,也就是我们写的action调用            result = mi.proceed();        } finally {            super.finallyInvocation(token);        }// afterInvocationManager处理        return super.afterInvocation(token, result);    }

 


这段代码与上面的流程说明对应起来,那就应该是上面说的流程中的1、2、3对应了这段代码中的 预先处理部分。就来看看beforeInvocation:
 

// 参数object就是方法调用protected InterceptorStatusToken beforeInvocation(Object object) {        Assert.notNull(object, "Object was null");        final boolean debug = logger.isDebugEnabled();         if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {            throw new IllegalArgumentException("Security invocation attempted for object "                    + object.getClass().getName()                    + " but AbstractSecurityInterceptor only configured to support secure objects of type: "                    + getSecureObjectClass());        }// 收集与方法调用相关的特性配置        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);         if (attributes == null || attributes.isEmpty()) {            if (rejectPublicInvocations) {                throw new IllegalArgumentException("Secure object invocation " + object +                        " was denied as public invocations are not allowed via this interceptor. "                                + "This indicates a configuration error because the "                                + "rejectPublicInvocations property is set to ‘true‘");            }             if (debug) {                logger.debug("Public object - authentication not attempted");            }             publishEvent(new PublicInvocationEvent(object));             return null; // no further work post-invocation        }         if (debug) {            logger.debug("Secure object: " + object + "; Attributes: " + attributes);        }         if (SecurityContextHolder.getContext().getAuthentication() == null) {            credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",                    "An Authentication object was not found in the SecurityContext"), object, attributes);        }// 取得该用户已验证的身份        Authentication authenticated = authenticateIfRequired();         // Attempt authorization        try {           // 进行用户授权this.accessDecisionManager.decide(authenticated, object, attributes);        }        catch (AccessDeniedException accessDeniedException) {            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));             throw accessDeniedException;        }         if (debug) {            logger.debug("Authorization successful");        }         if (publishAuthorizationSuccess) {            publishEvent(new AuthorizedEvent(object, attributes, authenticated));        }// 切换为其他身份        // Attempt to run as a different user        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);         if (runAs == null) {            if (debug) {                logger.debug("RunAsManager did not change Authentication object");            }             // no further work post-invocation            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);        } else {            if (debug) {                logger.debug("Switching to RunAs Authentication: " + runAs);            }            SecurityContext origCtx = SecurityContextHolder.getContext();            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());            SecurityContextHolder.getContext().setAuthentication(runAs);             // need to revert to token.Authenticated post-invocation            return new InterceptorStatusToken(origCtx, true, attributes, object);        }    }

 

4、Localization异常消息的国际化 

 

Spring Security分为了身份认证和授权两个过程,上面已经讲明 。在这两个过程中,发生异常是在所难免的。Spring为此提供了异常消息的国际化支持。

 

Spring Security:核心组件说明