【业务功能篇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
 @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的方法如下:

  1. 使用该 Jwts.parser()方法创建 JwtParserBuilder实例。
  2. setSigningKey() 与builder中签名方法signWith()对应,parser中的此方法拥有与signWith()方法相同的三种参数形式,用于设置JWT的签名key,用户后面对JWT进行解析。
  3. 最后,parseClaimsJws(String)用您的jws调用该方法,生成原始的JWS。
  4. 如果解析或签名验证失败,则整个调用将包装在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}
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;
     }
 
     @Override
     public byte[] serialize(T t) throws SerializationException
     {
         if (t == null)
         {
             return new byte[0];
         }
         return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
     }
 
     @Override
     public 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配置类

 @Configuration
 public 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和public
         mapper.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" })
 @Component
 public class RedisCache
 {
     @Autowired
     public 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=UTC
         username: root
         password: root
         driver-class-name: com.mysql.cj.jdbc.Driver
  • 创建实体类
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @TableName("sys_user")
 public class SysUser implements Serializable {
 
     /**
      * 主键
      */
     @TableId
     private 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");
     }
 }
  • 测试
 @SpringBootTest
 public class MapperTest {
 
     @Autowired
     private UserMapper userMapper;
 
     @Test
     public void testUserMapper(){
         List<User> users = userMapper.selectList(null);
         System.out.println(users);
     }
 }
(2) 引入SpringSecurity

第一步: 编写一个类,实现UserDetailsService接口,并重写其中的loadUserByUsername方法。在该方法中,使用用户名从数据库中检索用户信息。

 /**
  * 根据用户名检索用户信息
  * @date 2023/4/14
  **/
 @Service
 public class UserDetailsServiceImpl implements UserDetailsService {
 
     @Autowired
     private UserMapper userMapper;
 
     @Override
     public 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
 @AllArgsConstructor
 public class LoginUser implements UserDetails {
 
     private SysUser sysUser;
 
 
     /**
      *  用于获取用户被授予的权限,可以用于实现访问控制。
      */
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
         return null;
     }
 
     /**
      * 用于获取用户的密码,一般用于进行密码验证。
      */
     @Override
     public String getPassword() {
         return sysUser.getPassword();
     }
 
     /**
      * 用于获取用户的用户名,一般用于进行身份验证。
      */
     @Override
     public String getUsername() {
         return sysUser.getUserName();
     }
 
     /**
      * 用于判断用户的账户是否未过期,可以用于实现账户有效期控制。
      */
     @Override
     public boolean isAccountNonExpired() {
         return true;
     }
 
     /**
      * 用于判断用户的账户是否未锁定,可以用于实现账户锁定功能。
      */
     @Override
     public boolean isAccountNonLocked() {
         return true;
     }
 
     /**
      * 用于判断用户的凭证(如密码)是否未过期,可以用于实现密码有效期控制。
      */
     @Override
     public boolean isCredentialsNonExpired() {
         return true;
     }
 
     /**
      * 用于判断用户是否已激活,可以用于实现账户激活功能。
      */
     @Override
     public boolean isEnabled() {
         return true;
     }
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值