Cookie
HTTP
是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过
cookie
或者
session
去实现。
cookie
存储在客户端
:
cookie
是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
cookie
是不可跨域的: 每个
cookie
都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是
domain
)
Cookie机制
cookie
的设置以及发送会经历以下4个步骤:
- 客户端发送一个请求到服务器
-
服务器发送一个
HttpResponse
响应到客户端,其中包含
Set-Cookie
的头部 -
客户端保存
cookie
,之后向服务器发送请求时,
HttpRequest
请求中会包含一个
Cookie
的头部 - 服务器返回响应数据
Cookie 重要属性
属性 | 说明 |
---|---|
name=value | 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型 |
domain | 指定 cookie 所属域名,默认是当前域名 |
path | 指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’ |
maxAge | cookie 失效的时间,单位秒 如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1 |
expires | 过期时间,在设置的某个时间点后该 cookie 就会失效。 |
secure | 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。 |
httpOnly | 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全 |
Cookie 的Java操作
public void writeCookie(HttpServletRequest req, HttpServletResponse resp) {
int time = 10 * 24 * 60 * 60;
Cookie cookie = new Cookie("DAIHAO", System.currentTimeMillis() + "");
cookie.setPath("/");
cookie.setDomain(req.getHeader("Origin"));
cookie.setMaxAge(time);
cookie.setHttpOnly(true);
// 设置生命周期为MAX_VALUE,永久有效
// cookie.setMaxAge(Integer.MAX_VALUE);
// MaxAge为负数,是一个临时Cookie,不会持久化
// cookie.setMaxAge(-1);
resp.setCharacterEncoding("UTF-8");
// 修改Cookie只能使用一个同名的Cookie来覆盖原先的Cookie。如果要删除某个Cookie,则只需要新建一个同名的Cookie,并将maxAge设置为0,并覆盖原来的Cookie即可。
resp.addCookie(cookie);
}
Session
session
是另一种记录服务器和客户端会话状态的机制
session
是基于
cookie
实现的,
session
存储在服务器端,
sessionId
会被存储到客户端的
cookie
中
Session机制
-
用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的
Session
-
请求返回时将此
Session
的唯一标识信息
SessionID
返回给浏览器 -
浏览器接收到服务器返回的
SessionID
信息后,会将此信息存入到
Cookie
中,同时
Cookie
记录此
SessionID
属于哪个域名 -
当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在
Cookie
信息,如果存在自动将
Cookie
信息也发送给服务端,服务端会从
Cookie
中获取
SessionID
,再根据
SessionID
查找对应的
Session
信息,如果没有找到说明用户没有登录或者登录失效,如果找到
Session
证明用户已经登录可执行后面操作。
Session的Java操作
@WebServlet(name = "SessionServlet", urlPatterns = "/session")
public class SessionServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 Session 对象
HttpSession session = req.getSession();
// 将用户名存储到 Session 中
session.setAttribute("username", req.getParameter("username"));
// 重定向到另一个页面
resp.sendRedirect("anotherPage.jsp");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
Cookie 和 Session 的区别
-
安全性:
Session
比
Cookie
安全,
Session
是存储在服务器端的,
Cookie
是存储在客户端的 -
存取值的类型不同:
Cookie
只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,
Session
可以存任意数据类型 -
有效期不同:
Cookie
可设置为长时间保持,比如我们经常使用的默认登录功能,
Session
一般失效时间较短,客户端关闭(默认情况下)或者
Session
超时都会失效。 -
存储大小不同: 单个
Cookie
保存的数据不能超过
4K
,
Session
可存储数据远高于
Cookie
,但是当访问量过多,会占用过多的服务器资源。
Token
Token
,访问资源接口(
API
)时所需要的资源凭证 ,简单来说就是类似
cookie
的一种验证信息,客户端通过登录验证后,服务器会返回给客户端一个加密的
token
,然后当客户端再次向服务器发起连接时,带上
token
,服务器直接对
token
进行校验即可完成权限校验。
Token机制
1、客户端使用用户名跟密码请求登录
2、服务端收到请求,验证用户名与密码,验证成功后,服务端签发
token
发送给客户端
3、客户端收到
token
以后,会把它存储起来,比如放在
cookie
里或者
localStorage
里
4、客户端每次向服务端请求资源的时候需要带着服务端签发的
token
5、服务端收到请求,然后去验证客户端请求里面带着的
token
,如果验证成功,就向客户端返回请求的数据
Token 和 Session 的区别
-
Session
是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而
Token
是令牌,访问资源接口(
API
)时所需要的资源凭证。
Token
使服务端无状态化,不会存储会话信息。 -
Session
和
Token
并不矛盾,作为身份认证
Token
安全性比
Session
好,因为每一个请求都有签名还能防止监听以及重放攻击,而
Session
就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加
Session
来在服务器端保存一些状态。
JWT
JSON Web Token (JWT)
是一个开放标准
(RFC 7519)
,它定义了一种紧凑的、自包含的方式,用于作为
JSON
对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT机制
1、用户使用账号、密码登录应用,登录的请求发送到服务端。
2、服务端进行用户验证,然后创建
JWT
字符串返回给客户端。
3、客户端请求接口时,在请求头带上
JWT
。
4、服务端验证
JWT
合法性,如果合法则继续调用应用接口返回结果。
JWT数据结构
JWT
一般是这样一个字符串,分为三个部分,以
“.”
隔开:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header
JWT
第一部分是头部分,它是一个描述
JWT
元数据的
Json
对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
alg
属性表示签名使用的算法,默认为
HMAC SHA256(写为HS256)
,
typ
属性表示令牌的类型,
JWT
令牌统一写为
JWT
。
最后,使用
Base64 URL
算法将上述
JSON
对象转换为字符串保存。
Payload
JWT
第二部分是
Payload
,也是一个
Json
对象,除了包含需要传递的数据,还有七个默认的字段供选择。
-
iss (issuer):
签发人/发行人 -
sub (subject):
主题 -
aud (audience):
用户 -
exp (expiration time):
过期时间 -
nbf (Not Before):
生效时间,在此之前是无效的 -
iat (Issued At):
签发时间 -
jti (JWT ID):
用于标识该
JWT
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
默认情况下
JWT
是未加密的,任何人都可以解读其内容,因此一些敏感信息不要存放于此,以防信息泄露。
JSON
对象也使用
Base64 URL
算法转换为字符串后保存,是可以反向反编码回原样的,这也是为什么不要在
JWT
中放敏感数据的原因。
Signature
JWT
第三部分是签名。生成首先需要指定一个
secret
,该
secret
仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要
base64URL
加密后的
header
和
base64URL
加密后的
payload
使用 . 连接组成的字符串,然后通过
header
中声明的加密算法 进行加盐
secret
组合加密,然后就得出一个签名哈希,也就是
Signature
,且无法反向解密。
那么
Application Server
如何进行验证呢?可以利用
JWT
前两段,用同一套哈希算法和同一个
secret
计算一个签名值,然后把计算出来的签名值和收到的
JWT
第三段比较,如果相同则认证通过。
JWT的优点
-
json
格式的通用性,所以
JWT
可以跨语言支持,比如
Java
、
JavaScript
、
PHP
、
Node
等等。 -
可以利用
Payload
存储一些非敏感的信息。 -
便于传输,
JWT
结构简单,字节占用小。 -
不需要在服务端保存会话信息,易于应用的扩展。
JWT的Java操作
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
JwtUtils 工具类
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
//过期时间 15分钟
private static final long EXPIRE_TIME = 15 * 60 * 1000;
//私钥
private static final String TOKEN_SECRET = "DAIHAO";
//会话连接的有效时长,单位分钟
public static final int SCOKET_EFFECTIVE_TIME_MINUTE = 120;
/**
* 生成签名
*/
public static String sign(Map<String, Object> map) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("typ", "jwt");
// 返回token字符串
JWTCreator.Builder builder = JWT.create()
.withHeader(header)
.withIssuedAt(new Date()) //发证时间
.withExpiresAt(date); //过期时间
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Integer) {
builder.withClaim(entry.getKey(), (Integer) entry.getValue());
} else if (entry.getValue() instanceof Long) {
builder.withClaim(entry.getKey(), (Long) entry.getValue());
} else if (entry.getValue() instanceof Boolean) {
builder.withClaim(entry.getKey(), (Boolean) entry.getValue());
} else if (entry.getValue() instanceof String) {
builder.withClaim(entry.getKey(), String.valueOf(entry.getValue()));
} else if (entry.getValue() instanceof Double) {
builder.withClaim(entry.getKey(), (Double) entry.getValue());
} else if (entry.getValue() instanceof Date) {
builder.withClaim(entry.getKey(), (Date) entry.getValue());
}
}
return builder.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否正确
*
* @param **token**
* @return
*/
public static boolean verify(String token) {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
}
/**
* 获取用户自定义Claim集合
*
* @param token
* @return
*/
public static Map<String, Claim> getClaims(String token) {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
Map<String, Claim> jwt = verifier.verify(token).getClaims();
return jwt;
}
/**
* 获取过期时间
*
* @param token
* @return
*/
public static Date getExpiresAt(String token) {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
return JWT.require(algorithm).build().verify(token).getExpiresAt();
}
/**
* 获取jwt发布时间
*/
public static Date getIssuedAt(String token) {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
return JWT.require(algorithm).build().verify(token).getIssuedAt();
}
/**
* 验证token是否失效
*
* @param token
* @return true:过期 false:没过期
*/
public static boolean isExpired(String token) {
try {
final Date expiration = getExpiresAt(token);
return expiration.before(new Date());
} catch (TokenExpiredException e) {
// e.printStackTrace();
return true;
}
}
/**
* 直接Base64解密获取header内容
*
* @param token
* @return
*/
public static String getHeaderByBase64(String token) throws Exception {
if (StringUtils.isEmpty(token)) {
return null;
} else {
byte[] header_byte = decryBASE64(token);
return new String(header_byte);
}
}
/**
* 直接Base64解密获取payload内容
*
* @param token
* @return
*/
public static JSONObject getPayloadByBase64(String token) throws Exception {
if (StringUtils.isEmpty(token)) {
return null;
} else {
byte[] payload_byte = decryBASE64(token);
String decode = new String(payload_byte, "UTF-8");
int i = decode.lastIndexOf("{");
int j = decode.lastIndexOf("}");
decode = decode.substring(i, j + 1);
return JSONObject.parseObject(decode);
}
}
public static Boolean refreshToken(String token, HttpServletResponse response) {
try {
String[] tokenSplit = token.split("\\.");
JSONObject tokenPayloadJson = getPayloadByBase64(tokenSplit[1]);
Long expL = tokenPayloadJson.getLong("exp");
Date date = new Date();
Date expDate = new Date(expL);
double millisecond = date.getTime() - expDate.getTime() * 1000;
double time = millisecond / (60 * 1000);
if (time > SCOKET_EFFECTIVE_TIME_MINUTE) {
return false;
}
Map<String, Object> map = new HashMap<>();
for (Map.Entry entry : tokenPayloadJson.entrySet()) {
if (!"exp".equals(entry.getKey()))
map.put((String) entry.getKey(), entry.getValue());
}
String newToken = sign(map);
response.setHeader("DAIHAO_TOKEN", newToken);
response.setHeader("Access-Control-Expose-Headers", "DAIHAO_TOKEN");
return true;
} catch (Exception e) {
return false;
}
}
/***
* BASE64解密
* @param key
* @return
* @throws Exception
*/
public static byte[] decryBASE64(String key) throws Exception {
return (new Base64Decoder()).decode(key);
}
/***
* BASE64加密
* @param key
* @return
* @throws Exception
*/
public static String encryptBASE64(byte[] key) throws Exception {
return (new Base64Encoder()).encode(key);
}
public static void main(String[] args) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("userId", "1");
map.put("rose", "DAIHAO");
map.put("integer", 1111);
map.put("double", 112.222);
map.put("Long", 112L);
map.put("bool", true);
map.put("date", new Date());
String token = sign(map); //生成token
System.out.println("token:" + token);
System.out.println("校验token:" + verify(token));//验证token是否正确
String rose = getClaims(token).get("rose").asString(); //使用方法
System.out.println("rose:" + rose);
System.out.println("获取签发token时间:" + getIssuedAt(token));
System.out.println("获取过期时间:" + getExpiresAt(token));
//Thread.sleep(1000*40);
System.out.println("检查是否已过期:" + isExpired(token));
System.out.println("获取头:" + getHeaderByBase64(token));
System.out.println("获取负荷:" + getPayloadByBase64(token));
}
}
Token刷新
拦截器中判断失效,返回
token
public class TokenInteceptor extends HandlerInterceptorAdapter {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
super.afterCompletion(request, response, handler, ex);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
R<?> r = null;//自定义返回结果
String token = request.getHeader("DAIHAO_TOKEN");
try {
JwtUtils.verify(token);
} catch (SignatureVerificationException e) {
e.printStackTrace();
r = R.error(401, "invalid signature");
} catch (TokenExpiredException e) {
e.printStackTrace();
if (JwtUtils.refreshToken(token, response)) {
return super.preHandle(request, response, handler);
}
r = R.error(401, "token expired");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
r = R.error(401, "inconsistent token algorithm");
} catch (Exception e) {
e.printStackTrace();
r = R.error(401, "token invalidation");
}
if (r != null) {
String json = new ObjectMapper().writeValueAsString(r);
response.setContentType("application/json;charset=UTF-8");
Writer out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
out.write(json);
out.flush();
out.close();
return false;
}
return super.preHandle(request, response, handler);
}
}