【Java手搓RAGFlow】-3- 用户认证与权限管理
- [1 引言](#1 引言)
- [2 JWT](#2 JWT)
-
- [2.1 JWT组成](#2.1 JWT组成)
- [2.2 JWT流程](#2.2 JWT流程)
- [2.3 生成JWT Secret Key](#2.3 生成JWT Secret Key)
- [2.4 创建JwtUtils](#2.4 创建JwtUtils)
- [3 密码加密](#3 密码加密)
- [4 实现User相关代码](#4 实现User相关代码)
-
- [4.1 实现UserService](#4.1 实现UserService)
- [4.2 实现CustomUserDetailsService](#4.2 实现CustomUserDetailsService)
- [5 实现安全配置与认证流程](#5 实现安全配置与认证流程)
-
- [5.1 实现JwtAuthenticationFilter](#5.1 实现JwtAuthenticationFilter)
- [5.2 更新SecurityConfig](#5.2 更新SecurityConfig)
- [5.3 实现认证控制器](#5.3 实现认证控制器)
- [6 测试](#6 测试)
-
- [6.1 测试注册接口](#6.1 测试注册接口)
- [6.2 测试登录接口](#6.2 测试登录接口)
1 引言
在构建任何一个成熟的RAG系统之前,一个稳定可靠的用户认证与权限管理模块是必不可少的基础设施。它能确保系统的安全性,保护用户数据,并为后续的个性化服务和资源隔离打下基础。上一章我们已经搭建好了项目的基础骨架,现在,我们需要亲手为它填充上认证与权限管理的核心功能。
常见的认证方案有:
- Session 认证:传统方案,需要服务器存储会话这是一种传统的认证方案。用户登录成功后,服务器会创建一个Session来存储用户信息,并将一个Session ID通过Cookie返回给浏览器。后续请求浏览器都会携带这个Session ID,服务器以此来识别用户。这种方案的优点是简单直观,但缺点是需要在服务器端存储会话信息,当系统需要水平扩展时,会面临Session共享的难题。
- JWT 认证:它是一种无状态的认证方案。用户登录后,服务器会生成一个包含用户信息的加密Token并返回给客户端。客户端在后续请求中只需携带这个Token即可,服务器通过验证Token的签名来确认用户身份,无需在服务端存储任何会话信息。这使得它天然适合分布式系统和微服务架构。
- OAuth2:它是一个关于授权的开放标准,更侧重于允许第三方应用访问用户在某个服务上的资源,而不是直接的用户认证。我们常见的"微信登录"、"GitHub登录"等第三方登录功能,就是基于OAuth2实现的。
我们最终选择 JWT 作为本项目的认证方案。主要原因有两点:一是我个人对它比较熟悉,开发效率更高;二是因为它的无状态特性,这对于未来系统可能的扩展和维护都极为友好。=v=
2 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。
2.1 JWT组成
JWT 由三部分组成:
java
Header.Payload.Signature
-
Header:通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法(如HS256或RS256)。这部分内容会经过Base64Url
-
Payload:负载:也称为声明Claims,是存放有效信息的地方。这里可以包含用户ID、用户名、角色、过期时间等自定义数据。需要注意的是,Payload默认只经过Base64Url编码,并非加密,因此不应在其中存放任何敏感信息。
-
Signature:签名的作用是验证消息在传输过程中没有被篡改,并验证发送者的身份。它由三部分组合生成:编码后的Header、编码后的Payload、以及一个只有服务器知道的密钥(Secret),再通过Header中指定的签名算法进行加密生成。
2.2 JWT流程
JWT的认证流程清晰明了,具体步骤如下:
1. 用户登录
↓
2. 服务器验证用户名密码
↓
3. 生成 JWT Token
↓
4. 返回 Token 给客户端
↓
5. 客户端存储 Token(localStorage/cookie)
↓
6. 后续请求携带 Token
↓
7. 服务器验证 Token
↓
8. 返回数据
2.3 生成JWT Secret Key
签名的密钥是JWT安全性的核心,它必须被妥善保管在服务端,绝不能泄露。我们可以通过下面的代码来生成一个足够安全的密钥。
创建com.alibaba.utils.GenerateJwtKey,我们通过代码生成一个高强度的随机密钥,并将其配置在application.yml中,而不是硬编码在代码里,这是一种更安全的实践。
java
import io.jsonwebtoken.security.Keys;
import java.util.Base64;
public class GenerateJwtKey {
public static void main(String[] args) {
byte[] keyBytes = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256).getEncoded();
String base64Key = Base64.getEncoder().encodeToString(keyBytes);
System.out.println("Base64 Encoded Key: " + base64Key);
}
}
运行这个类,将生成的密钥复制到 application.yml,这个用我们openssl也可以的。
2.4 创建JwtUtils
为了让JWT相关的操作(生成、解析、验证)逻辑集中管理,我们创建一个JwtUtils工具类。
当其他服务需要操作JWT时,只需注入并调用这个工具类即可,避免了在多处编写重复的代码。
创建com.alibaba.utils.JwtUtils
java
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT 工具类
* 负责生成、验证和解析 JWT Token
*/
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${jwt.secret-key}")
private String secretKeyBase64;
@Value("${jwt.expiration}")
private long expiration;
/**
* 解析 Base64 密钥,并返回 SecretKey
*/
private SecretKey getSigningKey() {
byte[] keyBytes = Base64.getDecoder().decode(secretKeyBase64);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成 JWT Token
*
* @param username 用户名
* @param userId 用户ID
* @param role 用户角色
* @return JWT Token
*/
public String generateToken(String username, Long userId, String role) {
SecretKey key = getSigningKey();
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId.toString());
claims.put("role", role);
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 验证 JWT Token 是否有效
*
* @param token JWT Token
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
logger.warn("Token expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.warn("Unsupported token: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.warn("Malformed token: {}", e.getMessage());
} catch (Exception e) {
logger.error("Error validating token", e);
}
return false;
}
/**
* 从 JWT Token 中提取用户名
*
* @param token JWT Token
* @return 用户名
*/
public String extractUsernameFromToken(String token) {
try {
Claims claims = extractClaims(token);
return claims != null ? claims.getSubject() : null;
} catch (Exception e) {
logger.error("Error extracting username from token", e);
return null;
}
}
/**
* 从 JWT Token 中提取用户ID
*
* @param token JWT Token
* @return 用户ID
*/
public String extractUserIdFromToken(String token) {
try {
Claims claims = extractClaims(token);
return claims != null ? claims.get("userId", String.class) : null;
} catch (Exception e) {
logger.error("Error extracting userId from token", e);
return null;
}
}
/**
* 从 JWT Token 中提取用户角色
*
* @param token JWT Token
* @return 用户角色
*/
public String extractRoleFromToken(String token) {
try {
Claims claims = extractClaims(token);
return claims != null ? claims.get("role", String.class) : null;
} catch (Exception e) {
logger.error("Error extracting role from token", e);
return null;
}
}
/**
* 提取 Claims
*/
private Claims extractClaims(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.debug("Cannot extract claims from token: {}", e.getMessage());
return null;
}
}
}
3 密码加密
用户的密码绝不能以明文形式存储在数据库中,这会带来巨大的安全风险。我们必须对密码进行加密处理。这里我们选用BCrypt算法。
为什么使用BCrypt?
BCrypt是一种专为密码哈希设计、经过时间考验的强大算法。它有两个显著的优点:
- 自动加盐:对于同一个原始密码,每次加密生成的结果都是不同的。这是因为它在加密时会自动生成一个随机的盐,并将盐和密文存储在一起。这可以有效抵御彩虹表攻击。
- Spring Security 内置支持:Spring Security框架原生支持BCrypt算法,我们可以非常方便地集成,无需引入额外的依赖和复杂的配置。
创建com.alibaba.utils.PasswordUtil
java
public class PasswordUtil {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
/**
* 加密密码
*/
public static String encode(String rawPassword) {
return encoder.encode(rawPassword);
}
/**
* 验证密码是否匹配
*/
public static boolean matches(String rawPassword, String encodedPassword) {
return encoder.matches(rawPassword, encodedPassword);
}
}
4 实现User相关代码
有了JWT工具和密码加密工具,我们现在可以开始编写用户管理的核心业务逻辑了。
4.1 实现UserService
UserService是一个业务逻辑层的组件。它负责处理与用户相关的业务操作,如注册和登录验证。
创建com.alibaba.service.UserService
java
/**
* 用户服务类
* 处理用户注册、登录等业务逻辑
*/
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
/**
* 注册新用户
*
* @param username 用户名
* @param password 密码
* @throws RuntimeException 如果用户名已存在
*/
@Transactional
public User registerUser(String username, String password) {
// 检查用户名是否已存在
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("Username already exists");
}
// 创建新用户
User user = new User();
user.setUsername(username);
user.setPassword(PasswordUtil.encode(password)); // 加密密码
user.setRole(User.Role.USER); // 默认角色为普通用户
User savedUser = userRepository.save(user);
logger.info("User registered successfully: {}", username);
return savedUser;
}
/**
* 验证用户登录
*
* @param username 用户名
* @param password 密码
* @return 用户对象,如果验证失败返回 null
*/
public User validateUser(String username, String password) {
Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isEmpty()) {
logger.warn("User not found: {}", username);
return null;
}
User user = userOpt.get();
// 验证密码
if (!PasswordUtil.matches(password, user.getPassword())) {
logger.warn("Invalid password for user: {}", username);
return null;
}
logger.info("User authenticated successfully: {}", username);
return user;
}
/**
* 根据用户名查找用户
*
* @param username 用户名
* @return 用户对象
*/
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
}
4.2 实现CustomUserDetailsService
Spring Security在进行认证时,需要一个标准的方式来根据用户名加载用户的详细信息(包括加密后的密码和权限列表)。
UserDetailsService就是这个标准接口。我们通过实现它的loadUserByUsername方法,告诉Spring Security:"当你需要用户信息时,请调用这个方法,我会从我的数据库中查询,并以你(Spring Security)能理解的UserDetails格式返回给你"。这样,我们的用户体系就成功地接入了Spring Security的认证流程。
为了让Spring Security 能够加载用户信息,我们需要实现 UserDetailsService:
java
import com.alibaba.model.User;
import com.alibaba.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
/**
* 自定义 UserDetailsService
* 用于 Spring Security 加载用户信息
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user.getRole())
);
}
/**
* 将用户角色转换为 Spring Security 的权限格式
*/
private Collection<? extends GrantedAuthority> getAuthorities(User.Role role) {
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
}
5 实现安全配置与认证流程
现在,我们将所有组件串联起来,配置Spring Security,让它使用我们定义的JWT认证逻辑。
5.1 实现JwtAuthenticationFilter
Filter(过滤器)是Java Web中处理请求和响应的链式结构。我们自定义的JwtAuthenticationFilter是整个JWT认证流程的核心执行者。
它的作用是拦截每一个进入系统的HTTP请求。在请求到达真正的业务控制器(Controller)之前,这个过滤器会检查请求头中是否携带了有效的JWT。如果Token有效,它会解析出用户信息,并将其设置到Spring Security的"安全上下文"(SecurityContextHolder)中。
这个动作相当于告诉Spring Security:"当前这个请求的用户已经通过认证了,他的身份是xxx,角色是yyy"。这样,后续的授权检查(比如检查用户是否有权访问某个API)才能正常工作。
在每个请求中检查JWT Token,并设置用户认证信息
java
import com.alibaba.service.CustomUserDetailsService;
import com.alibaba.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 认证过滤器
* 在每个请求中检查 JWT Token,并设置用户认证信息
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 从请求头中提取 JWT Token
String token = extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
// Token 有效,提取用户名
String username = jwtUtils.extractUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到 Spring Security 上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
String userId = jwtUtils.extractUserIdFromToken(token);
request.setAttribute("userId", userId);
}
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}",e);
}
filterChain.doFilter(request, response);
}
/**
* 从请求头中提取 JWT Token
* 格式:Authorization: Bearer <token>
*/
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // 去掉 "Bearer " 前缀
}
return null;
}
}
5.2 更新SecurityConfig
更新com.alibaba.config.SecurityConfig
java
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security 配置类
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF
.csrf(csrf -> csrf.disable())
// 配置请求授权
.authorizeHttpRequests(auth -> auth
// 允许公开访问的接口
.requestMatchers("/api/v1/users/register", "/api/v1/users/login").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 设置会话管理策略为无状态
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 添加 JWT 认证过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5.3 实现认证控制器
它向外界暴露了两个核心的API接口:/register用于用户注册,/login用于用户登录。
它负责接收前端传递过来的HTTP请求和JSON数据,对参数进行基础校验,然后调用UserService来执行实际的业务逻辑。
当业务逻辑成功完成后(例如登录成功),它会调用JwtUtils生成Token,并将其封装在标准的HTTP响应中返回给客户端。
创建com.alibaba.controller.AuthController
java
import com.alibaba.model.User;
import com.alibaba.service.UserService;
import com.alibaba.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 认证控制器
* 处理用户注册和登录
*/
@RestController
@RequestMapping("/api/v1/users")
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
/**
* 用户注册
*/
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
try {
// 参数验证
if (request.username() == null || request.username().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("code", 400, "message", "Username cannot be empty"));
}
if (request.password() == null || request.password().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("code", 400, "message", "Password cannot be empty"));
}
// 注册用户
User user = userService.registerUser(request.username(), request.password());
// 生成 Token
String token = jwtUtils.generateToken(
user.getUsername(),
user.getId(),
user.getRole().name()
);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "Registration successful",
"data", Map.of(
"token", token,
"user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"role", user.getRole().name()
)
)
));
} catch (RuntimeException e) {
return ResponseEntity.badRequest()
.body(Map.of("code", 400, "message", e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(500)
.body(Map.of("code", 500, "message", "Internal server error"));
}
}
/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
// 参数验证
if (request.username() == null || request.username().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("code", 400, "message", "Username cannot be empty"));
}
if (request.password() == null || request.password().isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("code", 400, "message", "Password cannot be empty"));
}
// 验证用户
User user = userService.validateUser(request.username(), request.password());
if (user == null) {
return ResponseEntity.status(401)
.body(Map.of("code", 401, "message", "Invalid username or password"));
}
// 生成 Token
String token = jwtUtils.generateToken(
user.getUsername(),
user.getId(),
user.getRole().name()
);
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "Login successful",
"data", Map.of(
"token", token,
"user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"role", user.getRole().name()
)
)
));
} catch (Exception e) {
return ResponseEntity.status(500)
.body(Map.of("code", 500, "message", "Internal server error"));
}
}
// 请求 DTO
record RegisterRequest(String username, String password) {}
record LoginRequest(String username, String password) {}
}
6 测试

