4.2.3 认证
4.2.3.1 什么是认证(Authentication)
- 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)
- 互联网中的认证
- 用户名密码登录
- 邮箱发送登录链接
- 手机号接收验证码
- 只要你能收到邮箱/验证码,就默认你是账号的主人
4.2.3.2 两种认证方式
1) 基于Session的认证方式
session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 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 的身份验证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 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
@SpringBootTestclass SpringsecurityExampleApplicationTests {@Testvoid contextLoads() {}@Testpublic void testJJWT(){JwtBuilder builder = Jwts.builder().setId("9527") //设置唯一ID.setSubject("hejiayun_community") //设置主体.setIssuedAt(new Date()) //设置签约时间.signWith(SignatureAlgorithm.HS256, "mashibing");//设置签名 使用HS256算法,并设置SecretKey//压缩成String形式,签名的JWT称为JWSString 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块中。
@Testpublic 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 并设置过期时间
@Testpublic 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称为JWSString 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。
创建测试类,并设置测试方法:
@Testpublic 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称为JWSString jws = builder.compact();System.out.println(jws);/*** eyJhbGciOiJIUzI1NiJ9.* eyJqdGkiOiI5NTI3Iiwic3ViIjoiaGVqaWF5dW5fY29tbXVuaXR5IiwiaWF0IjoxNjgxMTM3MjI0LCJleHAiOjE2ODExMzcyMjR9.* evc01MRxLjpbksbMLdVPM9sJGYGhpC3UYOfm4-0sMGE*/}
解析TOKEN,打印结果
{jti=9527, sub=hejiayun_community, iat=1681137464, exp=1681237464, roles=admin}
4.2.3.5 入门案例认证流程分析
(1) 入门案例认证流程图
1) AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
的职责也就非常明确: 处理所有HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication。- 它的实现类 UsernamePasswordAuthenticationFilter 表示当前访问系统的用户,封装了用户相关信息。
2) AuthenticationManager
-
AuthenticationManager
定义了认证Authentication的方法 , 用来尝试对传入的Authentication对象进行认证。用于处理身份验证的核心逻辑;
ProviderManager
ProviderManager
是Authentication的一个实现,并将具体的认证操作委托给一系列的AuthenticationProvider来完成,从而可以实现支持多种认证方式。
3) AbstractUserDetailsAuthenticationProvider
-
ProviderManager 本身并不直接处理身份认证请求,它会委托给内部配置的Authentication Provider列表providers。该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证
-
providers集合的泛型是AuthenticationProvider接口,AuthenticationProvider接口有多个实现子类
4) DaoAuthenticationProvider
- AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,该类又有一个直接子类DaoAuthenticationProvider.
- Spring Security中默认就是使用Dao Authentication Provider来实现基于数据库模型认证授权工作的!
5) UserDetailsService
- DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。
- 如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;
- 如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。
6) InMemoryUserDetailsManager
- 它是UserDetailsService接口的实现类, 在内存中维护用户信息。使用方便,但是数据只保存在内存中,重启后数据丢失.
(2) 认证流程中对象之间的关系
虽然 Spring Security 看似很复杂,但是其核心思想和以前那种简单的认证流程依然是一样的。只不过,Spring Security 将其中的关键部分抽象了处理,又提供了相应的扩展接口。
我们在使用时,便可以实现自己的 UserDetailsService 和 UserDetails 来获取保存用户信息,实现自己的 Authentication 来保存特定的用户认证信息, 实现自己的 AuthenticationProvider 使用自己的 UserDetailsService 和 Authentication 来对用户认证信息进行效验。
4.2.3.6 重构入门案例-准备工作
(1) 需求分析
登录操作
- 自定义登录接口
- 调用ProviderManager的方法进行认证 如果认证通过生成jwt
- 把用户信息存入redis中
- 自定义UserDetailsService
- 在这个实现类中去查询数据库
(2) 添加依赖
<!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.74</version></dependency><!--jwt依赖--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version></dependency><!-- Mysql驱动包 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.32</version></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0</version></dependency><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency>
(3 )SpringBoot Redis缓存序列化处理
Spring Data Redis为我们封装了Redis客户端的各种操作,简化使用。
- 当Redis当做数据库或者消息队列来操作时,我们一般使用RedisTemplate来操作
- 当Redis作为缓存使用时,我们可以将它作为Spring Cache的实现,直接通过注解使用
SpringBoot RedisTemplate的序列化问题
- SpringBoot RedisTemplate用来操作Key-Value为对象类型,默认采用JDK序列化类型,JDK序列化性能差,而且存储到Redis服务端是二进制不便查询,JDK序列化要求实体实现
Serializable
接口.
① 添加序列化工具类,让Redis使用FastJson序列化,提高序列化效率, 将存储在Redis中的value值,序列化为JSON格式便于查看
/*** Redis使用FastJson进行序列化* @date 2023/4/10**/public class FastJsonJsonRedisSerializer<T> implements RedisSerializer<T> {@SuppressWarnings("unused")private ObjectMapper objectMapper = new ObjectMapper();public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}public void setObjectMapper(ObjectMapper objectMapper){Assert.notNull(objectMapper, "'objectMapper' must not be null");this.objectMapper = objectMapper;}protected JavaType getJavaType(Class<?> clazz){return TypeFactory.defaultInstance().constructType(clazz);}}
② 添加Redis配置类
@Configurationpublic class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();//配置连接工厂template.setConnectionFactory(connectionFactory);//使用FastJson2JsonRedisSerializer 来序列化和反序列化redis的value值FastJsonJsonRedisSerializer serializer = new FastJsonJsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();//指定要序列化的域: field,get和set,以及修饰符范围,ANY表示包括private和publicmapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);//指定序列化输入的类型,类必须是非final修饰的, final修饰的类会报异常.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);serializer.setObjectMapper(mapper);//redis中存储的value值,采用json序列化template.setValueSerializer(serializer);//redis中的key值,使用StringRedisSerializer来序列化和反序列化template.setKeySerializer(new StringRedisSerializer());//初始化RedisTemplate的一些参数设置template.afterPropertiesSet();return template;}}
(4) 导入工具类
- Redis工具类
/*** spring redis 工具类*/@SuppressWarnings(value = { "unchecked", "rawtypes" })@Componentpublic class RedisCache{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> long setCacheSet(final String key, final Set<T> dataSet){Long count = redisTemplate.opsForSet().add(key, dataSet);return count == null ? 0 : count;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}}
- JWT工具类
import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;import java.util.Date;import java.util.UUID;/*** JWT工具类*/public class JwtUtil {//有效期为public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时//设置秘钥明文public static final String JWT_KEY = "mashibing";public static String getUUID(){String token = UUID.randomUUID().toString().replaceAll("-", "");return token;}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @return*/public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间return builder.compact();}/*** 生成jtw* @param subject token中要存放的数据(json格式)* @param ttlMillis token超时时间* @return*/public static String createJWT(String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid) //唯一的ID.setSubject(subject) // 主题 可以是JSON数据.setIssuer("sg") // 签发者.setIssuedAt(now) // 签发时间.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥.setExpiration(expDate);}/*** 创建token* @param id* @param subject* @param ttlMillis* @return*/public static String createJWT(String id, String subject, Long ttlMillis) {JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}public static void main(String[] args) throws Exception {String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";Claims claims = parseJWT(token);System.out.println(claims);}/*** 生成加密后的秘钥 secretKey* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}
JWT工具类使用相关问题
-
秘钥长度不合理,将秘钥明文长度设置为 6位.
异常信息: Exception in thread "main" java.lang.IllegalArgumentException: Last unit does not have enough valid bits
//设置秘钥明文(长度为6位)public static final String JWT_KEY = "msbhjy";
-
1.8 以上版本,需要引入
JAXB API
相关依赖异常信息: java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0</version></dependency><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency>
- 字符串渲染工具类
public class WebUtils{/*** 将字符串渲染到客户端* * @param response 渲染对象* @param string 待渲染的字符串* @return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}}
4.2.3.7 重构入门案例-具体实现
(1) 通过数据库校验用户
通过前面的分析,我们得出结论:可以自定义一个UserDetailsService,并让Spring Security使用它。我们的UserDetailsService可以从数据库中获取用户名和密码。
- 创建数据库及用户表
CREATE TABLE `sys_user` (`user_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` VARCHAR(30) NOT NULL COMMENT '用户昵称',`password` VARCHAR(100) DEFAULT '' COMMENT '密码',`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',`sex` CHAR(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',`status` CHAR(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',PRIMARY KEY (`user_id`) USING BTREE) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户信息表'
- 引入MybatisPuls和mysql驱动的依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version></dependency><!-- Mysql驱动包 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.32</version></dependency>
- 配置数据库信息
spring:datasource:url: jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTCusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
- 创建实体类
@Data@AllArgsConstructor@NoArgsConstructor@TableName("sys_user")public class SysUser implements Serializable {/*** 主键*/@TableIdprivate Long userId;/*** 用户名*/private String userName;/*** 昵称*/private String nickName;/*** 密码*/private String password;/*** 手机号*/private String phonenumber;/*** 用户性别(0男,1女,2未知)*/private String sex;/*** 账号状态(0正常 1停用)*/private String status;}
- 定义Mapper接口
public interface UserMapper extends BaseMapper<User> {}
- 配置Mapper扫描
@SpringBootApplication@MapperScan("com.mashibing.springsecurity_example.mapper")public class SpringsecurityExampleApplication {public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(SpringsecurityExampleApplication.class, args);System.out.println("123456");}}
- 测试
@SpringBootTestpublic class MapperTest {@Autowiredprivate UserMapper userMapper;@Testpublic void testUserMapper(){List<User> users = userMapper.selectList(null);System.out.println(users);}}
(2) 引入SpringSecurity
第一步: 编写一个类,实现UserDetailsService接口,并重写其中的loadUserByUsername方法。在该方法中,使用用户名从数据库中检索用户信息。
/*** 根据用户名检索用户信息* @date 2023/4/14**/@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户信息LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUser::getUserName,username);SysUser user = userMapper.selectOne(wrapper);//如果查询不到数据,抛出异常 给出提示if(Objects.isNull(user)){throw new RuntimeException("用户名或密码错误");}//方法的返回值是 UserDetails接口类型,需要返回自定义的实现类return new LoginUser(user);}}
第二步 为了将用户信息转换为UserDetails类型的对象,需要创建一个类来实现UserDetails接口,并将用户信息封装在其中。
@Data@NoArgsConstructor@AllArgsConstructorpublic class LoginUser implements UserDetails {private SysUser sysUser;/*** 用于获取用户被授予的权限,可以用于实现访问控制。*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}/*** 用于获取用户的密码,一般用于进行密码验证。*/@Overridepublic String getPassword() {return sysUser.getPassword();}/*** 用于获取用户的用户名,一般用于进行身份验证。*/@Overridepublic String getUsername() {return sysUser.getUserName();}/*** 用于判断用户的账户是否未过期,可以用于实现账户有效期控制。*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 用于判断用户的账户是否未锁定,可以用于实现账户锁定功能。*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 用于判断用户的凭证(如密码)是否未过期,可以用于实现密码有效期控制。*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 用于判断用户是否已激活,可以用于实现账户激活功能。*/@Overridepublic boolean isEnabled() {return true;}}