美文网首页
认证授权 源码 Spring Security 1 --Arch

认证授权 源码 Spring Security 1 --Arch

作者: _River_ | 来源:发表于2021-05-07 09:25 被阅读0次
    学习连接(阿里中间件工程师 徐靖峰 徐老师的相关文章):
    https://www.cnkirito.moe/tags/Spring-Security/
    

    0:架构概览图

    下面文章会先以 Architecture First 的方式讲解架构:
    
    如果对 Spring Security 的这些概念感到理解不能,因为这是 Architecture First 导致的必然结果,
    后续的文章会秉持 Code First 的理念,陆续详细地讲解这些实现类的使用场景,源码分析。
    
    另外,一些 Spring Security 的过滤器还未囊括在架构概览中:
    如将表单信息包装成 UsernamePasswordAuthenticationToken 的过滤器
    考虑到这些虽然也是架构的一部分,但是真正重写他们的可能性较小,所以打算放到后面的章节讲解。
    
    1:核心组件 SecurityContextHolder
    SecurityContextHolder 用于存储安全上下文(security context)的信息。
    当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限… 这些都被保存在 SecurityContextHolder 中。
    
    SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。
    看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。
    
    在 web 场景下使用 Spring Security:
    在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
    
        //获取当前用户的信息
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            String username = ((UserDetails)principal).getUsername();
        } else {
             String username = principal.toString();
        }
    
    getAuthentication()返回了认证信息,再次 getPrincipal() 返回了身份信息,
    UserDetails 便是 Spring 对身份信息封装的一个接口。
    
    Authentication 和 UserDetails 后续介绍
    
    2:核心组件 Authentication
    Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。
    可以见得,Authentication 在 spring security 中是最高级别的身份 / 认证的抽象。
    
    由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
    
    authentication.getPrincipal() 返回了一个 Object,使用 instanceof 判断类型,
    强转成对应的具体实现类(Spring Security 中最常用的 UserDetails)。
    
    public interface Authentication extends Principal, Serializable {
    
         //getPrincipal(),最重要的身份信息,
        //大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。
        Object getPrincipal();
        
        //getDetails(),详细信息,web 应用中的实现接口通常为 WebAuthenticationDetails,
        //它记录了访问者的 ip 地址和 sessionId 的值。
        Object getDetails();
    
         //getAuthorities(),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,
        //通常是代表权限信息的一系列字符串。
        Collection<? extends GrantedAuthority> getAuthorities();
    
        //getCredentials(),密码信息,用户输入的密码字符串,
        //在认证过后通常会被移除,用于保障安全。
        Object getCredentials();
    
        boolean isAuthenticated();
    
        void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    }
    
    3:Spring Security 是如何完成身份认证的?
    1:用户名和密码被过滤器获取到,封装成 Authentication, 
        通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
    
    2:AuthenticationManager 身份管理器负责验证这个 Authentication
    
    3:认证成功后,AuthenticationManager 身份管理器
        返回一个被填充满了信息的:包括上面提到的权限信息,身份信息,细节信息,
        但密码通常会被移除的Authentication 实例。
    
    4:SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication,
        通过 SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到其中。
    
    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 {
                    //name 为 principal   password 为 credentials
                    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"));
        }
    
        @Override
        public Authentication authenticate(Authentication auth) throws AuthenticationException {
            //auth.getName()  就是调用 Principal中getName() 的方法  获取名称
            if (auth.getName().equals(auth.getCredentials())) {
                return new UsernamePasswordAuthenticationToken(auth.getName(),
                        auth.getCredentials(), AUTHORITIES);
            }
            throw new BadCredentialsException("Bad Credentials");
        }
    }
    
    4:核心组件 AuthenticationManager
    AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,
    因为在实际需求中,我们可能会允许用户使用用户名 + 密码登录,同时允许用户使用邮箱 + 密码,手机号码 + 密码登录,
    甚至,可能允许用户使用指纹登录.
    
    因此AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,
    存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
    
    核心的认证入口始终只有一个:AuthenticationManager
    以下不同的认证方式  则对应了三个 AuthenticationProvider:
                        1:用户名 + 密码(UsernamePasswordAuthenticationToken),
                        2:邮箱 + 密码
                        3:手机号码 + 密码登录
    
    //  暂时只是阅读ProviderManager类的核心代码 
    
    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 信息,则直接返回
    
            //ProviderManager 中的 List,会依照次序去认证,认证成功则立即返回,若认证失败则返回 null,
            //下一个 AuthenticationProvider 会继续尝试认证,
            //如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。
    
            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;
        }
    }
    
    5:上述总结
    认证相关的核心类其实都已经介绍完毕
    身份信息的存放容器 SecurityContextHolder
    身份信息的抽象 Authentication
    身份认证器 AuthenticationManager 及其认证流程。
    
    后续会详细介绍各个主要的  类与接口
    
    6:AuthenticationProvider
    AuthenticationProvider 最最最常用的一个实现便是 DaoAuthenticationProvider。
    顾名思义:Dao正是数据访问层的缩写,也暗示了这个身份验证器的实现思路。
    
    DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,
    如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)
    
    按照最直观的思路,如何去认证一个用户?
    用户前台提交了用户名和密码,而数据库中保存了用户名和密码,
    认证便是对比同一个用户名,提交的密码和保存的密码是否相同。
    
    在 Spring Security 中。
    1:用户提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken;
    2:在retrieveUser方法 通过 username 获取 UserDetails
    3:在additionalAuthenticationChecks 方法对比 UsernamePasswordAuthenticationToken与UserDetails
    
    //  retrieveUser 获取 UserDetails
    //参数1:username  通过  UserDetailsService 方法获取UserDetails
    //参数2:authentication   防止计时攻击(不用管)
    protected final UserDetails retrieveUser
        (String username, UsernamePasswordAuthenticationToken authentication)   
    
     // UsernamePasswordAuthenticationToken 和 UserDetails 密码的比较
     //如果这个 void 方法没有抛异常,则认为比对成功。
     //比对密码的过程,用到了 PasswordEncoder(加密) 和 SaltSource(加盐)
     protected void additionalAuthenticationChecks
        (UserDetails userDetails,  UsernamePasswordAuthenticationToken authentication)
    

    7:UserDetails 与 UserDetailsService

    UserDetails接口:
    它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
    
    它和 Authentication 接口很类似,比如它们都拥有 username,authorities,
    区分他们也是本文的重点内容之一。
    
    Authentication 的 getCredentials():用户提交的密码;
    UserDetails 中的 getPassword() :是用户正确的密码;
     
    认证器其实就是对这两者的比对。
    
    特别注意:
    Authentication 中的 getAuthorities()实际是由 UserDetails 的 getAuthorities() 传递而形成的。
    
    Authentication 接口中的 getUserDetails() 方法:
        其中的 UserDetails 用户详细信息便是经过了 
        AuthenticationProvider (DaoAuthenticationProvider)之后被填充的。
    
    public interface UserDetails extends Serializable {
    
       Collection<? extends GrantedAuthority> getAuthorities();
    
       String getPassword();
    
       String getUsername();
    
       boolean isAccountNonExpired();
    
       boolean isAccountNonLocked();
    
       boolean isCredentialsNonExpired();
    
       boolean isEnabled();
    }
    
    
     注意:UserDetailsService 和 AuthenticationProvider 的区别
    
    UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已:
    UserDetailsService 常见的实现类有 :
        1:JdbcDaoImpl 从数据库加载用户
        2:InMemoryUserDetailsManager 从内存中加载用户,
    也可以自己实现 UserDetailsService,通常这更加灵活。   
    
    public interface UserDetailsService {  
        UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
     }
    

    相关文章

      网友评论

          本文标题:认证授权 源码 Spring Security 1 --Arch

          本文链接:https://www.haomeiwen.com/subject/kdnkdltx.html