启动成功后我们一一进行测试
6.1 测试注册接口
POST http://localhost:8081/api/v1/users/register

返回:
json
{
"message": "Registration successful",
"data": {
"user": {
"username": "testuser",
"id": 1,
"role": "USER"
},
"token": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiVVNFUiIsInVzZXJJZCI6IjEiLCJzdWIiOiJ0ZXN0dXNlciIsImlhdCI6MTc2MzI5NzUxMCwiZXhwIjoxNzYzMzgzOTEwfQ.5Uthh_z_iXD0vE__Fdv53FN-MNJ7185NntF6Dnoj7x0"
},
"code": 200
}
查看数据库,落表成功

6.2 测试登录接口
POST http://localhost:8081/api/v1/users/login

返回:
java
{
"message": "Login successful",
"data": {
"user": {
"username": "testuser",
"id": 1,
"role": "USER"
},
"token": "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiVVNFUiIsInVzZXJJZCI6IjEiLCJzdWIiOiJ0ZXN0dXNlciIsImlhdCI6MTc2MzI5NzY5MSwiZXhwIjoxNzYzMzg0MDkxfQ.l6iBcLc9VoXjgPauMqh1juCdXr8SAmPW5G3NNEsQoRY"
},
"code": 200
}
至此,我们的用户认证和权限管理已经编写完毕,下一章开始我们就会开始真正的编写我们的核心流程。