常见的认证机制

1、HTTP Basic Auth

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

2、Cookie Auth

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

3、OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在
某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和
密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提
供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时
段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这
样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信
息,而非所有内容

4、Token Auth

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

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

优点:

支持跨域访问,无状态(不存储登录用户信息),更适用CDN ,去耦(可在任意地方生成token),更适用于移动应用 ,CSRF 不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)防范 ,性能 更高

JWT (JSON WEB TOKEN)

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

JWT组成

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

头部(Header

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

  1. {"typ":"JWT","alg":"HS256"} //在头部指明了签名算法是HS256算法

使用base64加密上述json字符串

  1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包
含三个部分

  1. 标准中注册的声明(建议但不强制使用)
    1. iss: jwt签发者
    2. sub: jwt所面向的用户
    3. aud: 接收jwt的一方
    4. exp: jwt的过期时间,这个过期时间必须要大于签发时间
    5. nbf: 定义在什么时间之前,该jwt都是不可用的.
    6. iat: jwt的签发时间
    7. jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
  1. 公共的声明
    公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.
    但不建议添加敏感信息,因为该部分在客户端可解密

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

定义一个payload:

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

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

  1. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)
payload (base64后的)
secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符
串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第
三部分

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
  2. kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg
  3. Q

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

JAVA的JJWT实现JWT

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

  1. /**
  2. * Author: Durian
  3. * Date: 2020/2/13 14:12
  4. * Description: 生成token
  5. */
  6. public class CreateJwtTest
  7. {
  8. public static void main(String[] args)
  9. {
  10. // 过期时间设置为1分钟
  11. long now = System.currentTimeMillis();
  12. long exp = now + 1000 * 60;
  13. JwtBuilder builder = Jwts.builder().setId("2020").
  14. setSubject("Durian").
  15. setIssuedAt(new Date()). // 设置签发时间
  16. signWith(SignatureAlgorithm.HS256, "ywlog"). // 设置签名秘钥
  17. setExpiration(new Date(exp)).
  18. claim("roles", "admin"). // 自定义信息
  19. claim("logo", "logo.png"); // 自定义信息
  20. System.out.println(builder.compact());
  21. }
  22. }
  1. /**
  2. * Author: Durian
  3. * Date: 2020/2/13 14:16
  4. * Description: 解析token
  5. */
  6. public class ParseJwtTest
  7. {
  8. public static void main(String[] args)
  9. {
  10. String token = "生成的token";
  11. Claims claims = Jwts.parser().setSigningKey("ywlog").parseClaimsJws(token).
  12. getBody();
  13. System.out.println("id:" + claims.getId());
  14. System.out.println("subject:" + claims.getSubject());
  15. System.out.println("IssuedAt: " + claims.getIssuedAt());
  16. SimpleDateFormat sdf = new SimpleDateFormat("yyyy‐MM‐dd hh:mm:ss");
  17. System.out.println("签发时间:" + sdf.format(claims.getIssuedAt()));
  18. System.out.println("过期时间:"+sdf.format(claims.getExpiration()));
  19. System.out.println("当前时间:" + sdf.format(new Date()));
  20. System.out.println("roles:"+claims.get("roles"));
  21. System.out.println("logo:"+claims.get("logo"));
  22. }
  23. }

测试运行,当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。

springboot项目中简单使用JWT

  1. 新建工具类

    1. /**
    2. * Author: Durian
    3. * Date: 2020/2/13 14:31
    4. * Description:
    5. */
    6. @Data
    7. @Component
    8. @ConfigurationProperties("jwt.config")
    9. public class JwtUtils
    10. {
    11. private String key;
    12. /** 过期时间 ms */
    13. private long ttl;
    14. /**
    15. * 生成jwt
    16. *
    17. * @param id id信息
    18. * @param subject 用户信息
    19. * @param roles 角色信息
    20. * @return jwt串
    21. */
    22. public String createJWT(String id, String subject, String roles)
    23. {
    24. long nowMillis = System.currentTimeMillis();
    25. Date now = new Date(nowMillis);
    26. JwtBuilder builder = Jwts.builder().setId(id)
    27. .setSubject(subject)
    28. .setIssuedAt(now)
    29. .signWith(SignatureAlgorithm.HS256, key).claim("roles", roles);
    30. if (ttl > 0)
    31. {
    32. builder.setExpiration(new Date(nowMillis + ttl));
    33. }
    34. return builder.compact();
    35. }
    36. /**
    37. * 解析jwt
    38. *
    39. * @param jwtStr jwt串
    40. * @return 解析内容
    41. */
    42. public Claims parseJWT(String jwtStr)
    43. {
    44. return Jwts.parser()
    45. .setSigningKey(key)
    46. .parseClaimsJws(jwtStr)
    47. .getBody();
    48. }
    49. }
  1. 将工具类在启动类中配置为bean

  2. 用户登录成功后生成token并返回给客户端

    1. String token = jwtUtils.createJWT(user.getId(), user.getNickname(), "user");
    2. Map<String, String> data = new HashMap<>(2);
    3. data.put("token", token);
    4. data.put("roles", user.getRoles());
    5. return new Result<>(StatusCode.OK, true, "登录成功", data);
  1. 新建拦截器

    1. @Component
    2. public class JwtInterceptor implements HandlerInterceptor
    3. {
    4. private final JwtUtils jwtUtils;
    5. @Autowired
    6. public JwtInterceptor(JwtUtils jwtUtils)
    7. {
    8. this.jwtUtils = jwtUtils;
    9. }
    10. @Override
    11. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    12. {
    13. /*
    14. * 只处理token信息,对具体业务逻辑不做判断
    15. */
    16. // 虽然所有路径都拦截,但是对于未登录进行查看操作时,token不存在,直接放行了
    17. // 登录状态token存在,检查合法性正确后放行
    18. final String header = request.getHeader("Authorization");
    19. // 获取头信息,用户登录成功后前端访问后台接口在http请求头中添加头信息
    20. if (!StrUtil.isBlank(header) && header.startsWith("Tensquare_"))
    21. {
    22. final String token = header.substring(10);
    23. try
    24. {
    25. Claims claims = jwtUtils.parseJWT(token);
    26. if (!StrUtil.isBlank((CharSequence) claims.get("roles")))
    27. {
    28. request.setAttribute("roles", claims.get("roles"));
    29. }
    30. } catch (Exception e)
    31. {
    32. throw new RuntimeException("身份验证失败!");
    33. }
    34. }
    35. return true;
    36. }
    37. }
  1. 新建一个拦截器配置类

    1. /**
    2. * Author: Durian
    3. * Date: 2020/2/13 15:36
    4. * Description:
    5. */
    6. @Configuration
    7. public class InterceptorConfig extends WebMvcConfigurationSupport
    8. {
    9. private final JwtInterceptor jwtInterceptor;
    10. @Autowired
    11. public InterceptorConfig(JwtInterceptor jwtInterceptor)
    12. {
    13. this.jwtInterceptor = jwtInterceptor;
    14. }
    15. @Override
    16. protected void addInterceptors(InterceptorRegistry registry)
    17. {
    18. // 拦截除了登录方法外的所有请求路径
    19. registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").
    20. excludePathPatterns("/**/login");
    21. }
    22. }
  1. 因为在工具类中配置了@ConfigurationProperties(“jwt.config”),所有需要在配置文件中配置ttl过期时间和key加密盐
    1. jwt:
    2. config:
    3. ttl: 1800000
    4. key: ${TENSQUARE_HOME}

此处的key没有明文配置,而是将值配置在了名为TENSQUARE_HOME的系统环境中