1 常见的认证机制

1.1 HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

1.2 Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效;
图片.png

1.3 OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容,下面是OAuth2.0的流程:
图片.png
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

1.4 Token Auth

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向
    客户端返回请求的数据

图片.png
Token Auth的优点
Token机制相对于Cookie机制又有什么好处呢?

  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
  • 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
  • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

    2 基于JWT的Token认证机制实现

    2.1 什么是JWT

    JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

    2.2 JWT组成

    一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

    头部(Header)

    头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

    1. {"typ":"JWT","alg":"HS256"}

    在头部指明了签名算法是HS256算法。 我们进行BASE64编
    http://base64.xpcha.com/,编码后的字符串如下:

    1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

    小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

    载荷(playload)

    载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
    (1)标准中注册的声明(建议但不强制使用)

    1. iss: jwt签发者
    2. sub: jwt所面向的用户
    3. aud: 接收jwt的一方
    4. exp: jwt的过期时间,这个过期时间必须要大于签发时间
    5. nbf: 定义在什么时间之前,该jwt都是不可用的.
    6. iat: jwt的签发时间
    7. jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    (2)公共的声明
    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
    (3)私有的声明
    私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
    定义一个payload:

    {"sub":"1234567890","name":"John Doe","admin":true}
    

    然后将其进行base64编码,得到Jwt的第二部分。

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
    

    签证(signature)

    wt的第三部分是一个签证信息,这个签证信息由三部分组成:
    header (base64后的)
    payload (base64后的)
    secret
    这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

    TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    

    将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
    kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg
    Q
    

    注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

    3 Java的JJWT实现JWT

    3.1 什么是JJWT

    JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

    3.2 JJWT快速入门

    3.2.1 token的创建

    (1)创建maven工程,引入依赖

                  <dependency>
              <groupId>io.jsonwebtoken</groupId>
              <artifactId>jjwt</artifactId>
              <version>0.6.0</version>
          </dependency>
    

    (2)创建类CreateJwtTest,用于生成token

    /**
    * @author: Luck-zb
    * description:创建token -- 测试
    * Date:2021/1/31 - 10:23
    */
    public class CreateJwtTest {
    
      public static void main(String[] args) {
    
          JwtBuilder jwtBuilder = Jwts.builder()
                  .setId("12345")
                  .setSubject("创建token测试")
                  .setIssuedAt(new Date())   // 以上三个属性为负载属性
                  .signWith(SignatureAlgorithm.HS256, "fantastic"); // 这两个属性为头信息,签名根据负载、头、盐生成,【注意】:这盐写中文会报错
          // token由头、负载、签名三部分生成
    
          // 生成token字符串
          String token = jwtBuilder.compact();
          System.out.println(token);
      }
    }
    

    setIssuedAt用于设置签发时间
    signWith用于设置签名秘钥
    (3)测试运行,输出如下:

    eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1MjM0M
    TM0NTh9.gq0J‐cOM_qCNqU_s‐d_IrRytaNenesPmqAIhQpYXHZk
    

    再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

    3.2.2 token的解析

    我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
    创建ParseJwtTest ```java import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts;

import java.text.SimpleDateFormat;

/**

  • @author: Luck-zb
  • description:解析token — 测试
  • Date:2021/1/31 - 10:35 */ public class ParseJwtTest {

    public static void main(String[] args) {

     String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NSIsInN1YiI6IuWIm-W7unRva2Vu5rWL6K-VIiwiaWF0IjoxNjEyMDYwNDg0fQ." +
             "zenzNy8MN9RdUaKNS6MT6hHad6nUzAkWhraPZ-dKDx4";
     Claims claims = Jwts.parser().setSigningKey("fantastic").parseClaimsJws(token).getBody();
     System.out.println("登录id: " + claims.getId());
     System.out.println("登录用户名: " + claims.getSubject());
     System.out.println("token签发时间: " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(claims.getIssuedAt()));
    

    }

}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token
<a name="Y2UQI"></a>
### 3.2.3 token过期校验
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。<br />创建CreateJwtTest2
```java
/**
 * @author: Luck-zb
 * description:创建带过期时间的token -- 测试
 * Date:2021/1/31 - 10:50
 */
