JDWA Green-U 用户认证模块
本文档详细介绍了JDWA Green-U项目中用户认证模块的设计思路、实现细节和使用方法。该模块实现了基于JWT的用户认证系统,包括用户注册、登录、权限验证等核心功能。
模块概述
用户认证模块是整个系统的基础,负责用户身份验证与安全控制。本模块采用JWT(JSON Web Token)实现无状态认证,集成Spring Security框架提供全面的安全保障。
核心功能
- 用户注册与账号管理
- 用户登录与JWT令牌颁发
- 接口访问权限控制
- 用户角色与权限管理
- 安全防护(密码加密、令牌验证)
技术选型
- 认证框架: Spring Security
- 令牌技术: JWT (JSON Web Token)
- 加密算法: BCrypt + MD5
- 安全防护: CSRF防护、XSS过滤
数据模型设计
用户表设计
用户表(jdwa_user
)存储用户基本信息:
字段名 | 类型 | 说明 |
---|---|---|
id | BIGINT | 主键ID |
username | VARCHAR(50) | 用户名 |
password | VARCHAR(100) | 密码(加密存储) |
nickname | VARCHAR(50) | 昵称 |
avatar | VARCHAR(255) | 头像URL |
VARCHAR(100) | 邮箱 | |
phone | VARCHAR(20) | 手机号 |
gender | TINYINT | 性别(0-未知,1-男,2-女) |
birthday | DATE | 出生日期 |
total_carbon | DECIMAL(10,2) | 总碳减排量 |
carbon_points | INT | 当前积分 |
total_points | INT | 累计积分 |
status | TINYINT | 状态(0-禁用,1-启用) |
create_time | DATETIME | 创建时间 |
update_time | DATETIME | 更新时间 |
角色与权限表
在简化版实现中,角色和权限直接硬编码在代码中,生产环境应建立专门的角色权限表。
核心类与接口
实体类
@Data
@TableName("jdwa_user")
public class JDWAUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
private String avatar;
private String email;
private String phone;
private Integer gender;
private LocalDate birthday;
private BigDecimal totalCarbon;
private Integer carbonPoints;
private Integer totalPoints;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
JWT工具类
@Component
public class JDWAJwtUtil {
// 密钥(实际应用中应从配置文件读取)
private final SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
// Token有效期(24小时)
private final long expiration = 86400000;
/**
* 生成JWT令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}
/**
* 验证令牌
*/
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 其他辅助方法...
}
JWT过滤器
@Component
public class JDWAJwtAuthenticationFilter extends OncePerRequestFilter {
private final JDWAJwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取Authorization
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
// 检查请求头中是否包含Bearer令牌
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
try {
// 从令牌中提取用户名
username = jwtUtil.getUsernameFromToken(token);
} catch (Exception e) {
logger.error("JWT解析失败", e);
}
}
// 验证用户身份并设置认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug("用户 [{}] 认证成功", username);
}
}
filterChain.doFilter(request, response);
}
}
安全配置类
@Configuration
@EnableWebSecurity
public class JDWASecurityConfig extends WebSecurityConfigurerAdapter {
private final JDWAJwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/user/register", "/api/user/login").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
用户控制器
@RestController
@RequestMapping("/api/user")
public class JDWAUserController extends JDWABaseController {
private final JDWAUserService userService;
private final JDWAJwtUtil jwtUtil;
private final JDWAUserDetailsService userDetailsService;
/**
* 用户注册
*/
@PostMapping("/register")
public JDWAResult<Object> register(@RequestBody Map<String, Object> params) {
// 参数校验
String username = (String) params.get("username");
String password = (String) params.get("password");
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return JDWAResult.paramError("用户名和密码不能为空");
}
// 创建用户对象
JDWAUser user = new JDWAUser();
user.setUsername(username);
user.setPassword(password);
// 设置其他用户信息...
// 注册用户
boolean result = userService.register(user);
if (result) {
return JDWAResult.success("注册成功");
} else {
return JDWAResult.serverError("注册失败,请稍后重试");
}
}
/**
* 用户登录
*/
@PostMapping("/login")
public JDWAResult<Object> login(@RequestBody Map<String, Object> params) {
String username = (String) params.get("username");
String password = (String) params.get("password");
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return JDWAResult.paramError("用户名和密码不能为空");
}
// 登录验证
JDWAUser user = userService.login(username, password);
if (user != null) {
// 生成JWT Token
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String token = jwtUtil.generateToken(userDetails);
// 构建返回结果
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("userId", user.getId());
data.put("username", user.getUsername());
return JDWAResult.success("登录成功", data);
} else {
return JDWAResult.error(401, "用户名或密码错误");
}
}
/**
* 获取当前用户信息
*/
@GetMapping("/info")
public JDWAResult<Object> getUserInfo() {
Long userId = getCurrentUserId();
JDWAUser user = userService.getById(userId);
if (user == null) {
return JDWAResult.notFound("用户不存在");
}
// 构建返回数据(排除敏感信息)
Map<String, Object> data = new HashMap<>();
data.put("userId", user.getId());
data.put("username", user.getUsername());
data.put("nickname", user.getNickname());
// 其他用户信息...
return JDWAResult.success("获取成功", data);
}
}
认证流程详解
注册流程
- 客户端提交用户名、密码等注册信息
- 服务端验证数据有效性,检查用户名是否已存在
- 对密码进行加密处理(MD5+盐值)
- 创建用户记录并保存到数据库
- 返回注册结果
登录流程
- 客户端提交用户名和密码
- 服务端验证用户名和密码
- 验证成功后,生成JWT令牌:
- 创建包含用户名的声明(Claims)
- 设置令牌有效期(如24小时)
- 使用密钥签名令牌
- 将JWT令牌返回给客户端
- 客户端存储令牌用于后续请求
认证流程
- 客户端发送请求,在Authorization头中附带JWT令牌
- JWT过滤器拦截请求,从Authorization头中提取令牌
- 验证令牌的有效性:
- 检查签名是否有效
- 确认令牌未过期
- 提取用户名并加载用户详情
- 创建认证对象并设置到SecurityContext
- 请求被传递给下一个过滤器,最终到达目标控制器
- 控制器方法执行前,可通过
getCurrentUserId()
获取当前用户ID
安全性考虑
密码安全
项目中使用MD5+盐值的方式加密存储密码:
private String encryptPassword(String password) {
// 使用MD5加密 + 简单盐值
return DigestUtils.md5DigestAsHex((password + "JDWA_SALT").getBytes(StandardCharsets.UTF_8));
}
生产环境建议:
- 使用更强的加密算法如BCrypt或Argon2
- 为每个用户生成唯一的盐值
- 考虑密码强度校验
JWT安全性
确保JWT安全的关键措施:
- 使用足够强度的密钥:项目中使用
Keys.secretKeyFor(SignatureAlgorithm.HS512)
生成安全强度足够的密钥 - 设置合理的过期时间:默认24小时,可根据业务需求调整
- 验证令牌完整性:检查签名是否有效
- HTTPS传输:确保令牌通过HTTPS传输,防止中间人攻击
常见安全问题
令牌泄露:JWT令牌一旦泄露,攻击者可以冒充用户身份
- 解决方案:设置较短的过期时间,实现黑名单机制
跨站请求伪造(CSRF):项目中禁用了CSRF保护,因为JWT认证本身提供了一定防护
- 生产环境考虑:为敏感操作添加额外的验证机制
跨站脚本(XSS):攻击者可能通过XSS攻击窃取令牌
- 解决方案:客户端将令牌存储在HttpOnly Cookie中,避免JavaScript访问
最佳实践
令牌管理
- 实现令牌刷新机制,延长用户会话
- 维护令牌黑名单,实现令牌主动失效
异常处理
- 统一处理认证异常,返回友好错误信息
- 记录异常日志,便于安全审计
权限粒度
- 实现细粒度的资源访问控制
- 基于角色和权限组合的授权模型
故障排除
登录失败,报错"用户名或密码错误"
可能的原因:
- 用户名或密码确实错误
- 密码加密算法不匹配
- 用户账号被禁用(status=0)
排查步骤:
- 检查日志中的详细错误信息
- 验证用户状态是否正常
- 确认密码加密逻辑一致性
接口访问报401未授权错误
可能的原因:
- JWT令牌过期
- JWT令牌签名无效
- 未携带Authorization头部
排查步骤:
- 检查令牌是否已过期
- 验证令牌格式是否正确
- 确认请求头中是否包含
Authorization: Bearer {token}
无法从SecurityContext获取用户信息
可能的原因:
- JWT过滤器配置错误,没有正确设置SecurityContext
- getCurrentUserId()方法实现有误
排查步骤:
- 检查JWT过滤器是否正确注册
- 验证SecurityContext中的Authentication对象
- 调试getCurrentUserId()方法
优化与扩展
短期优化
- 引入密码强度校验:确保用户设置的密码符合安全要求
- 完善用户状态管理:增加邮箱验证、手机验证等功能
- 增强日志审计:记录关键操作日志,便于安全审计
长期规划
- 实现完整的RBAC权限模型:构建角色、权限、资源的关系数据模型
- 多因素认证(MFA):增加短信验证码、邮箱验证等辅助认证手段
- OAuth2.0集成:支持第三方登录,如微信、QQ等