session登录、企业级Token登录实现流程、JWT登录
众所周知,http请求是无状态的,每次发送请求的时候,对方都不知道是谁发是请求。
实际情况,在登入系统后,需要系统记住是谁登入了,方便系统的权限管理,保证系统的安全性。
1、Session实现登入逻辑
Session是一块保存在服务器端的内存空间,一般用于保存用户的会话信息!
Session原理:
1、Session会为每一次会话分配一个Session对象
2、同一个浏览器发起的多次请求,同属于一次会话(Session)
3、首次使用到Session时,服务器会自动创建Session,并创建Cookie存储SessionId发送回客户端
S e s s i o n 登录实现流程: \color{red} {Session登录实现流程:} Session登录实现流程:
- 1、用户输入用户名密码点击登入按钮,服务端匹配用户名密码,然后执行request.getSession()会自动创建session对象(同时会创建一个cookie,cookie的name是JSESSIONID),然后把用户信息存入session中,后面根据session中是否有用户信息来判断该用户是否登录。
- 2、服务端自动返回给浏览器一个sessionID,存入浏览器的cookie中。
每次请求任意接口,浏览器都会带着这个cookie中的sessionID,服务器自动通过sessionID去寻找对应的session,如果session中已经有了这个用户的登陆信息,则说明用户已经登陆过了。 - 3、request.getSession()方法,如果不存在session,会自动创建session。如果存在session,会自动根据带来的sessionID,返回对应的session。
- 4、实际实现中,会写个拦截器,每次拦截session是否为空,判断是否登录过。
代码实现: \color{red} {代码实现:} 代码实现:
@RestController
@RequestMapping("session")
public class SessionController {
@RequestMapping("/login")
public String login(HttpServletRequest request, String userName, String passWord) {
if ("admin".equals(userName) && "123456".equals(passWord)) {
System.out.println("login success");
//如果不存在session,会自动创建session
HttpSession session = request.getSession();
//后面可以用session中是否存在username来判断用户是否登录。
session.setAttribute("username", userName);
if (session.isNew()) {
System.out.println("Session create success:" + session.getId());
} else {
System.out.println("Session is exit:" + session.getId());
}
return "success";
} else {
System.out.println("login fail");
return "fail";
}
}
}
S e s s i o n 作为登入的弊处: \color{red} {Session作为登入的弊处:} Session作为登入的弊处:
- 1、服务器压力大,通常Session是存储在内存中的,每个用户通过认证之后都会将session数据保存在服务器的内存中,而当用户量增大时,服务器的压力增大。
- 2、重启服务,session清空,需要重新登入。
- 3、Session共享:现在很多应用都是分布式集群,需要我们做额外的操作进行Session共享。
session基本已经不会应用到企业级登录了
2、Token实现登入逻辑
Token是现在最常用的方式。
T o k e n 登录实现流程: \color{red} {Token登录实现流程:} Token登录实现流程:
-
1、用户发出登录请求,带着用户名和密码到服务器进行验证,服务器验证成功就在后台生成一个token返回给客户端。token就是一个加密后的字符串(比如用JWT生成的token字符串)。
-
2、客户端将token存储到cookie中,服务端将token存储到redis中,可以设置存储token的有效期。
-
3、后续客户端的每次请求资源都必须携带token,服务端接收到请求首先校验是否携带token,以及token是否和redis中的匹配,若不存在或不匹配直接拦截返回错误信息(如未认证)。
-
4、其实我个人认为,用jwt生成的token和用MD5(或者其他加密方式) 生成的token,最近认证时的逻辑都是对比前端传过来的token是否一致。 要说JWT有啥优势,可能就是JWT中不包含用户密码吧,还有人说用JWT能减轻服务器压力,那可能就是减轻加密的压力,或者和数据库比对的压力。
而JWT就是上述流程当中token的一种具体实现方式,其全称是JSON Web Token。
3、JWT概念详解
JWT 是由三段信息构成的,将这三段信息文本用 . 连接一起就构成了 JWT 字符串。JWT 的三个部分依次为头部:Header,负载:Payload 和签名:Signature。
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。可以把用户信息,token过期时间存进去,但是不要写用户密码写进去。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
token的组成是这样的:header(头).payload(负载).signature(签名),header和payload都是base64编码,可以直接使用base64工具解析。JWT在线解析
JWT 的使用方式
客户端收到服务器返回的 JWT 之后需要在本地做保存。保存到浏览器的key叫Authorization,value就是token,记得前缀手动拼接字符串 Bearer 空格 。
Authorization: Bearer <token>
JWT 的特性
1、JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。项目中我们也很少对JWT进行二次加密
2、JWT 不加密的情况下,不能将秘密数据写入 JWT。
4、java工具类生成JWTToken
maven引入jwt包
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JwtUtil工具类
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author zhanggang
* @since 2023/11/21 10:29
*/
public class JwtUtil {
public static final String JWT_ID = UUID.randomUUID().toString();
/**
* jwt 加密解密密钥(可自行填写Base64加密)
* 必须使用最少88位的Base64对该令牌进行编码
*/
private static final String JWT_SECRET = "ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=";
/**
* 创建一个SimpleDateFormat对象,定义日期和时间的格式
*/
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 过期时间,单位为秒
**/
private static final long EXPIRATION = 30L;
private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
public static void main(String[] args) {
//jwt中的Payload部分,切记不要存放密码
HashMap<String, Object> claimsMap = new HashMap<>();
claimsMap.put("userId", "1");
claimsMap.put("userName", "zhangsan");
//生成jwtToken
String jwtToken = createJwt(claimsMap);
System.out.println(jwtToken);
//验证并解析token
Claims claims = verifyToken(jwtToken);
System.out.println("签发时间:"+ formatter.format(claims.getIssuedAt()));
System.out.println("到期时间:"+ formatter.format(claims.getExpiration()));
//延期token
String updateJwtTokenExpiration = updateJwtTokenExpiration(jwtToken, 120L);
Claims claimsYq = verifyToken(updateJwtTokenExpiration);
System.out.println("签发时间:"+ formatter.format(claims.getIssuedAt()));
System.out.println("到期时间:"+ formatter.format(claimsYq.getExpiration()));
System.out.println(updateJwtTokenExpiration);
}
/**
* @param claims
* @return
*/
public static String createJwt(Map<String, Object> claims) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
//采用什么算法是可以自己选择的,不一定非要采用HS256
Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
SecretKey secretKey = generalKey();
//jwt中的Header部分
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("alg", "HS256");
headerMap.put("typ", "JWT");
//下面就是在为payload添加各种标准声明和私有声明了,new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
.setHeader(headerMap)
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(JWT_ID)
//iat: jwt的签发时间
.setIssuedAt(new Date())
//设置过期时间
.setExpiration(expireDate)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey);
return builder.compact();
}
/**
* 验证并解析jwt
*
* @param token token
* @return 结果集
*/
public static Claims verifyToken(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
SecretKey key = generalKey();
Claims claims;
try {
//得到DefaultJwtParser
claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
.parseClaimsJws(token).getBody();
} catch (Exception e) {
System.out.println("解析token失败:"+e.getMessage());
e.printStackTrace();
claims = null;
}
return claims;
}
/**
* 刷新token并设置过期时间
*
* @param token 旧的token
* @param newExpirationInMillis 过期时间,单位秒
* @return 生成新的jwt token
*/
public static String updateJwtTokenExpiration(String token, Long newExpirationInMillis) {
newExpirationInMillis = 1000 * newExpirationInMillis;
SecretKey secretKey = generalKey();
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
claims.setExpiration(new Date(System.currentTimeMillis() + newExpirationInMillis));
return Jwts.builder()
.setClaims(claims)
.setId(JWT_ID)
.signWith(signatureAlgorithm, secretKey)
.compact();
}
/**
* 由字符串生成加密key
*
* @return 结果
*/
public static SecretKey generalKey() {
byte[] keyBytes = Base64.decodeBase64(JWT_SECRET);
return new SecretKeySpec(keyBytes, signatureAlgorithm.toString());
}
}