public class CreateJwtTest2 {

    public static void main(String[] args) {

        // 当前时间
        long nowTime = System.currentTimeMillis();
        // 过期时间为1分钟
        long expTime = nowTime + 1000*60;

        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("12345")
                .setSubject("测试token过期时间")
                .setIssuedAt(new Date())
                .setExpiration(new Date(expTime))
                .signWith(SignatureAlgorithm.HS256, "fantastic");

        // 生成token字符串
        String token = jwtBuilder.compact();
        System.out.println(token);
    }
}

setExpiration 方法用于设置过期时间
创建ParseJwtTest2

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.cache.annotation.CacheEvict;

import java.text.SimpleDateFormat;

/**
 * @author: Luck-zb
 * description:带超时时间的token解析 -- 测试
 * Date:2021/1/31 - 10:56
 */
public class ParseJwtTest2 {

    public static void main(String[] args) {

        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NSIsInN1YiI6IuS_j-S_j-eahOWxgeiCoeWlvee_mCIsImlhdCI6MTYxMjA2MjIyMCwiZXhwIjoxNjEyMDYyMjgwfQ.6c3sn4_lifgFcIpQaR7_vHEz2zaynTs_rkMuIjTh78Y";
        Claims claims = Jwts.parser().setSigningKey("fantastic").parseClaimsJws(token).getBody();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM--dd hh:mm:ss");

        System.out.println("登录id: " + claims.getId());
        System.out.println("登录用户名: " + claims.getSubject());
        System.out.println("token签发时间: " + sdf.format(claims.getIssuedAt()));
        System.out.println("token过期时间: " + sdf.format(claims.getExpiration()));
    }
}

3.2.4 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims 创建CreateTokenWithCustomParamTest

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

/**
 * @author: Luck-zb
 * description:创建自定义参数的token -- 测试 -- 通过使用claim键值对的方式指定自定义参数
 * Date:2021/1/31 - 11:11
 */
public class CreateTokenWithCustomParamTest {

    public static void main(String[] args) {

        // 当前时间
        long nowTime = System.currentTimeMillis();
        // 过期时间
        long expTime = nowTime + 1000*60;

        JwtBuilder jwtBuilder = Jwts.builder()
                .setId("12345")
                .setSubject("测试自定义claims")
                .setIssuedAt(new Date())
                .setExpiration(new Date(expTime))
                .claim("role", "admin") // 通过使用claim键值对的方式指定自定义参数
                .signWith(SignatureAlgorithm.HS256, "qidfaoqiafdodepiguzhenxiang");

        // 生成token字符串
        String token = jwtBuilder.compact();
        System.out.println(token);
    }
}

创建ParesTokenWithCustomParamTest

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

import java.text.SimpleDateFormat;

/**
 * @author: Luck-zb
 * description:解析带自定义参数的token -- 测试
 * Date:2021/1/31 - 11:18
 */
public class ParesTokenWithCustomParamTest {

    public static void main(String[] args) {

        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NSIsInN1YiI6IuS_j-S_j-eahOWxgeiCoeecn-i9ryIsImlhdCI6MTYxMjA2MzM5MCwiZXhwIjoxNjEyMDYzNDUwLCJyb2xlIjoiYWRtaW4ifQ.l0JqB4eNKZW3oBnncLxVhWSiB7L4yTn45_mp33QUMfA";
        Claims claims = Jwts.parser().setSigningKey("qiaoqiaodepiguzhenxiang").parseClaimsJws(token).getBody();

        System.out.println("登录id: " + claims.getId());
        System.out.println("登录用户名: " + claims.getSubject());

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        System.out.println("token签发时间: " +  sdf.format(claims.getIssuedAt()));
        System.out.println("token过期时间: " + sdf.format(claims.getExpiration()) );

        System.out.println("自定义token参数role: " + claims.get("role"));
    }
}

4 微服务鉴权实战

