首页 > 技术文章 > 登录认证实现——实现原理笔记

yangguanglei 2019-11-09 20:48 原文

登录认证实现(springboot+vue)

1. 登录认证相关介绍

登录认证的整个过程有两个部分组成,分别是用户认证和权限认证。用户认证是对用户的相关登录认证,权限认证是对已经登录的用户的操作权限识别与限制。

用户认证:核对数据库用户名和密码的相关信息是否正确。

权限认证:查看用户是否具有相应的操作权限,例如:收到用户修改信息的请求,会首先判断用户是会否具有ROLE_DELETE权限,如果没有则告诉客户端没有访问权限。

2. 准备工作

2.1 服务端

spring boot,spring security,JWT,token

2.2 客户端

vuex,token,router

3. 服务端

3.1 spring security

spring security是一个基于spring aop 和servlet过滤器的安全框架。

详细介绍链接: https://www.w3cschool.cn/springsecurity/

3.1.1 spring security的核心类

3.1.1.1 Authentication

用于用户信息认证,用户在进行登录认证之前会将用户的相关信息封装在一个Authentication的实现类中,在登录成功之后,又会将用户权限(ROLE_USER)封装到相关的Authentication实现类中,然后将其保存在SecurityContextHolder下的SecurityContext中,供后续的权限鉴定使用。

3.1.1.2 SecurityContextHolder

SecurityContextHolder是使用ThreadLocal来保存SecurityContext的,threadlocal在使用之后会自动清除(Security已经帮帮助我们做了)。当我们需要使用security中的用户名是可以使用 Authentication.getPrincipal() 获取当前用户的信息 ,这里的Authentication是Authentication的对象。

3.1.1.3 AuthenticationManager 和 AuthenticationProvider

AuthenticationManager 是一个用于处理认证请求的接口,默认实现是 ProviderManager 。AuthenticationManger不会自己实现认证请求,而是委托给其所配置的AuthenticationProvider列表,然后使用每一个AuthenticationProvider进行认证。 如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。

3.1.1.4 UserDetailsService(UserDetails很重要)

UserDetails是spring security中的一个核心接口,其中定义了一些可以获取用户名、密码、权限等于认证有关的信息。 Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类。在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的 UserDetails,比如在登录认证的时候。登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中。之后如果需要使用用户信息的时候就是通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。

3.1.1.5 GrantedAuthority

Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails 的。

GrantedAuthority 中只定义了一个 get Authority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null。

Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。

3.1.2 spring security认证的过程

  1. 用户使用用户名和密码进行登录。
  2. Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
  3. 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
  4. AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
  5. 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。

3.1.3 疑问

在request的时候,用户的相关的信息是放在SecurityContext中,SecurityContext又放在LocalThread中,而LocalThread在request结束之后会自动清除,那下一次在request的时候不是有需要在进行一次用户认证和鉴权吗?

原来的解决办法(session管理):使用session来存储SecurityContext中的内容,每次访问的时候再将session中的内容取出来放在SecurityContext中,结束之后再将SecurityContext中的内容再次放入session中。

上述办法的弊端:首先我需要存储session,其次,我需要解决服务器之间的session共享问题,还有CSRF安全问题等。

现在的解决办法(token管理):不用存储SecurityContext中的内容,自一次请求的时候生成token发送给客户端,后面每次请求都在请求头中带上token,服务器判断token之后,解析出用户相关信息只有交予Security进行后续的操作。

3.2 token(令牌)

3.2.1 token是干啥的

token是在客户端与服务端频繁交互,服务端又和数据库频繁交互查询用户名和密码确认身份,这样的背景下产生的。它的使用减轻了服务器的压力,较少了服务器与数据库的交互,增强服务器的健壮性。

token是服务端生成的一串字符串,在为客户端请求的令牌,在客户端每次请求中都会带上token,是服务端确认用户的唯一标志,而不需要携带用户名和密码,也保证了安全性。此外 token还可以是无状态的(什么是有状态和无状态?)

3.2.2 工作原理

登录

业务请求

token过期刷新

以上处理过程都是有状态的 ,需要在服务端做相关的token存储的,和session没啥区别,那怎么才能做到服务端不保存相关信息呢?——将所有的信息全部放到token里面。这样只需要服务端确保是自己签发的token就可以确认token有效,由于token的签发和验收都是服务端,所以可以使用对称加密算法加以解决,由于不需要还原加密内容,所以使用散列算法——HMAC

3.2.3 疑问

上面说的这些个什么对称加密算法,散列算法——HMAC什么的需要自己实现吗?需要的的话那也太复杂了吧,我还不如乖乖用有状态的呢?

