Spring Security实现用户认证四:使用JWT与Redis实现无状态认证

Spring Security实现用户认证四:使用JWT与Redis实现无状态认证

  • 1 什么是无状态认证?
  • 2 什么是JWT?
    • 2.1 需要注意的事项
    • 2.2 JWT构成
  • 3 Spring Security + JWT实现无状态认证
    • 3.1 创建一个Spring Boot项目
      • 3.1.1 依赖
      • 3.1.2 Main
      • 3.1.3 application.yml
    • 3.2 Controller
      • 3.2.1 LoginController
      • 3.3.2 IndexController
    • 3.3 User Entity
    • 3.4 Service
      • UserService
      • UserServiceImpl
    • 3.5 UserMapper.java
    • 3.6 UserMapper.xml
    • 3.7 JwtTokenProvider.java
    • 3.8 Redis配置类
    • 3.9 MyRedisSecurityContextRepository.java
    • 3.10 DBUserDetailManager.java
    • 3.11 SpringSecurityConfig配置类
    • 3.12 MyAuthenticationEntryPoint

1 什么是无状态认证?

在基本的通信流程中,我们一般采用Session去存储用户的认证状态。在Spring Security实现用户认证三中讲过,在拿到前端传输过来的用户名和密码之后,会有专门的过滤器UsernamePasswordAuthenticationFilter处理这部分的需求,并且对认证成功的用户生成Token且存储在Session中。在下次发起请求时,直接从Session中取出同用户名的token进行密码哈希的比较要认证用户。

对于无状态认证,则我们的认证不依赖与服务器端存储的Session的状态。所以无状态认证需要我们每次从前端传输一个包含完整认证信息的Token到服务器端进行自定义的认证过程,这使得服务器无需存储和管理会话数据。常见的无状态认证方法包括 JSON Web Token (JWT)、API Key和 OAuth 2.0。

2 什么是JWT?

JWT(JSON Web Token)是一种基于JSON的开放标准(RFC 7519),用于在各方之间传递信息。JWT可以进行数字签名,并且可以选择加密其内容。它定义了一种紧凑和自包含的方式, 可以通过URL、POST参数或HTTP头在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的,但是签名不能保证数据的机密性。JWT 可以使用 HMAC 算法、RSA 或 ECDSA 的公钥/私钥对进行签名。

JWT最常见的用途是用户身份验证。一旦用户登录成功,服务器会生成一个JWT并返回给客户端。客户端将JWT存储在本地(如localStorage或cookie),并在每次请求时将其发送到服务器,服务器通过验证JWT来验证用户身份。

2.1 需要注意的事项

  • 保密:不要在JWT中存储敏感信息,因为JWT是可以被解码的。
  • 过期处理:设置合理的过期时间,并且在需要时支持刷新令牌机制。
  • 使用HTTPS:确保在传输JWT时使用HTTPS,防止中间人攻击。

2.2 JWT构成

JWT由三个主要部分构成:Header(头部)、Payload(负载)和 Signature(签名)。每个部分都有其特定的作用和结构。

  1. Header(头部)
    头部通常包含两个部分:令牌类型和使用的签名算法。头部数据结构为一个JSON对象,然后进行Base64Url编码。
{
  "alg": "HS256",
  "typ": "JWT"
}
  1. Payload(负载)
    负载部分包含了声明(claims),即需要传输的数据。这些数据可以是关于用户的信息或者其他的元数据。声明可以分为三类:
  • Registered claims(注册声明):预定义的一些声明,如 iss(签发者),exp(过期时间),sub(主题),aud(受众)。
  • Public claims(公共声明):可以自定义的声明,但为了避免冲突,应使用URI命名。
  • Private claims(私有声明):由双方约定的声明,用于信息交换。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  1. Signature(签名)
    签名部分用于验证消息的发送者和确保消息在传递过程中未被篡改。签名的生成过程如下:
  • 将编码后的Header和Payload用句点 (.) 连接起来:
base64UrlEncode(header) + "." + base64UrlEncode(payload)
  • 使用头部中指定的签名算法,并结合一个密钥对上述连接的字符串进行签名。
  • 对于HMAC SHA256算法,签名过程如下:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

最终,JWT的格式为:

header.payload.signature

3 Spring Security + JWT实现无状态认证

登录认证流程
登录认证流程如上。

3.1 创建一个Spring Boot项目

