Spring Security 入门学习(一)

话不多说,程序员都是从一个helloworld开始

public class AuthHelloWorld {
    private static AuthenticationManager manager = new SampleAuthenticationManager();
    public static void main(String[] args) throws Exception {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        while (true) {
            System.out.println("用户名:");
            String name = in.readLine();
            System.out.println("密码:");
            String password = in.readLine();
            try {
                // 1.将用户名密码封装成Authentication
                Authentication request = new UsernamePasswordAuthenticationToken(name, password);
                // 2.AuthenticationManager 身份管理器验证这个Authentication
                Authentication result = manager.authenticate(request);
                // 4.SecurityContextHolder填充Authentication
                SecurityContextHolder.getContext().setAuthentication(result);
                break;
            } catch (AuthenticationException e) {
                System.out.println("验证失败: " + e.getMessage());
            }
        }
        System.out.println("验证成功: " +
                SecurityContextHolder.getContext().getAuthentication().getName());
    }
}

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 {
        //3 认证成功后,返回Authentication实例。
        if (auth.getName().equals(auth.getCredentials())) {
            return new UsernamePasswordAuthenticationToken(auth.getName(),
                    auth.getCredentials(), AUTHORITIES);
        }
        throw new BadCredentialsException("Bad Credentials");
    }
}

1.Spring Security的一次验证过程是怎么样的?

上面的测试helloworld代码来表现主要分四个阶段:

1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2. AuthenticationManager身份管理器负责验证上面封装的Authentication。

3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。

4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到上下文。

2.核心组件介绍

先抛个类图

2.1 SecurityContextHolder

SecurityContextHolder用于存储安全上下文的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都将被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder的策略则需要被替换。

获取当前用户的信息:因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息,一个典型的获取当前登录用户的姓名的例子如下:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof UserDetails){
    String username=((UserDetails)authentication).getUsername();
} else {
        String username=authentication.toString();
}

2.2 Authentication

Authentication 是一个接口,实现类都会定义 authorities,credentials,details,principal,authenticated 等字段,具体含义如下:

在验证前,principal 填充的是用户名,credentials 填充的是密码,detail 填充的是用户的 IP 或者经纬度之类的信息。通过验证后,Spring Security 对 Authentication 重新注入,principal 填充用户信息(包含用户名、年龄等), authorities 会填充用户的角色信息,authenticated 会被设置为 true。重新注入的 Authentication 会被填充到 SecurityContext 中。

看看源码定义

package org.springframework.security.core;
 
public interface Authentication extends Principal, Serializable {
    //获取用户权限,一般情况下获取到的是用户的角色信息
    Collection<? extends GrantedAuthority> getAuthorities(); 

    //获取证明用户认证的信息,通常情况下获取到的是密码等信息。
    Object getCredentials();

    //获取用户的额外信息,比如 IP 地址、经纬度等。
    Object getDetails();

    //获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)。
    Object getPrincipal();
    
    //获取当前 Authentication 是否已认证。
    boolean isAuthenticated();
     
    //设置当前 Authentication 是否已认证。
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

2.3 AuthenticationManager

初次接触Spring Security的朋友相信会被AuthenticationManager,ProviderManager ,AuthenticationProvider …这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录,所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List<AuthenticationProvider>列表,存放多种认证方式,实际上这是委托者模式的应用。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider,在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。

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;
       Authentication result = null;
 
       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);
 
             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                 //移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
             //发布登录成功事件(扩展点)
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
       }
       ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       //发布登录失败的事件(扩展点)
       prepareException(lastException, authentication);
       throw lastException;
    }
}

ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。

到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。在上面的代码中我们发现了两个扩展点,在认证成功和失败后会发出对应的event出来,可自己实现对应的handler接收对应的event进行处理

下面来介绍下AuthenticationProvider接口的具体实现

2.4 DaoAuthenticationProvider

AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路

按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。

如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

2.4 UserDetails与UserDetailsService

public interface UserDetails extends Serializable {
    //1.权限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    //2.用户正确的密码区别于用户输入Authentication的的getCredentials() 
    String getPassword();
    //3.用户名
    String getUsername();
    //4.用户是否过期
    boolean isAccountNonExpired();
    //5.是否锁定   
    boolean isAccountNonLocked();
    //6.用户密码是否过期   
    boolean isCredentialsNonExpired();
    //7.账号是否可用(可理解为是否删除)
    boolean isEnabled();
}

它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。

//负责从特定的地方(通常是数据库)加载用户信息
public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混 UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。

OK,看到这里,希望大家对spring security的工作机制有了一定的认识。

我以前没有使用过spring security,目前正好公司项目应用到,自己也做了一次学习,把学习的过程记录下来备忘,也希望能帮助需要入门和了解的同学

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值