JDWA 技术文档
首页
  • 数据库
  • 前端开发
  • 后端开发
  • 开发工具
  • 虚拟化技术
  • KVM显卡直通
  • FPGA仿真固件
  • 项目实战
  • 踩坑记录
  • 开发心得
  • 软件工具
  • 学习资料
  • 开发环境
更新日志
关于我
Gitee
GitHub
首页
  • 数据库
  • 前端开发
  • 后端开发
  • 开发工具
  • 虚拟化技术
  • KVM显卡直通
  • FPGA仿真固件
  • 项目实战
  • 踩坑记录
  • 开发心得
  • 软件工具
  • 学习资料
  • 开发环境
更新日志
关于我
Gitee
GitHub
  • Green-U低碳生活平台(后端)

    • JDWA Green-U 后端技术文档索引
    • JDWA Green-U 项目架构概述
    • JDWA Green-U 数据库设计文档
    • JDWA Green-U 用户认证模块技术文档
    • JDWA Green-U 用户活动模块技术文档
    • JDWA Green-U 碳减排统计模块技术文档
    • JDWA Green-U 积分系统模块技术文档
    • JDWA Green-U 成就系统模块技术文档
    • JDWA Green-U 奖品兑换模块技术文档
    • JDWA Green-U API接口文档
    • JDWA Green-U 系统安全机制与防护措施
    • JDWA Green-U 部署与运维指南

JDWA Green-U 用户认证模块

本文档详细介绍了JDWA Green-U项目中用户认证模块的设计思路、实现细节和使用方法。该模块实现了基于JWT的用户认证系统,包括用户注册、登录、权限验证等核心功能。

  • 模块概述
    • 核心功能
    • 技术选型
  • 数据模型设计
    • 用户表设计
    • 角色与权限表
  • 核心类与接口
    • 实体类
    • JWT工具类
    • JWT过滤器
    • 安全配置类
    • 用户控制器
  • 认证流程详解
    • 注册流程
    • 登录流程
    • 认证流程
  • 安全性考虑
    • 密码安全
    • JWT安全性
    • 常见安全问题
  • 最佳实践
  • 故障排除
  • 优化与扩展
    • 短期优化
    • 长期规划
  • 参考资源

模块概述

用户认证模块是整个系统的基础,负责用户身份验证与安全控制。本模块采用JWT(JSON Web Token)实现无状态认证,集成Spring Security框架提供全面的安全保障。

核心功能

  • 用户注册与账号管理
  • 用户登录与JWT令牌颁发
  • 接口访问权限控制
  • 用户角色与权限管理
  • 安全防护(密码加密、令牌验证)

技术选型

  • 认证框架: Spring Security
  • 令牌技术: JWT (JSON Web Token)
  • 加密算法: BCrypt + MD5
  • 安全防护: CSRF防护、XSS过滤

数据模型设计

用户表设计

用户表(jdwa_user)存储用户基本信息:

字段名类型说明
idBIGINT主键ID
usernameVARCHAR(50)用户名
passwordVARCHAR(100)密码(加密存储)
nicknameVARCHAR(50)昵称
avatarVARCHAR(255)头像URL
emailVARCHAR(100)邮箱
phoneVARCHAR(20)手机号
genderTINYINT性别(0-未知,1-男,2-女)
birthdayDATE出生日期
total_carbonDECIMAL(10,2)总碳减排量
carbon_pointsINT当前积分
total_pointsINT累计积分
statusTINYINT状态(0-禁用,1-启用)
create_timeDATETIME创建时间
update_timeDATETIME更新时间

角色与权限表

在简化版实现中,角色和权限直接硬编码在代码中,生产环境应建立专门的角色权限表。

核心类与接口

实体类

@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);
    }
}

认证流程详解

注册流程

  1. 客户端提交用户名、密码等注册信息
  2. 服务端验证数据有效性,检查用户名是否已存在
  3. 对密码进行加密处理(MD5+盐值)
  4. 创建用户记录并保存到数据库
  5. 返回注册结果

登录流程

  1. 客户端提交用户名和密码
  2. 服务端验证用户名和密码
  3. 验证成功后,生成JWT令牌:
    • 创建包含用户名的声明(Claims)
    • 设置令牌有效期(如24小时)
    • 使用密钥签名令牌
  4. 将JWT令牌返回给客户端
  5. 客户端存储令牌用于后续请求

