前后端分离-JWT

参考:jwt.io
参考:https://blog.csdn.net/bntx2jsqfehy7/article/details/79224042
参考:https://blog.csdn.net/kevin_lcq/article/details/74846723
参考:https://www.cnblogs.com/eret9616/p/9661314.html

Cookie&Session

HTTP的无状态性

  HTTP 是无状态协议,它不对之前发送过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。假设要求登录认证的 Web 页面本身无法进行状态的管理(不记录已登录的状态),那么每次跳转新页面不是要再次登录,就是要在每次请求报文中附加参数来管理登录状态。

  不可否认,无状态协议当然也有它的优点。由于不必保存状态,自然可减少服务器的 CPU 及内存资源的消耗。从另一侧面来说,也正是因为 HTTP 协议本身是非常简单的,所以才会被应用在各种场景里。

Cookie技术的引入

  如果让服务器管理全部客户端状态则会成为负担,保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术。Cookie 技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。

  Cookie会根据从服务器端发送的响应报文内的一个叫做Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie 值后发送出去。

基于Session的登录

  目前用户的认证多半是基于表单的认证,基于表单的认证一般会使用 Cookie 来管理Session(Session会话,Session代表着服务器和客户端一次会话的过程,直到Session失效(服务端关闭)或者客户端关闭时结束)。基于表单认证本身是通过服务器端的 Web应用,将客户端发送过来的用户ID和密码与之前登录过的信息做匹配来进行认证的。
  但鉴于 HTTP 是无状态协议, 之前已认证成功的用户状态无法通过协议层面保存下来。 即无法实现状态管理, 因此即使当该用户下一次继续访问,也无法区分他与其他的用户。于是我们会使用Cookie 来管理 Session,以弥补 HTTP 协议中不存在的状态管理功能。

同源策略

  同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

  只有满足以下三种情况浏览器才会认定为同源:

1
2
3
•    协议相同
• 域名相同
• 端口相同

同源问题解决

  • JSOUP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $.ajax({
    dataType:'jsonp',
    data:'id=10',
    jsonp:'jsonp_callback',
    url:'http://www.yiwuku.com/getdata',
    success:function(){
    //dostuff
    },
    });
  • CORS

    CORS需要浏览器和服务器同时支持。(通过@CrossOrigin注解允许跨域,origins设置了允许跨域请求的域,allowCredentials允许设置和接受Cookie。

集群情况下的问题

  Session 是存储在Web服务器(例如:Tomcat)中的,并针对每个客户端(客户),通过SessionID来区别不同用户的。Session是以Cookie技术或URL重写实现,默认以Cookie技术实现,服务端会给这次会话创造一个JSESSIONID的Cookie值。
  但是一个显著的问题就是,在集群模式下如果通过Nginx负载均衡的时候,如果有一个用户登录的时候请求被分配到服务器A上,登录成功后设置的Session就会存放在服务器A上了,但是在服务器B上却没有该用户的Session数据,当用户再次发起一个请求的时候,此时请求如果被分配到服务器B上,则就不会查询到该用户的登录状态,就会出现登录失败的情况!
  一种可以想到的方式就是将多个Web服务器上存储的Session统一存储到某一存储介质中,保证进集群中的每一台机器都可以看到所有相同Session数据,这里的同步体现在所有的Session存储在同一的存储介质里边。

JWT(JSON Web Token)

  jwt是目前最流行的跨域认证解决方案;原理是json对象+签名Ebook

JWT数据结构

  JWT包含三个由点(.)分隔的部分,它们是:

  1. 头部(header)

  2. 有效负载(payload)

  3. 签名(signature)

    1
    2
    //Header.Payload.Signature
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

1
2
3
4
5
6
{
"alg": "HS256",//令牌的类型

"typ": "JWT"//散列算法

}

  最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

Payload

  Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

1
2
3
4
5
6
7
•    iss (issuer):签发人
• exp (expiration time):过期时间
• sub (subject):主题
• aud (audience):受众
• nbf (Not Before):生效时间
• iat (Issued At):签发时间
• jti (JWT ID):编号

  除了官方字段,还可以在这个部分定义私有字段,下面就是一个例子。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

  注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用Base64URL 算法转成字符串。

Signature

  Signature 部分是对前两部分的签名,防止数据篡改。

  首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

  算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

Base64URL

  前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64算法基本类似,但有一些小的不同。

  JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_。这就是 Base64URL 算法。

案例

这里依赖于开源库auth0

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>

用户认证请求

用户提交认证请求到服务器,服务器认证用户有效性并生成签名,签名方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static String sign(AuthToken authToken) {
try {
logger.debug("begin sign token");
//随机生成加密算法

Algorithm algorithm = generateAlgorithm(authToken);
Date issuedAt = new Date();
//根据配置文件的会话超时时间计算token失效时间

Date expiresAt = DateUtils.addMinutes(issuedAt, authToken.getMaxAge());
logger.debug("begin build jwt builder");
//构建jwt

JWTCreator.Builder builder = JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(issuedAt)
.withExpiresAt(expiresAt)
//添加主题--学号

.withSubject(authToken.getReaderBarCode())
//添加私有字段--角色

.withClaim(USER_ROLE, authToken.getUserRole());
String token = builder.sign(algorithm);
logger.debug("sign token success {0}", token);
logger.debug("begin authToken.setRecommendId (md5hex(token)) 32 ");
logger.debug("begin authToken.setBeginDate");
authToken.setBeginDate(issuedAt.getTime());
logger.debug("begin authToken.setExpiredDate");
authToken.setExpiredDate(expiresAt.getTime());
return token;
} catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
logger.error("sing error", e);
return null;
}
}

最终服务器将token保存在缓存中并返回给浏览器,浏览器将token保存在本地,每次请求要在header中携带token参数。

用户令牌验证

本例使用spring拦截器和自定义注解方式实现令牌校验。其中Auth为自定义注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//auth 注解判断
if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {
Auth authToken = ((HandlerMethod) handler).getMethodAnnotation(Auth.class);
if (authToken != null) {
//获取请求头中token字符串

String token = request.getHeader(WebAuthUtils.JWT_TOKEN);
if (StringUtils.isNotEmpty(token) && token.length() > WebAuthUtils.JWT_TOKEN_AUTH_PREFIX.length()) {
//去除前缀

token = token.substring(WebAuthUtils.JWT_TOKEN_AUTH_PREFIX.length());
AuthVerificationRequest authRequest = new AuthVerificationRequest(token, authToken.acceptRole());
//验证token和角色

CertifiedUser certifiedUser = authService.verifyToken(authRequest);
request.setAttribute(WebAuthUtils.WEB_READER, certifiedUser);
} else {
throw new ServiceException(ErrorCode.Common.NEED_AUTH, "请登录");
}
}
}
return super.preHandle(request, response, handler);
}

unsign方法

1
2
3
4
5
6
7
8
public static void unsign(String token, AuthToken authToken) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
Algorithm algorithm = getAlgorithm(authToken);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build(); //Reusable verifier instance

verifier.verify(token);
}