4.2.2.3 SpringSecurity工作流程分析
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KjoRRost-1690534711077)(https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/16657/1643969014457696256/230246543a084d3ba7b4544744d675ba.png)]
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
- UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor:负责权限校验的过滤器。
Debug分析
- 添加断点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLJAGv3t-1690534711078)(https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/16657/1643969014457696256/048968b78791495e8f0c254b4c6fe239.png)]
- 获取容器中的Bean
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9ZsH3eS-1690534711078)(https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/16657/1643969014457696256/1fde6f48b31546258ed0c2be6732cc79.png)]
4.2.3 认证
4.2.3.1 什么是认证(Authentication)
- 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)
- 互联网中的认证
- 用户名密码登录
- 邮箱发送登录链接
- 手机号接收验证码
- 只要你能收到邮箱/验证码,就默认你是账号的主人
4.2.3.2 两种认证方式
1) 基于Session的认证方式
session 认证流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dx7Dhq4K-1690534711078)(https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/16657/1643969014457696256/21df9b83630744fda76955d1be535e6c.png)]
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动把此域名下的 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
session 认证存在的问题
- 在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、Session黏贴等方案。
2) 基于Token的认证方式
什么是Token? (令牌)
- 访问资源接口(API)时所需要的资源凭证
- 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
服务器对 Token 的存储方式:
- 存到数据库中,每次客户端请求的时候取出来验证(服务端有状态)
- 存到 redis 中,设置过期时间,每次客户端请求的时候取出来验证(服务端有状态)
- 不存,每次客户端请求的时候根据之前的生成方法再生成一次来验证(JWT,服务端无状态)
Token特点:
- 服务端无状态化、可扩展性好
- 支持移动端设备
- 安全
- 支持跨程序调用
token 的身份验证流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5eFgrYtn-1690534711079)(https://fynotefile.oss-cn-zhangjiakou.aliyuncs.com/fynote/fyfile/16657/1643969014457696256/0b87426c52674ce89d238c33fd81aaa6.png)]
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
注意:
登录时 token 不宜保存在 localStorage,被 XSS 攻击时容易泄露。所以比较好的方式是把 token 写在 cookie 里。为了保证 xss 攻击时 cookie 不被获取,还要设置 cookie 的 http-only。这样,我们就能确保 js 读取不到 cookie 的信息了。再加上 https,能让我们的请求更安全一些。
token认证方式的优缺点
- 优点: 基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。
- 缺点: token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
3) Token 和 Session 的区别
- Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
- Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重复攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
4.2.3.3 JWT (JSON Web Token)
(1) JWT简介
什么是JWT
- JWT是一种基于 Token 的****认证授权机制.
- JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT有什么用
- JWT最常见的场景就是授权认证,用户登录之后,后续的每个请求都将包含JWT, 系统在每次处理用户请求之前,都要先进行JWT的安全校验,通过校验之后才能进行接下来的操作.
JWT认证方式
- JWT通过数字签名的方式,以JSON对象为载体,在用户和服务器之间传递安全可靠的信息.
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token head.payload.singurater
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
(2) JWT的组成部分
头部(Header)
- 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
{"typ":"JWT","alg":"HS256"}
//typ(Type):令牌类型,也就是 JWT。
//alg(Algorithm) :签名算法,比如 HS256。
JWT签名算法中,一般有两个选择,一个采用HS256,另外一个就是采用RS256
进行BASE64编码https://base64.us/,编码后的字符串如下:eyJhbGciOiJIUzI1NiJ9
载荷(payload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
{"sub":"1234567890","name":"John Doe","admin":true}
将上面的JSON数据进行base64编码,得到Jwt第二部分: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
字段说明,下面的字段都是由 JWT的标准所定义的
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
签名(signature)
服务器通过 Payload、Header 和一个密钥(Secret) 使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
Signature 部分是对前两部分的签名,作用是防止 Token(主要是 payload) 被篡改。
这个签名的生成需要用到:
- Header + Payload。
- 存放在服务端的密钥(一定不要泄露出去)。
- 签名算法。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
String encodeString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
String secret = HMACSHA256(encodeString,secret);
签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
(3) 签名的目的
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。
一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用 alg
字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
(4) JWT与Token的区别
Token 和 JWT (JSON Web Token) 都是用来在客户端和服务器之间传递身份验证信息的一种方式。但是它们之间有一些区别。
- Token 是一个通用术词,可以指代任何用来表示身份的字符串。它可以是任何形式的字符串,并不一定是 JWT。
- JWT 是一种特殊的 Token,它是一个 JSON 对象,被编码成字符串并使用秘密密钥进行签名。JWT 可以用来在身份提供者和服务提供者之间安全地传递身份信息,因为它可以被加密,并且只有拥有秘密密钥的方能解密。
总的来说,JWT 是一种特殊的 Token,它具有更强的安全性和可靠性。
(5) JWT的优势
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
4.2.3.4 JJWT签发与验证token
使用jjwt实现jwt的签发和解析获取payload中的数据.
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0)。
官方文档:https://github.com/jwtk/jjwt
(1) 引入依赖
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
(2) 创建 Token
@SpringBootTest
class SpringsecurityExampleApplicationTests {
@Test
void contextLoads() {
}
@Test
public void testJJWT(){
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ.
* ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC8
*/
}
}
运行打印结果:
eyJhbGciOiJIUzI1NiJ9.
eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ.
ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC
(3) 解析Token
我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
解析JJWS的方法如下:
- 使用该
Jwts.parser()
方法创建JwtParserBuilder
实例。 setSigningKey()
与builder中签名方法signWith()对应,parser中的此方法拥有与signWith()方法相同的三种参数形式,用于设置JWT的签名key,用户后面对JWT进行解析。- 最后,
parseClaimsJws(String)
用您的jws调用该方法,生成原始的JWS。 - 如果解析或签名验证失败,则整个调用将包装在try / catch块中。
@Test
public void parserJWT(){
String JWS = "eyJhbGciOiJIUzI1NiJ9." +
"eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM1ODY2fQ." +
"ybkDJLVj1Fsi8m3agyxtyd0wxv7lHDqCWNOLN-eOxC8";
//claims = 载荷 (payload)
try {
Claims claims = Jwts.parser().setSigningKey("mashibing")
.parseClaimsJws(JWS)
.getBody();
System.out.println(claims);
} catch (Exception e) {
System.out.println("Token验证失败! !");
e.printStackTrace();
}
}
运行打印结果:
{jti=9527, sub=hejiayun_community, iat=1681135866}
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token。
sub: jwt所面向的用户
(4) 设置过期时间
有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。
- 创建token 并设置过期时间
@Test
public void testJJWT2(){
long currentTimeMillis = System.currentTimeMillis();
Date expTime = new Date(currentTimeMillis);
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.setExpiration(expTime) //设置过期时间
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM3MjI0LCJleHAiOjE2ODExMzcyMjR9.
* evc01MRxLjpbksbMLdVPM9sJGYGhpC3UYOfm4-0sMGE
*/
}
- 解析TOKEN
打印效果: 异常信息: JWT签名与本地计算的签名不匹配。JWT有效性不能断言,也不应该被信任
Token验证失败! !
io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found:
(5) 自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
@Test
public void testJJWT3(){
long currentTimeMillis = System.currentTimeMillis()+100000000L;
Date expTime = new Date(currentTimeMillis);
JwtBuilder builder = Jwts.builder()
.setId("9527") //设置唯一ID
.setSubject("hejiayun_community") //设置主体
.setIssuedAt(new Date()) //设置签约时间
.setExpiration(expTime) //设置过期时间
.claim("roles","admin") //设置角色
.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey
//压缩成String形式,签名的JWT称为JWS
String jws = builder.compact();
System.out.println(jws);
/**
* eyJhbGciOiJIUzI1NiJ9.
* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM3MjI0LCJleHAiOjE2ODExMzcyMjR9.
* evc01MRxLjpbksbMLdVPM9sJGYGhpC3UYOfm4-0sMGE
*/
}
解析TOKEN,打印结果
{jti=9527, sub=hejiayun_community, iat=1681137464, exp=1681237464, roles=admin}