在Spring Boot中编写基于JWT(JSON Web Token)的用户登录和注册功能,同时结合Spring Security和Redis来实现安全认证和令牌管理是一项复杂的任务。下面是一个基本的步骤和示例代码,可以帮助你入门。

# 步骤1: 创建Spring Boot项目

首先,创建一个Spring Boot项目并配置所需的依赖。确保在pom.xml文件中添加以下依赖项:

<dependencies>
    <!-- Hutool的JWT工具集 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-jwt</artifactId>
        <version>5.8.22</version>
    </dependency>
    <!-- jjwt:JSON Web令牌工具 -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <!-- spring-boot-starter-security:鉴权认证框架 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>2.6.2</version>
    </dependency>
</dependencies>

# 步骤2: 配置Spring Security

创建一个SecurityConfig类来配置Spring Security,并启用基本的身份验证和授权:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/register").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin().permitAll()
                .and()
            .logout().permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

# 步骤3: 创建用户实体和存储库

创建一个用户实体类,以及一个用于访问用户数据的存储库:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;

    // getters and setters
}
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

# 步骤4: 创建用户注册和登录控制器

创建一个控制器类来处理用户的注册和登录请求,并生成JWT令牌:

import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RestController
public class AuthController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.expirationMs}")
    private Long jwtExpirationMs;

    @PostMapping("/register")
    public String register(@RequestBody UserRequest userRequest) {
        if (StrUtil.isEmpty(userRequest.getUsername()) || StrUtil.isEmpty(userRequest.getPassword())) {
            return "Username and password are required!";
        }

        if (userRepository.findByUsername(userRequest.getUsername()) != null) {
            return "Username is already taken!";
        }

        User user = new User();
        user.setUsername(userRequest.getUsername());
        user.setPassword(passwordEncoder.encode(userRequest.getPassword()));
        userRepository.save(user);
        return "Registration successful!";
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody UserRequest userRequest) {
        User user = userRepository.findByUsername(userRequest.getUsername());
        if (user == null || !passwordEncoder.matches(userRequest.getPassword(), user.getPassword())) {
            throw new BadCredentialsException("Invalid username or password");
        }

        String token = generateToken(user.getUsername());

        Map<String, String> response = new HashMap<>();
        response.put("token", token);
        return response;
    }

    private String generateToken(String username) {
        Date expirationDate = new Date(System.currentTimeMillis() + jwtExpirationMs);

        return Jwts.builder()
                .setSubject(username)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }
}

# 步骤5: 配置JWT和Redis

application.propertiesapplication.yml中配置JWT和Redis相关属性:

# JWT Configuration
jwt.secret=YourSecretKey
jwt.expirationMs=86400000 # 1 day in milliseconds

# Redis Configuration
spring.redis.host=localhost
spring.redis.port=6379

# 步骤6: 运行应用程序

现在,你可以运行你的Spring Boot应用程序并测试注册和登录功能。注册用户后,将获得JWT令牌,可以在后续的请求中使用该令牌进行身份验证。

请注意,这只是一个基本的示例,实际生产环境中需要更多的安全措施,例如错误处理、身份验证错误处理、令牌刷新等。同时,应该保护/login/register端点,以防止恶意攻击。

要在登录或注册成功后将JWT令牌存入Redis,并在调用其他接口时检查请求头中的令牌与Redis中的令牌是否匹配,可以按照以下步骤进行操作:

# 步骤1: 在登录成功后将JWT令牌存入Redis

在登录成功后,将生成的JWT令牌存入Redis中,以便后续的访问进行验证。可以在AuthController中的login方法中添加以下代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

// ...

@Autowired
private RedisTemplate<String, String> redisTemplate;

// ...

@PostMapping("/login")
public Map<String, String> login(@RequestBody UserRequest userRequest) {
    // ...

    String token = generateToken(user.getUsername());

    // 存储令牌到Redis
    redisTemplate.opsForValue().set(user.getUsername(), token);

    Map<String, String> response = new HashMap<>();
    response.put("token", token);
    return response;
}

# 步骤2: 在其他接口中验证JWT令牌

创建一个拦截器或过滤器来验证请求头中的令牌是否与Redis中的令牌匹配。首先,创建一个JwtTokenFilter类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.server.ResponseStatusException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String token = extractToken(request);
            
            if (token != null) {
                String username = getUsernameFromToken(token);

                // 从Redis中获取存储的令牌
                String storedToken = redisTemplate.opsForValue().get(username);

                // 验证请求头中的令牌是否与Redis中的令牌匹配
                if (token.equals(storedToken)) {
                    SecurityContextHolder.getContext().setAuthentication(new JwtAuthentication(username));
                } else {
                    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token");
                }
            }
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException e) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token");
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        // 从请求头中提取令牌
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }

    private String getUsernameFromToken(String token) {
        return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
    }
}

# 步骤3: 注册拦截器

JwtTokenFilter注册为Spring Bean,以便它能够拦截请求并验证JWT令牌。在你的应用程序主类(通常带有@SpringBootApplication注解的类)中添加以下代码:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class YourApplication {

    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }

    @Bean
    public JwtTokenFilter jwtTokenFilter() {
        return new JwtTokenFilter();
    }
}

# 步骤4: 配置Spring Security

为了使JwtTokenFilter生效,你需要配置Spring Security以跳过对特定路径的验证。在SecurityConfig中添加以下代码:

import org.springframework.security.config.annotation.web.builders.WebSecurity;

// ...

@Override
public void configure(WebSecurity web) throws Exception {
    // 忽略对特定路径的验证
    web.ignoring().antMatchers("/register", "/login");
}

这将使Spring Security跳过对注册和登录路径的验证,但其他路径仍然会进行验证。

现在,你已经完成了JWT令牌存储和验证的配置。在调用其他需要身份验证的接口时,请求头中的令牌将与Redis中的令牌进行对比,以确保令牌的有效性。如果令牌无效,将返回401 Unauthorized响应。