【业务功能篇58】Springboot + Spring Security 权限管理 【中篇】

4.2.3 认证

4.2.3.1 什么是认证(Authentication)

  • 通俗地讲就是验证当前用户的身份,证明“你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)
  • 互联网中的认证
    • 用户名密码登录
    • 邮箱发送登录链接
    • 手机号接收验证码
    • 只要你能收到邮箱/验证码,就默认你是账号的主人

4.2.3.2 两种认证方式

1) 基于Session的认证方式

session 认证流程:

image.png

  1. 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  2. 请求返回时将此 Session 的唯一标识 SessionID 返回给浏览器
  3. 浏览器接收到服务器返回的 SessionID 后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  4. 当用户第二次访问服务器的时候,请求会自动把此域名下的 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 的存储方式:

  1. 存到数据库中,每次客户端请求的时候取出来验证(服务端有状态)
  2. 存到 redis 中,设置过期时间,每次客户端请求的时候取出来验证(服务端有状态)
  3. 不存,每次客户端请求的时候根据之前的生成方法再生成一次来验证(JWT,服务端无状态)

Token特点:

  • 服务端无状态化、可扩展性好
  • 支持移动端设备
  • 安全
  • 支持跨程序调用

token 的身份验证流程:

image.png

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 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对象为载体,在用户和服务器之间传递安全可靠的信息.

image.png

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token head.payload.singurater
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
(2) JWT的组成部分

头部(Header)

  • 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
 {"typ":"JWT","alg":"HS256"}//typ(Type):令牌类型,也就是 JWT。//alg(Algorithm) :签名算法,比如 HS256。

JWT签名算法中,一般有两个选择,一个采用HS256,另外一个就是采用RS256

进行BASE64编码https://base64.us/,编码后的字符串如下:eyJhbGciOiJIUzI1NiJ9

image.png

载荷(payload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

 {"sub":"1234567890","name":"John Doe","admin":true}

将上面的JSON数据进行base64编码,得到Jwt第二部分: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

image.png

字段说明,下面的字段都是由 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) 签名的目的

最后一步签名的过程,实际上是对头部以及载荷内容进行签名。

image.png

一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。

所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。

服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用 alg字段指明了我们的加密算法了。

如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

image.png

(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的方法如下:

  1. 使用该 Jwts.parser()方法创建 JwtParserBuilder实例。
  2. setSigningKey() 与builder中签名方法signWith()对应,parser中的此方法拥有与signWith()方法相同的三种参数形式,用于设置JWT的签名key,用户后面对JWT进行解析。
  3. 最后,parseClaimsJws(String)用您的jws调用该方法,生成原始的JWS。
  4. 如果解析或签名验证失败,则整个调用将包装在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) 入门案例认证流程图

image.png

image.png

1) AbstractAuthenticationProcessingFilter

  • AbstractAuthenticationProcessingFilter的职责也就非常明确: 处理所有HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication。
  • 它的实现类 UsernamePasswordAuthenticationFilter 表示当前访问系统的用户,封装了用户相关信息。

2) AuthenticationManager

  • AuthenticationManager 定义了认证Authentication的方法 , 用来尝试对传入的Authentication对象进行认证。用于处理身份验证的核心逻辑;

    image.png

ProviderManager

  • ProviderManager是Authentication的一个实现,并将具体的认证操作委托给一系列的AuthenticationProvider来完成,从而可以实现支持多种认证方式。

3) AbstractUserDetailsAuthenticationProvider

  • ProviderManager 本身并不直接处理身份认证请求,它会委托给内部配置的Authentication Provider列表providers。该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证

    image.png

  • providers集合的泛型是AuthenticationProvider接口,AuthenticationProvider接口有多个实现子类

    image.png

4) DaoAuthenticationProvider

  • AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,该类又有一个直接子类DaoAuthenticationProvider.
  • Spring Security中默认就是使用Dao Authentication Provider来实现基于数据库模型认证授权工作的!

