JWT Spring-security

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做比较 如果不一致验签失败
    • 那重复登录踢出和自动过期的实现方式就很显然了 不详细说了
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容