思路:用户登录请求,根据请求的url都是要放行的,拦截器等不对登录请求做任何拦截,用户登录就是在后台跟数据库中的数据进行用户名和密码的校验,如果检验成功,则登录成功,并返回生成的token,后面的操作请求都要带着这个token,进行认证

(1)tensquare_common工程引入依赖(考虑到工具类的通用性)

                <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>

(2)修改tensquare_common工程,创建util.JwtUtil

package util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.Date;

/**
 * Created by Administrator on 2018/4/11.
 */
@ConfigurationProperties("jwt.config")
public class JwtUtil {

    private String key ;

    private long ttl ;//一个小时

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getTtl() {
        return ttl;
    }

    public void setTtl(long ttl) {
        this.ttl = ttl;
    }

    /**
     * 生成JWT
     *
     * @param id
     * @param subject
     * @return
     */
    public String createJWT(String id, String subject, String roles) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder().setId(id)
                .setSubject(subject)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
        if (ttl > 0) {
            builder.setExpiration( new Date( nowMillis + ttl));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr){
        return  Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();
    }

}

(3)修改tensquare_user工程的application.yml, 添加配置

jwt:
 config:
    key: itcast  # key这个具体问业务,或者经理
    ttl: 360000  # 过期时间

(4)管理员登陆后台签发token

(1)配置bean .修改tensquare_user工程Application类

@Bean    
public JwtUtil jwtUtil(){    
return new util.JwtUtil();        
}

(2)修改AdminController的login方法

 @Autowired
    private JwtUtil jwtUtil;

    /**
     * 登录 -- admin
     *
     * @param admin
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public Result login(@RequestBody Admin admin) {
        // 约定登录成功返回Admin对象,否则返回null
        admin = adminService.login(admin);
        if (admin == null) {
            return new Result(false, StatusCode.LOGINERROR, "登录失败!");
        }

        // 生成token并返回到客户端
        String token = jwtUtil.createJWT(admin.getId(), admin.getLoginname(), "admin");

        // 约定返回一个包含token和用户信息的map
        Map<String, String> map = new HashMap<>();
        map.put("token", token);
        map.put("role", "admin");
        return new Result(true, StatusCode.OK, "登录成功!", map);
    }

测试运行结果

{
  "flag": true,
  "code": 20000,
  "message": "登陆成功",
  "data": {
    "token":
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI5ODQzMjc1MDc4ODI5MzgzNjgiLCJzdWIiOiJ4aWF
vbWkiLCJpYXQiOjE1MjM1MjQxNTksInJvbGVzIjoiYWRtaW4iLCJleHAiOjE1MjM1MjQ1MTl9
._YF3oftRNTbq9WCD8Jg1tqcez3cSWoQiDIxMuPmp73o",
    "name":"admin"
  }
}

(5)删除用户功能鉴权

需求:删除用户,必须拥有管理员权限,否则不能删除。前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格+token
(1)修改UserController的delete方法 ,判断请求中的头信息,提取token并验证权限。

/**
     * 删除
     *
     * @param id
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public Result delete(@PathVariable String id) {

        /*String header = request.getHeader("Authorization");

        // 检验头信息
        if (StringUtils.isBlank(header)) {
            return new Result(false, StatusCode.ACCESSERROR, "权限不足");
        }

        if (!header.startsWith("Bearer ")) {
            return new Result(false, StatusCode.ACCESSERROR, "权限不足");
        }

        // 校验头信息中的token
        String token = header.substring(7);
        try {
            Claims claims = jwtUtil.parseJWT(token);
            String roles = (String) claims.get("roles");
            if (StringUtils.isBlank(roles) || !"admin".equals(roles)) {
                return new Result(false, StatusCode.ACCESSERROR, "权限不足");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }*/

        // 以上代码通过使用拦截器改进后如下
        // 因为删除用户,需要管理员权限,所以应先判断当前请求是否具有管理员权限
        Claims claims = (Claims) request.getAttribute("admin_claims");
        if (claims == null) {
            return new Result(false, StatusCode.ACCESSERROR, "权限不足!");
        }

        userService.deleteById(id);
        return new Result(true, StatusCode.OK, "删除成功");
    }