5) UserDetailsService

  • DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。
    • 如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;
    • 如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

6) InMemoryUserDetailsManager

  • 它是UserDetailsService接口的实现类, 在内存中维护用户信息。使用方便,但是数据只保存在内存中,重启后数据丢失.
(2) 认证流程中对象之间的关系

image.png

虽然 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工具类使用相关问题

  1. 秘钥长度不合理,将秘钥明文长度设置为 6位.

     异常信息: Exception in thread "main" java.lang.IllegalArgumentException: Last unit does not have enough valid bits
    
     //设置秘钥明文(长度为6位)public static final String JWT_KEY = "msbhjy"; 
    
  2. 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;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/12653.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode 122. 买卖股票的最佳时机 II

2023.7.29 把整体利润拆分成每天的利润&#xff0c;将股票值想象成一个折线图&#xff0c;将所有上升的值相加即可。 代码&#xff1a; class Solution { public:int maxProfit(vector<int>& prices) {int ans 0;for(int i1; i<prices.size(); i){if(prices[i]-…

SpringBoot使用jetty和tomcat还有undertow以及ssl配置支持https请求

一般使用SpringBoot开发应用程序都是使用的tomcat 稍微注意点性能就使用undertow&#xff0c;配置支持https请求常用nginx来做代理&#xff0c;直接用SpringBoot配置还是很少的&#xff0c;八成用不到&#xff0c;就怕需要用到的时候又不能及时弄出来&#xff0c;于是记录一下。…

CenOS设置启动级别

背景知识 init一共分为7个级别&#xff0c;这7个级别的所代表的含义如下 0&#xff1a;停机或者关机&#xff08;千万不能将initdefault设置为0&#xff09;1&#xff1a;单用户模式&#xff0c;只root用户进行维护2&#xff1a;多用户模式&#xff0c;不能使用NFS(Net File S…

第2集丨webpack 江湖 —— 创建一个简单的webpack工程demo

目录 一、创建webpack工程1.1 新建 webpack工程目录1.2 项目初始化1.3 新建src目录和文件1.4 安装jQuery1.5 安装webpack1.6 配置webpack1.6.1 创建配置文件&#xff1a;webpack.config.js1.6.2 配置dev脚本1.7 运行dev脚本 1.8 查看效果1.9 附件1.9.1 package.json1.9.2 webpa…

ReactRouterv5在BrowserRouter和HashRouter模式下对location.state的支持

结论&#xff1a;HashRouter不支持location.state 文档&#xff1a;ReactRouter v5 从文档可看到history.push()方法支持2个参数&#xff1a;path, [state] state即是location.state&#xff0c;常用于隐式地传递状态参数 但文档未提的是&#xff0c;仅适用于BrowserRouter&am…

《面试1v1》Kafka的架构设计是什么样子

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

百度开发者平台API地理编码,根据地址获取经纬度

地理编码 | 百度地图API SDK (baidu.com) 原始csv # encoding:utf-8 import requests import csv import json # 接口地址 url "https://api.map.baidu.com/geocoding/v3"# 此处填写你在控制台-应用管理-创建应用后获取的AK ak "XXXXXXX"# 创建CSV文件并…

04-树6 Complete Binary Search Tree

思路&#xff1a; 先排序 用数组建一棵空树 中序遍历填数 顺序输出即为层次遍历

企业电子招投标采购系统源码之-java spring cloud+spring boot

​ 信息数智化招采系统 服务框架&#xff1a;Spring Cloud、Spring Boot2、Mybatis、OAuth2、Security 前端架构&#xff1a;VUE、Uniapp、Layui、Bootstrap、H5、CSS3 涉及技术&#xff1a;Eureka、Config、Zuul、OAuth2、Security、OSS、Turbine、Zipkin、Feign、Monitor、…

PC音频框架学习

