美文网首页
JWT Spring-security

JWT Spring-security

作者: 骑驴追地瓜 | 来源:发表于2021-03-08 18:03 被阅读0次

    JWT设计原理 JWT结合spring-security在项目中的应用

    JWT译文

    • 什么是JWT
    whatIsJWT.jpg
    1. 开放标准
    2. 数字签名 支持HMAC,RSA,ECDSA加密
    3. 验签可以保证token的完整性即当token内容被篡改的时候可以通过验签发现
    4. 当使用加密后可以保证token内容不外泄,仅持有私钥的一方才能将token解开
    
    • 什么时候用JWT
    whenUseJWT.jpg
    1. 鉴权 支持单点登录 开销小 方便跨域
    2. 信息交换 JWT支持加密签名 所以可以安全的传递信息 可做验签和解密验证发送方是否可靠
    
    • JWT的标准结构应该是什么样的
    whatIsStructure.jpg
    1. JWT分为三段 头信息  负载信息  签名
    2. 头信息 通常由签名算法+令牌类型组成
    3. 中部有效负载
        1. 推荐添加到期时间和主题等信息
        2. 可以任意添加信息 但是注意如果非加密方式的token  建议token内不要包含敏感信息  因为token是暴露在外的
    4. 签名 需要将头信息和负载内容一起做签名 验签的时候可以避免信息被篡改
    

    SPRING-SECURITY译文

    • spring-security

    • 特性


      features.jpg
      1. 支持身份验证,授权,防范常见攻击
      2. 支持集成
    • 基础组件


      component.jpg
      • SecurityContextHolder 存储和获取验证后信息
        SecurityContextHolder.getContext().getAuthentication();
      
      • SecurityContext 从SecurityContextHolder中获得的上下文信息 包含认证信息
      • Authentication 不同阶段的鉴权对象 如:鉴权后的当前登陆人或鉴权前的PreAuthenticatedAuthenticationToken(预处理拦截器先处理得到预处理token再调用AuthenticationManager得到最终token)
      • GrantedAuthority 授予鉴权对象的权限 如:角色 范围等
      • AuthenticationManager 具体Filter如何执行身份验证的API
      • ProviderManager 是AuthenticationManager的具体实现
        • 首先实现AuthenticationProvider,注意里面的support方法 决定了Provider到底处理那种类型的Authentication,如上第二点所说Authentication 存在多种类型
      public interface AuthenticationProvider {
      // ~ Methods
      // ========================================================================================================
      
      /**
       * Performs authentication with the same contract as
       * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
       * .
       *
       * @param authentication the authentication request object.
       *
       * @return a fully authenticated object including credentials. May return
       * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
       * authentication of the passed <code>Authentication</code> object. In such a case,
       * the next <code>AuthenticationProvider</code> that supports the presented
       * <code>Authentication</code> class will be tried.
       *
       * @throws AuthenticationException if authentication fails.
       */
      Authentication authenticate(Authentication authentication)
              throws AuthenticationException;
      
      /**
       * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the
       * indicated <Code>Authentication</code> object.
       * <p>
       * Returning <code>true</code> does not guarantee an
       * <code>AuthenticationProvider</code> will be able to authenticate the presented
       * instance of the <code>Authentication</code> class. It simply indicates it can
       * support closer evaluation of it. An <code>AuthenticationProvider</code> can still
       * return <code>null</code> from the {@link #authenticate(Authentication)} method to
       * indicate another <code>AuthenticationProvider</code> should be tried.
       * </p>
       * <p>
       * Selection of an <code>AuthenticationProvider</code> capable of performing
       * authentication is conducted at runtime the <code>ProviderManager</code>.
       * </p>
       *
       * @param authentication
       *
       * @return <code>true</code> if the implementation can more closely evaluate the
       * <code>Authentication</code> class presented
       */
      boolean supports(Class<?> authentication);
      }
      
       - 其次ProviderManager的authenticate会遍历所有Provider(getProviders),然后找到上面提到的支持当前Authentication类型的Provider做处理
      
      public Authentication authenticate(Authentication authentication)
              throws AuthenticationException {
          Class<? extends Authentication> toTest = authentication.getClass();
          AuthenticationException lastException = null;
          AuthenticationException parentException = null;
          Authentication result = null;
          Authentication parentResult = null;
          boolean debug = logger.isDebugEnabled();
      
          for (AuthenticationProvider provider : getProviders()) {
              if (!provider.supports(toTest)) {
                  continue;
              }
      
              if (debug) {
                  logger.debug("Authentication attempt using "
                          + provider.getClass().getName());
              }
      
      • AuthenticationProvider ProviderManager众多执行者中的一个,如上面所讲,满足类型的AuthenticationProvider将被执行

      • AuthenticationEntryPoint 对于鉴权过程中如异常等响应的统一处理

      • AbstractAuthenticationProcessingFilter


        abstractAuthFilter.jpg
        • 以UsernamePasswordAuthenticationFilter为例,主要是实现attemptAuthentication方法将request中的参数进行封装,变为Authentication,再传递给下游AuthenticationManager
      • DaoAuthenticationProvider

        daoAuth.jpg
        • DaoAuthenticationProvider会从UserDetailsService中加载用户信息,然后与传递过来的用户名密码进行比较
        //如何定义DaoAuthenticationProvider及注入UserDetailsService
        //继承WebSecurityConfigurerAdapter并重写configure方法
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              //第一次登陆账号密码验证Provider
              //默认使用BCryptPasswordEncoder比对加密后的密码  daoProvider.setPasswordEncoder();
              //验证方法为spring-security内部提供的DaoAuthenticationProvider.additionalAuthenticationChecks
              DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
              daoProvider.setUserDetailsService(jwtUserDetailsService);
              daoProvider.setPasswordEncoder(new Md5PasswordEncoder());
              //定义两个Provider  daoProvider负责UserNameAndPasswordToken登录验证
              auth.authenticationProvider(daoProvider);
          }
        
      • UserDetailsService 获取当前登录用户信息,实现UserDetailsService然后返回UserDetails

      public interface UserDetailsService {
      // ~ Methods
      // ========================================================================================================
      
      /**
       * Locates the user based on the username. In the actual implementation, the search
       * may possibly be case sensitive, or case insensitive depending on how the
       * implementation instance is configured. In this case, the <code>UserDetails</code>
       * object that comes back may have a username that is of a different case than what
       * was actually requested..
       *
       * @param username the username identifying the user whose data is required.
       *
       * @return a fully populated user record (never <code>null</code>)
       *
       * @throws UsernameNotFoundException if the user could not be found or the user has no
       * GrantedAuthority
       */
      UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
      }
      
      
      • FilterInvocationSecurityMetadataSource 为当前请求的URL打上一些标签,如:当前的URL需要什么资源可以访问,ConfigAttribute为接口可以自己定义实现
        @Override
        public Collection<ConfigAttribute> getAttributes(Object o) {  
            //FilterInvocation filterInvocation=Object o; 获取当前request
            //当前URL的特殊标签  
            //获取什么资源可以允许当前request然后将资源id封装后返回
        }
      
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
           //全局标签
            return Collections.emptyList();
        }
      
        @Override
        public boolean supports(Class<?> aClass) {
            //什么类型的请求可以走此封装
            return FilterInvocation.class.isAssignableFrom(aClass);
        }
      
      • AccessDecisionManager 授权决策接口 跟FilterInvocationSecurityMetadataSource配套使用
      //根据之前提到的AuthenticationManager封装的Authentication中的角色信息及FilterInvocationSecurityMetadataSource中的请求标签 判断当前的角色是否有操作resourceIds的权限
      public void decide(Authentication auth, Object o, Collection<ConfigAttribute> resourceIds)
      
      //开启自定义资源认证
      //@EnableWebSecurity
      //public class WebSecurityConfig extends WebSecurityConfigurerAdapter
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //customMetadataSourceService每次请求根据数据库配置读取资源元信息及所需权限 并通过urlAccessDecisionManager与当前登录人所包含的权限进行比对
            http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                    o.setSecurityMetadataSource(customMetadataSourceService);
                    o.setAccessDecisionManager(urlAccessDecisionManager);
                    return o;
                }
            }).anyRequest().permitAll()
      
      • AuthenticationSuccessHandler 请求成功后处理 这个就不详细介绍了
    • 认证机制


      authenticationMechanisms.jpg
      • 因为我们主要说JWT所以简单说一下 Pre-Authentication Scenarios 当已经做了外部鉴权,到spring-security直接可用,即预验证场景
        • 首先需要实现AbstractPreAuthenticatedProcessingFilter,这里主要是实现方法getPreAuthenticatedPrincipal,从request中获取预授权信息
        • setCheckForPrincipalChanges(true),用来保证security上下文发生变更时候会走此预授权
        • AbstractPreAuthenticatedProcessingFilter内部会将principal封装成PreAuthenticatedAuthenticationToken(Authentication)并传递给下游AuthenticationManager
        • AuthenticationManager完成验证并返回实际Authentication将会存在SecurityContextHolder中便于在系统中获取当前人员
    • 上图 图1是普通登录生成token的过程 图2为使用token进行鉴权的过程


      common-login.jpg
      common-jwt.jpg
    • 对于JWT实现方式的一些探讨 能否借助redis做密钥生成 满足自动过期和仅允许一人登录 答案是可以的 下面就分几步简单介绍一下

      • header和payload不做探讨了 就是标准结构 两个JSON 且不包含敏感信息
      • 首先根据用户名+UUID(或任意比较复杂的随机方案) 生成一个当前用户的secret 并将secret保存在redis 如JWT_AAA_SEC=***
      • 然后将header和payload+secret通过hmacSha256Base64做一个签名为sign token为base64 header . base64 payload . sign
      • 当有请求时 首先根据username从redis中获取secret
      • 然后重复3中步骤生成sign并与当前token的sign做比较 如果不一致验签失败
      • 那重复登录踢出和自动过期的实现方式就很显然了 不详细说了

    相关文章

      网友评论

          本文标题:JWT Spring-security

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