3.1.1 依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- 或者jjwt-gson,如果你更喜欢Gson -->
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

<!--        redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<!--    数据库    -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
</dependency>
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.21</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
</dependency>

3.1.2 Main

package com.song.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.song.cloud.mapper")
public class ServiceSecurityJwt6501 {
    public static void main(String[] args) {
        SpringApplication.run(ServiceSecurityJwt6501.class, args);
    }
}

3.1.3 application.yml

spring:
  application:
    name: service-security-jwt

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root
    # 注意修改数据库名字
    url: jdbc:mysql://localhost:3306/test? characterEncoding=utf8&useSSL=false&serverTimeZone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
  data:  # 配置redis
    redis:
      port: 6379
      host: 192.168.62.128
      password: 1234

app:
  jwt-sign-secret: gOk33w29WESOMEx8vUQLb69AsGhlUb7UmrFwu3g2TOo=
  jwt-expiration-milliseconds: 604800000  # 七天过期

server:
  port: 6501


logging:
  level:
    web: debug
    org.springframework.security: debug

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.song.cloud.entities  # 注意修改成自己的包名
  configuration:
    map-underscore-to-camel-case: true

3.2 Controller

3.2.1 LoginController

用来处理

package com.song.cloud.controller;

import com.song.cloud.entities.User;
import com.song.cloud.service.UserService;
import com.song.cloud.utils.JwtTokenProvider;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {

    @Resource
    private JwtTokenProvider jwtTokenProvider;

    @Resource
    private UserService userService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @PostMapping("/api/auth")
    public String auth(@RequestBody User user){
        System.out.println(user);

        UserDetails userDetails =  userService.loadUserDetail(user);
        PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        boolean matches = delegatingPasswordEncoder.matches(user.getPasswordHash(), userDetails.getPassword());
        if(matches){
            UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
            token.setDetails(userDetails);
            System.out.println(token);
            //保存token到redis
            redisTemplate.opsForValue().set(userDetails.getUsername(), token);
            return jwtTokenProvider.generateToken(token);
        }
        return "fail";
    }
}

3.3.2 IndexController

package com.song.cloud.controller;

import jakarta.servlet.http.HttpSession;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@RestController
public class IndexController {

    @GetMapping("/")
    public Map index() {

        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        Object principal = authentication.getPrincipal();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //脱敏处理
        Object credentials = authentication.getCredentials();

        HashMap<Object, Object> map = new HashMap<>();

        map.put("username", authentication.getName());
        map.put("authorities", authorities);
        map.put("credentials", credentials);
        map.put("details", authentication.getDetails());
        map.put("principal", principal);
        
        return map;

    }
}

3.3 User Entity

package com.song.cloud.entities;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * 表名:t_users_test
*/
@Table(name = "t_users_test")
public class User {
    /**
     * id
     */
    @Id
    @GeneratedValue(generator = "JDBC")
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码hash
     */
    @Column(name = "password_hash")
    private String passwordHash;

    /**
     * 是否启用
     */
    private Boolean enable;

    /**
     * 获取id
     *
     * @return id - id
     */
    public Long getId() {
        return id;
    }

    /**
     * 设置id
     *
     * @param id id
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * 获取用户名
     *
     * @return username - 用户名
     */
    public String getUsername() {
        return username;
    }

    /**
     * 设置用户名
     *
     * @param username 用户名
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 获取密码hash
     *
     * @return passwordHash - 密码hash
     */
    public String getPasswordHash() {
        return passwordHash;
    }

    /**
     * 设置密码hash
     *
     * @param passwordHash 密码hash
     */
    public void setPasswordHash(String passwordHash) {
        this.passwordHash = passwordHash;
    }

    /**
     * 获取是否启用
     *
     * @return enable - 是否启用
     */
    public Boolean getEnable() {
        return enable;
    }

    /**
     * 设置是否启用
     *
     * @param enable 是否启用
     */
    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    @Override
    public String toString() {
        return "User{" +
                "enable=" + enable +
                ", id=" + id +
                ", username='" + username + '\'' +
                ", passwordHash='" + passwordHash + '\'' +
                '}';
    }
}

3.4 Service

UserService

package com.song.cloud.service;

import com.song
上一篇:[移动通讯]【无线感知-P2】[特征,算法,数据集】


下一篇:Web前端不挂科:深入探索与实战指南