1.整体链路 下行播放&#xff1a; App下发音源→CPU Audio Engine 信号处理→DSP数字信号处理→Codec DAC→PA→SPK 上行录音&#xff1a; MIC拾音→集成运放→Codec ADC→DSP数字信号处理→CPU Audio Engine 信号处理→App 2.硬件 CPU PCH DSP(可选) Codec PA SPKbox MIC…

Ansible安装部署与应用

文章目录 一、ansible简介二、ansible 环境安装部署三、ansible 命令行模块3.1 command 模块3.2 shell 模块3.3 cron 模块3.4 user 模块3.5 group 模块3.6 copy 模块3.7 file 模块3.8 hostname 模块3.9 ping 模块3.10 yum 模块3.11 service/systemd 模块3.12 script 模块3.13 m…

latex论文----写作代码

一般来说论文机构会给定latex模板代码&#xff0c;我们只需要知道怎么写就行&#xff0c;格式机构都给你调好了 1 各类标题 section是最大的标题&#xff0c;后边每一级小标题&#xff0c;都在前边加个sub就行 \section{Method} \subsection{Dataset} \subsubsection{Dataset…

Mac 系统钥匙串证书不受信任

Mac 系统钥匙串证书不受信任 解决办法 通过尝试安装 Apple PKI 的 Worldwide Developer Relations - G4 (Expiring 12/10/2030 00:00:00 UTC) 解决该异常问题 以上便是此次分享的全部内容&#xff0c;希望能对大家有所帮助!

【图论】LCA(倍增)

一.LCA介绍 LCA通常指的是“最近共同祖先”&#xff08;Lowest Common Ancestor&#xff09;。LCA是一种用于解决树或图结构中两个节点的最低共同祖先的问题的算法。 在树结构中&#xff0c;LCA是指两个节点的最近层级的共同祖先节点。例如&#xff0c;考虑一棵树&#xff0c;…

element-ui form表单的动态rules校验

在vue 项目中&#xff0c;有时候可能会用到element-ui form表单的动态rules校验&#xff0c;比如说选择了哪个选项&#xff0c;然后动态显示或者禁用等等。 我们可以巧妙的运用element-ui form表单里面form-item想的校验规则来处理&#xff08;每一个form-item项都可以单独校验…

Elasticsearch-倒排索引

Elasticsearch和Lucene的关系 Lucene 是一个开源、免费、高性能、纯 Java 编写的全文检索引擎&#xff0c;可以算作是开源领域最好的全文检索工具包。ElasticSearch 是基于Lucene实现的一个分布式、可扩展、近实时性的高性能搜索与数据分析引擎。 Lucene索引层次结构 Lucene的…

组件间通信案例练习

1.实现父传子 App.vue <template><div class"app"><tab-control :titles["衣服","鞋子","裤子"]></tab-control><tab-control :titles["流行","最新","优选","数码&q…

win10系统wps无法启动(打开文档)

我的win10系统中&#xff0c;之前可以顺畅地打开wps&#xff0c;但最近无法打开文档&#xff0c;停留在启动页面&#xff0c;在任务管理器中可以看到启动的wps线程&#xff0c;如果继续双击文档&#xff0c;线程增加&#xff0c;但依然无法打开文档。 wps版本是刚刚更新的15120…

代码随想录算法训练营第二十五天 | 读PDF复习环节3

读PDF复习环节3 本博客的内容只是做一个大概的记录&#xff0c;整个PDF看下来&#xff0c;内容上是不如代码随想录网站上的文章全面的&#xff0c;并且PDF中有些地方的描述&#xff0c;是很让我疑惑的&#xff0c;在困扰我很久后&#xff0c;无意间发现&#xff0c;其网站上的讲…

海外ASO优化之应用商店本地化

大多数应用可供世界任何地方的用户使用&#xff0c;所以需要以多种不同语言来展示我们的应用。它能够包含在跨地理区域的搜索结果中&#xff0c;从而提高全球可见性和转化率。 1、关键词的研究&#xff0c;对于确定流行的本地关键词至关重要。 在本地化Google Play的应用页面时…