JWT Spring-security

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

  • JWT

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
     * null if the AuthenticationProvider is unable to support
     * authentication of the passed Authentication object. In such a case,
     * the next AuthenticationProvider that supports the presented
     * Authentication class will be tried.
     *
     * @throws AuthenticationException if authentication fails.
     */
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    
    /**
     * Returns true if this AuthenticationProvider supports the
     * indicated Authentication object.
     * 

    * Returning true does not guarantee an * AuthenticationProvider will be able to authenticate the presented * instance of the Authentication class. It simply indicates it can * support closer evaluation of it. An AuthenticationProvider can still * return null from the {@link #authenticate(Authentication)} method to * indicate another AuthenticationProvider should be tried. *

    *

    * Selection of an AuthenticationProvider capable of performing * authentication is conducted at runtime the ProviderManager. *

    * * @param authentication * * @return true if the implementation can more closely evaluate the * Authentication class presented */ boolean supports(Class authentication); }
     - 其次ProviderManager的authenticate会遍历所有Provider(getProviders),然后找到上面提到的支持当前Authentication类型的Provider做处理
    
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class 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 UserDetails
     * 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 null)
     *
     * @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 getAttributes(Object o) {  
          //FilterInvocation filterInvocation=Object o; 获取当前request
          //当前URL的特殊标签  
          //获取什么资源可以允许当前request然后将资源id封装后返回
      }
    
      @Override
      public Collection 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 resourceIds)
    
    //开启自定义资源认证
    //@EnableWebSecurity
    //public class WebSecurityConfig extends WebSecurityConfigurerAdapter
      @Override
      protected void configure(HttpSecurity http) throws Exception {
          //customMetadataSourceService每次请求根据数据库配置读取资源元信息及所需权限 并通过urlAccessDecisionManager与当前登录人所包含的权限进行比对
          http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor() {
              @Override
              public  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)