前言
目前在开发的小组结课项目中想试试JWT认证,简单分享一下,并看看与Session认证的异同。
登录认证(Authentication)的概念非常简单,就是通过一定手段对用户的身份进行确认。
我们都知道 HTTP 是无状态的,服务器接收的每一次请求,对它来说都是 “新来的”,并不知道客户端来过。
举个例子:
客户端A: 我是A, 给我一瓶水。
服务端B: 好,给你。
客户端A: 再给我来个面包。
服务端B: 啥,你是谁?
即每一个请求对服务器来说都是新的。
我们不可能每次操作都让用户输入用户名和密码,那么我们如何让服务器记住我们登录过了呢?
那就是凭证。即每次请求都给服务器一个凭证告诉服务器我是谁。
现在一般使用比较多的认证方式有四种:
- Session
- Token
- SSO单点登录
- OAtuth登录
下面就来说说比较常用的前两种。
Session
Cookie + Session
最常见的就是 Cookie + Session 认证。
Session,是一种有状态的会话管理机制,其目的就是为了解决HTTP无状态请求带来的问题。
当用户登录认证请求通过时,服务端会将用户的信息存储起来,并生成一个 SessionId
发送给前端,前端将这个 SessionId
保存起来。之后前端再发送请求时都携带 SessionId
,服务器端再根据这个 SessionId
来检查该用户有没有登录过。
这个 SessionId
, 一般是保存在Cookie中。
如果用户第一次访问某个服务器时,服务器响应数据时会在响应头的 Set-Cookie 标识里将Session Id 返回给浏览器,浏览器就将标识中的数据存在Cookie中。
下面我们来简单写个 demo 测试一下:
初始化一个spring boot 项目,并且代码如下:
demo
我们只需要在用户登录的时候将用户信息存在HttpSession中
@RestController
public class UserController {
@PostMapping("login")
public String login(@RequestBody User user, HttpSession session) {
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 登录成功 写入Session
session.setAttribute("sessionId", user);
return "login success";
}
return "username or password incorrect";
}
@GetMapping("/logout")
public String logout(HttpSession session) {
// 注销 删除Session
session.removeAttribute("sessionId");
return "logout success";
}
public String api(HttpSession session) {
// 用户操作 判断是否登录
User user = (User) session.getAttribute("sessionId");
if (user == null) {
return "please login";
}
return "return data";
}
}
下面我们向 login
地址 请求,并查看响应。 可以看到,用户登录时服务端会返回 Set-Cookie
字段。这些工作 Servlet帮我们做好了
下面我们向 api
地址请求。可以看到, 后续访问服务端自动就会携带Cookie:
Session + Header认证
当前开发的几个项目都是采用这种模式。
即 将 Session 会话放进 请求头中作为认证信息。
下面简单写个 demo 并用 postman 测试
demo
pom.xml:
org.springframework.session
spring-session
1.3.5.RELEASE
org.springframework.boot
spring-boot-starter-security
HeaderAndParamHttpSessionStrategy:
将cookie认证改为Header认证, 请求关键字为 x-auth-token
/**
* Header或是请求参数中的带有 token 的认证策略
* */
public class HeaderAndParamHttpSessionStrategy extends HeaderHttpSessionStrategy {
/**
* header认证关键字名称
*/
private String headerName = "x-auth-token";
@Override
public String getRequestedSessionId(HttpServletRequest request) {
String token = request.getHeader(this.headerName);
return (token != null && !token.isEmpty()) ? token : request.getParameter(this.headerName);
}
}
MvcSecurityConfig:
使用spring 提供的 MapSessionRepository 来帮助我们管理Session
@Configuration
@EnableWebSecurity
@EnableSpringHttpSession
public class MvcSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 开放端口
.antMatchers("**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and().cors()
.and().csrf().disable();
http.headers().frameOptions().disable();
return http.build();
}
/**
* 使用header认证来替换默认的cookie认证
*/
@Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderAndParamHttpSessionStrategy();
}
/**
* 由于我们启用了@EnableSpringHttpSession后,而非RedisHttpSession.
* 所以应该为SessionRepository提供一个实现。
* 而Spring中默认给了一个SessionRepository的实现MapSessionRepository.
*
* @return session策略
*/
@Bean
public SessionRepository sessionRepository() {
return new MapSessionRepository();
}
}
重新启动项目:
尝试1: 向 login
地址请求:
可以看到,服务端的响应头上带有了 x-auth-token, 这种将 session 放在请求头部的认证就是 header认证。
结果: 响应头返回 x-auth-token 字段
尝试2: 请求头不带 x-auth-token
, 向服务端请求 api
而这时候,假如客户端发送接下来的请求的时候,请求头不带上服务端返回的 x-auth-token
,那么是无法得到认证的。
结果: 认证失败
尝试3: 请求头带 x-auth-token
, 向服务端请求 api
将登录成功之后,服务端返回
结果: 认证成功
通过以上的方式: 我们成功将 Cookie + Session 认证 ,替换为了 Header + Session 认证。
那么换之后,有什么好处呢?
Header + Session 相较 Cookie + Session 有几点好处:
- 防止跨站脚本攻击(XSS):使用 Cookie 存储会话 ID 的话,Cookie 是通过浏览器自动管理的,容易受到 XSS 攻击的影响。而将会话 ID 存储在头部,可以避免这种攻击。
- 避免 CSRF 攻击:使用 Cookie 存储会话 ID 的话,攻击者可以利用 CSRF 攻击来获取 Cookie 中的会话 ID,从而伪造用户请求。将会话 ID 存储在头部的话,可以避免这种攻击。
- 不受第三方 Cookie 支持的限制:如果用户的浏览器禁用了第三方 Cookie,那么使用 Cookie + Session 的方式就无法使用。而将会话 ID 存储在头部,不需要使用 Cookie,不受这个限制。
缺点:
- 会话 ID 存储在头部,可能被重放攻击利用
- 执行性能代价较高:由于 HTTP 头比 Cookie 更大,因此将会话 ID 存储在头部通常会占用更多的网络资源,增加传输延迟。
因此,应该根据具体的应用场景、协议、需求和安全要求来选择合适的身份认证方式。
Token 认证
除了Session之外,目前比较流行的做法就是使用JWT(JSON Web Token)。
JWT具有以下俩种特性:
- 可以将一段数据加密成一段字符串,也可以从这字符串解密回数据
- 可以对这个字符串进行校验,比如有没有过期,有没有被篡改
看到这,这不和 Session + Header 认证一样嘛!就是把 SessionId 换成了JWT字符串而已,有必要么?
Session 和 JWT有一个重要的区别,就是 Session 是有状态的,JWT是无状态的。
即,Session 在服务端保存了用户信息,而JWT在服务端没有保存任何信息。
当前端携带Session Id到服务端时,服务端要检查其对应的 HttpSession 中有没有保存用户信息,保存了就代表登录了。
当使用JWT时,服务端只需要对这个字符串进行校验,校验通过就代表登录了。
下面继续从一个Demo体验:
demo
pom.xml:
io.jsonwebtoken
jjwt
0.9.1
写一个工具类
public interface CommonService {
/**
* 签名秘钥
*/
String SECRET = "shareMusic";
// 根据用户id生成token
static String createJwtToken(Long id) {
long ttlMillis = -1; // 表示不添加过期时间
return createJwtToken(id.toString(), ttlMillis);
}
/**
* 生成Token
*
* @param id 编号
* @param ttlMillis 签发时间 (有效时间,过期会报错)
* @return token String
*/
static String createJwtToken(String id, long ttlMillis) {
// 签名算法 ,将对token进行签名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成签发时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 通过秘钥签名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.signWith(signatureAlgorithm, signingKey);
// 如果指定了过期时间,则添加
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
// 验证并解析JWT
static Claims parseJWT(String jwt) { // 如果是空字符串直接返回null
if (jwt == null ||jwt.isEmpty()) {
return null;
}
// 这个Claims对象包含了许多属性,比如签发时间、过期时间以及存放的数据等
Claims claims = null;
// 解析失败了会抛出异常,所以我们要捕捉一下。token过期、token非法都会导致解析失败
try {
claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
.parseClaimsJws(jwt).getBody();
} catch (JwtException e) {
System.err.println("解析失败!");
}
return claims;
}
}
同时改写一下我们的 Controller:
登录成功返回
@PostMapping("login")
public String login(@RequestBody User user, HttpServletResponse response) {
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 判断成功 返回头加入token
String token = CommonService.createJwtToken(user.getUsername());
response.setHeader("Authorization", token);
return "login success";
}
return "username or password incorrect";
}
@GetMapping("/api")
public String api(HttpServletRequest request) {
String jwt = request.getHeader("Authorization");
// 解析失败就提示用户登录
if (CommonService.parseJWT(jwt) == null) {
return "please login";
}
return "return data";
}
重启服务开始测试。
尝试1: 向 login 地址请求.
可以看到,我们成功地在服务端响应头中返回了 JWT,它是以 Authorization
作为名字的字段。
尝试2: 不带JWT, 向 api 地址请求.
认证失败。
尝试3: 带JWT, 向 api 地址请求.
结果: 认证成功, 返回数据。
上面我们成功使用JWT完成了登录和请求api。下面简单看一下它的原理:
JWT原理:
JWT 通常由三部分组成:Header、Payload 和 Signature。
- Header:包含 Token 类型(即 JWT)和所使用的签名算法信息(如 HS256)。
- Payload:存储了一些描述信息,如 Token 的颁发者、过期时间、访问权限等,也可以包含一些用户的自定义数据。
- Signature:由服务器端生成,用于验证 Token 的正确性和完整性。
我们将刚才获取到的JWT去在线网站解密一下:
可以看到,获取到了我们传输的信息: 用户名
在线网站将 Header 和 Payload 中的 Base64 编码信息通过简单的算法将其还原成原始的明文数据
可以看到签名是无法解密的,这是因为 JWT 的签名主要是用于保证 JWT 的完整性和防止 JWT 被篡改 或伪造。
signature 可以选择对称加密算法或者非对称加密算法,常用的就是 HS256、RS256。
具体JWt解析过程可以看这篇文章: https://www.freecodecamp.org/chinese/news/how-to-sign-and-val...
JWT 注销
你可能会留意到, 上面的JWT方法没有注销的功能。那么如何注销?
事实上,JWT 是无状态的认证方式,因此它本身并不提供注销的机制。让我们从后台注销token,这对于jwt来说并不是那么简单,并不能像删除 session 那样来删除token。
JWT的目的与session不同,不可能强制删除或失效已经生成的token。
我们可以采用下面两种方式:
Token 过期⏰。
通过过期时间机制。可以在生成 JWT Token 时设置一个过期时间,一旦 Token 过期后则视为无效。通过这种方式,可以保证 Token 在一定时间内有效,同时也避免了 Token 滥用和被盗用的风险。
我还是想注销
假如我有一个严格的注销功能,无法等待Token自动过期怎么办?️
那么存储一个所谓的“名单”,判断Token是有效的。一般可以采用 Redis,
校验时,检查提供的 token 在 redis 中是否有效,如何无效的话就让用户去登录。
从这个方面也体现出了 JWt 更适合分布式结构。
Session 和 JWT
两者的不同:
存储位置:Session 信息是存储在服务端的,而 JWT 将认证信息存储在客户端的 Token 中。
是否需要状态:Session 基于状态来维护会话,如果会话状态丢失或者被篡改,服务器将会重新初始化会话。而 JWT 身份认证机制是无状态的,每个请求均包含足够的信息,服务器不需要维持任何状态。这一点使得 JWT 身份认证机制特别适合于分布式系统。
安全性:Session 是基于某种算法生成的 Session ID 来维护用户状态的,如果 Session ID 被窃取或者伪造,会话会受到攻击,凭证会失效。而 JWT 通过签名来防止伪造和篡改,只有在经过验证后才能使用。
扩展性:Session 方案一般适用于单一的服务或者单个应用,而 JWT 身份认证机制适用于跨域、分布式服务调用等多场景。
其实Session认证更适合我们平时的场景,可以看这篇文章,讲得很好https://www.796t.com/content/1546004284.html
JWT更适合一次性操作的认证:,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,因此也没必要储存JWT,真正实现无状态。
例如: 服务B你好, 服务A告诉我,我可以操作, 这是我的凭证(即JWT)
参考文章:
https://www.cnblogs.com/RudeCrab/p/14251154.html#%E6%94%B6%E5...
https://segmentfault.com/a/1190000041216780
https://cloud.tencent.com/developer/news/837117