答案是不用我们自己操作,因为有现成的开放标准供我们使用,是什么呢?

当然是JWT了,但有个问题使用JWT没办法处理过期刷新的问题,refreshToken需要使用redis来存储。

注:使用redis来管理toekn(有状态的,违背token的无状态,但使用的时候经常有人JWT和redis一起使用,方便实现token刷新延期)

3.3 JWT

3.3.1 什么是JWT

jwt是json web token的简称,看简称就知道是实现token的一种解决方案。

jwt是为了在网络应用环境之间传递声明而执行的一种基于json的开放标准(RFC 7519)。这里的token是紧凑和安全的,很适合分布式站点的单点登录场景,就是SSO

3.3.2 JWT组成

加密后的jwt是一个字符串,分别由Header,Payload,Signature三个部分组成,三个部分用“.”拼接在一起。

  • Header -由alg和type两个部分组成,alg是加密类型,type=jwt是不变的,用于表示token是JWT类型的。

    Header:
    {
     "alg": "HS256",
     "typ": "JWT"
    }
    
  • Payload(Claims),JWT的主体部分,包含你想要的信息(自定义字段)

    Payload:
    {
     "sub": "19969139664",
     "name": "ygl",
     "admin": true
    }
    

    除了自定义字段之外,JWT还提供了七个默认字段:

    iss:发行人

    exp:到期时间

    sub:主题

    aud:用户

    nbf:在此之前不可用

    iat:发布时间

    jti:JWT ID用于标识该JWT

    默认情况下JWT是未加密的,不要存放保密信息,防止信息泄露。

  • Signature,对Header,Payload的签名。

    Signature:
    base64UrlEncode(Header) + "." + base64UrlEncode(Payload)
    

    base64Url算法是JWT序列化使用的算法,和常见的base64算法差不多

    作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法,很简单把。
    

    签名部分通过指定的算法生成哈希,确保数据不会被篡改。

    具体做法:

    指定一个密码(secret),改密码只有服务器知道,然后使用标头中指定的的算法使用以下公式生成签名。

    HMACSHA256(signature,secret)//signature是上面定义的Signature
    
  • 在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

3.3.3 JWT用法

整个的JWT对象就是我们用于认证的用户信息的token,客户端在收到JWT之后,将收好,不要丢失,下次请求的时候就带上JWT对象,告诉服务端我是合法的,不能拒绝我。

4.客户端

4.1 vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

在这里主要用来管理token,删除,设置等操作都在这里面。

4.2 storage

主要用到了localstorage作为本地存储,存放token。

实例用法:

//访问了当前域名下的本地 Storage 对象,并通过 Storage.setItem() 增加了一个数据项目。
localStorage.setItem('myCat', 'Tom');
//读取 localStorage 项
let cat = localStorage.getItem('myCat');
//移除 localStorage 项
localStorage.removeItem('myCat');
//移除所有的 localStorage 项
localStorage.clear();

完整实例

4.3 路由导航守卫

这里主要用到的是全局前置守卫。

全局前置守卫: 当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。

在这里主要是用来拦截router,判断用户是否已经登录(localstorage里面有没有token),确定已经登录才能做下一步操作,执行目标路由跳转。(防止未登录使用)

实例代码:

router.beforeEach((to, from, next) => {
  ////////////////
  //添加自己的代码
  ////////////////
});

4.4 token

收到token之后将token放在localstorage中存放,每次做路由跳转的时候都会判断token的状态。放服务端发送请求的时候回在请求头里添加token的信息,以便服务端能识别用户。

token过期怎么和服务端同步?在使用中会有这样一种情况,明显token已经过期了,但是还是能做正常的路由跳转,为啥呢?

只要客户端不向服务端发送请求,客户端就不会知道token是不是过期,它只要一看有token就会做出跳转。

解决办法:在全局路由中添加默认请求,来判断token是否过期。如果不想每次router都做请求的话,可以做判断,没几次路由做一次token核对。

4.5 请求拦截器($ajaxSetup())

作用?拦截客户端发出的每一个请求。

这里用来干啥的?用来拦截请求向请求头中添加token,注意再添加token的时候需要将原始token的字首去掉,字首具体是啥需要看服务端设置的是啥(我设置的是Bearer)

实例代码:

$.ajaxSetup({
  dataType: "json",
  cache: false,
  headers: {
    "token": token
  },
  xhrFields: {
    withCredentials: true
  },
  complete: function (xhr) {
    if (xhr.code === 403) {//token过期,则跳转到登录页面
      that.$router.push({path: '/login'});
    }
  }
});

注:第一次接触做登录认证,可能记录的不一定完全正确。

推荐阅读