1. 前后端分离架构
1.1 简介
核心思想:前端HTML页面通过AJAX调用后端的 RESTful API 接口 并使用JSON数据进行交互。
参考博客:http://blog.720ui.com/2016/arch_web_server/
1.2 前后端分离的好处
分工明确,职责清晰,前端人员关注界面展现/交互逻辑,服务端人员关注业务/数据接口。
- 前端职责:页面UI,页面展示、交互、渲染,用户体验等。
- 后端职责:数据存储和业务逻辑,RESTful API 接口,系统性能、安全性等。
多端应用:
一套服务端RESTful API,可以为 WEB、APP、小程序、公共号等多客户端提供服务。
1.3 会话管理的三种方式
会话是浏览器和服务器之间的多次请求和响应,从浏览器访问服务器开始,到访问服务器结束,浏览器关闭为止的这段时间内容产生的多次请求和响应,合起来叫做浏览器和服务器之间的一次会话。
HTTP协议无状态,客户端一次请求结束,发起新的请求时,服务器不记录上一次的请求。如何知道两次请求是同一个用户发出的呢?需要用到会话管理。server-session
优点:经典传统的会话管理方式,安全性好、使用简单。
缺点:
- 会话信息存储在服务器,同时在线用户比较多时,会话信息会占据比较多的内存;
- 当应用采用集群部署的时,会遇到多台web服务器之间如何作session共享的问题。由于session是由单个服务器建立的,可是处理用户请求的服务器不一定是那个建立session的服务器,这样就拿不到以前已经放入到session中的登陆凭证之类的信息了;
当多个应用共享session时,会遇到跨域问题,由于不一样的应用可能部署的主机不同,须要在各个应用作好cookie跨域的处理。
cookie-based
把用户的登陆凭证存到客户端,用户登录成功后,把登录凭证写到cookie里,并给该cookie设置有效期,后续请求直接验证存有登陆凭证的cookie是否存在以及凭证是否有效,便可判断用户的登录状态。流程如下:
优点:实现了服务端无状态,会话不再占用服务器内存;
- 不存在Session共享问题。
缺点:
这种方式不使用Cookie进行token的传递,而是每次请求的时候,主动把token加到http header里面或请求参数里面。
优点:
- Cookie的缺点;
-
会话安全**
第一种方式的会话凭证仅仅是一个Sessionid,保证sessionid足够随机,其它人就不可能轻易地冒充别人的sessionid进行操做;
- 第二种方式的凭证ticket以及第三种方式的凭证token都是一个在服务端作了数字签名和加密处理的字符串,因此只要密钥不泄露,别人也没法轻易地拿到这个串中的有效信息并对它进行篡改。
会话劫持:
- 使用 HTTPS;
-
2. JWT
2.1 简介
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
应用场景: Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Token是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
2.2 组成结构
JSON Web Tokenv由三部分组成,它们之间用点
.
连接。这三部分分别是:Header
- Payload
- Signature
完整 JWT 结构如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTYxNDIzMDIsInVzZXJuYW1lIjoiamFjayJ9.l9kjzYelZJXrD7MxKhR2p3vjcEi6JBsmPVY9uxmG2zY
header
JWT 的头部包含两部分信息:
- typ:token 类型,这里是 JWT
alg:加密算法名称, 通常直接使用 HMAC SHA256
{
'typ': "JWT",
'alg': "HS256"
}
然后,用Base64 对这个JSON 编码就得到JWT的第一部分。
payload
声明有三种类型:
registered: 标准中注册的声明。
- public: 公共声明。
- private: 私有声明。
标准中注册的声明 (建议但不强制使用):
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp:jwt的过期时间,这个过期时间必须要大于签发时间
- nbf:定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明:
私有声明是提供者和消费者所共同定义的声明,但不建议添加敏感信息,因为该部分在客户端可解密。
定义一个payload:
{
"sub": "1234567890",
"name": "jack",
"admin": true
}
然后,用 Base64 对这个JSON 编码就得到JWT的第二部分。
signature
JWT 的第三部分是一个签证信息,这个签证信息由三部分组成:
- header
- payload
- secret
这个部分需要 Base64 编码后的 header 和 payload 使用 .
连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
伪代码:
String str = Base64(header) + "." + Base64(payload);
String secret = "39kjfeirjokjfffkYEE";
String signature = HMACSHA256(srt,secret);
将这三部分用.
连接成一个完整的字符串,构成了最终的 JWT。
注意:
- secret 保存在服务器端,jwt的签发和验证是在服务器端;
- secret 用来签发和验证 jwt 。
所以,secret 是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt了。
2.3 优点
- 因为json的通用性,所以JWT是可以进行跨语言支持的;
- 因为有了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息;
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的;
-
2.4 安全相关
不应该在 jwt 的 payload 部分存放敏感信息,因为该部分是客户端可解码的部分;
- 保护好 secret 私钥,该私钥非常重要;
-
2.5 java-jwt
java-jwt 是 JWT 开放标准 RFC 7519 的 Java 语言实现类库。
github地址:https://github.com/auth0/java-jwt引入类库
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.14.0</version>
</dependency>
创建 token
try { String secret = "iuq9339349589589dkjf"; Algorithm algorithm = Algorithm.HMAC256(secret); String token = JWT.create() .withIssuer("auth0") .sign(algorithm); } catch (JWTCreationException exception){ //Invalid Signing configuration Couldn't convert Claims. }
校验 token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; try { String secret = "iuq9339349589589dkjf"; Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withIssuer("auth0") .build(); //Reusable verifier instance DecodedJWT jwt = verifier.verify(token); } catch (JWTVerificationException exception){ //JWTVerificationException //Invalid signature/claims }
解析 token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; try { DecodedJWT jwt = JWT.decode(token); } catch (JWTDecodeException exception){ //Invalid token }
JWTUtil 工具类
import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Date; /** * JWT 工具类 */ public class JWTUtil { /** * 生成token * @param username 用户名 * @param secret 私钥 * @param expires 有效期 * @param issued 签发时间 * @return */ public static String generatorToken(String username, String secret, Long expires, Date issued) { Date date = new Date(System.currentTimeMillis() + expireTime * 1000); Algorithm algorithm = Algorithm.HMAC256(username + secret); return JWT.create() .withClaim("username", username) .withExpiresAt(date) .withIssuedAt(issued) .sign(algorithm); } /** * 校验 token * @param token * @param secret * @return */ public static boolean verifyToken(String token, String secret) { Algorithm algorithm = Algorithm.HMAC256(getUsername(token) + secret); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); return true; } /** * 从token中获取用户名 * @param token * @return */ public static String getUsername(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } /** * 从token中获取签发时间 * @param token * @return */ public static Date getIssuedTime(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getIssuedAt(); } }