认证流程

  1. 客户端发送请求,在Authorization头中附带JWT令牌
  2. JWT过滤器拦截请求,从Authorization头中提取令牌
  3. 验证令牌的有效性:
    • 检查签名是否有效
    • 确认令牌未过期
    • 提取用户名并加载用户详情
  4. 创建认证对象并设置到SecurityContext
  5. 请求被传递给下一个过滤器,最终到达目标控制器
  6. 控制器方法执行前,可通过getCurrentUserId()获取当前用户ID

安全性考虑

密码安全

项目中使用MD5+盐值的方式加密存储密码:

private String encryptPassword(String password) {
    // 使用MD5加密 + 简单盐值
    return DigestUtils.md5DigestAsHex((password + "JDWA_SALT").getBytes(StandardCharsets.UTF_8));
}

生产环境建议:

  • 使用更强的加密算法如BCrypt或Argon2
  • 为每个用户生成唯一的盐值
  • 考虑密码强度校验

JWT安全性

确保JWT安全的关键措施:

  1. 使用足够强度的密钥:项目中使用Keys.secretKeyFor(SignatureAlgorithm.HS512)生成安全强度足够的密钥
  2. 设置合理的过期时间:默认24小时,可根据业务需求调整
  3. 验证令牌完整性:检查签名是否有效
  4. HTTPS传输:确保令牌通过HTTPS传输,防止中间人攻击

常见安全问题

  1. 令牌泄露:JWT令牌一旦泄露,攻击者可以冒充用户身份

    • 解决方案:设置较短的过期时间,实现黑名单机制
  2. 跨站请求伪造(CSRF):项目中禁用了CSRF保护,因为JWT认证本身提供了一定防护

    • 生产环境考虑:为敏感操作添加额外的验证机制
  3. 跨站脚本(XSS):攻击者可能通过XSS攻击窃取令牌

    • 解决方案:客户端将令牌存储在HttpOnly Cookie中,避免JavaScript访问

最佳实践

  1. 令牌管理

    • 实现令牌刷新机制,延长用户会话
    • 维护令牌黑名单,实现令牌主动失效
  2. 异常处理

    • 统一处理认证异常,返回友好错误信息
    • 记录异常日志,便于安全审计
  3. 权限粒度

    • 实现细粒度的资源访问控制
    • 基于角色和权限组合的授权模型

故障排除

登录失败,报错"用户名或密码错误"

可能的原因:

  • 用户名或密码确实错误
  • 密码加密算法不匹配
  • 用户账号被禁用(status=0)

排查步骤:

  1. 检查日志中的详细错误信息
  2. 验证用户状态是否正常
  3. 确认密码加密逻辑一致性
接口访问报401未授权错误

可能的原因:

  • JWT令牌过期
  • JWT令牌签名无效
  • 未携带Authorization头部

排查步骤:

  1. 检查令牌是否已过期
  2. 验证令牌格式是否正确
  3. 确认请求头中是否包含Authorization: Bearer {token}
无法从SecurityContext获取用户信息

可能的原因:

  • JWT过滤器配置错误,没有正确设置SecurityContext
  • getCurrentUserId()方法实现有误

排查步骤:

  1. 检查JWT过滤器是否正确注册
  2. 验证SecurityContext中的Authentication对象
  3. 调试getCurrentUserId()方法

优化与扩展

短期优化

  1. 引入密码强度校验:确保用户设置的密码符合安全要求
  2. 完善用户状态管理:增加邮箱验证、手机验证等功能
  3. 增强日志审计:记录关键操作日志,便于安全审计

长期规划

  1. 实现完整的RBAC权限模型:构建角色、权限、资源的关系数据模型
  2. 多因素认证(MFA):增加短信验证码、邮箱验证等辅助认证手段
  3. OAuth2.0集成:支持第三方登录,如微信、QQ等

参考资源

  • Spring Security 官方文档
  • JSON Web Tokens 官方网站
  • OWASP认证安全指南
  • Spring Security JWT示例
Prev
JDWA Green-U 数据库设计文档
Next
JDWA Green-U 用户活动模块技术文档