Spring Cloud 实战攻坚:企业级用户服务开发(注册登录 + JWT 认证 + 权限控制)

引言

在微服务架构体系中,用户服务是所有业务服务的基础与核心,承担着用户身份认证、权限管控的关键职责。而注册登录、无状态认证、精细化权限控制,更是企业级微服务的必备能力 ------ 很多开发者在落地时,往往会遇到 JWT 配置不规范、权限粒度粗糙、认证流程有漏洞、密码存储不安全等问题,最终导致用户服务无法满足生产环境的要求。

本文将手把手带你开发一个企业级 Spring Cloud 用户服务,核心覆盖三大模块:用户注册与登录(含数据校验、密码加密)、JWT 无状态认证体系(含令牌生成、验证、刷新)、基于 Spring Security 的 RBAC 权限控制(含接口级权限拦截)。本文注重实战落地,所有代码示例均可直接复现,同时详解底层原理与避坑要点,兼顾深度与实用性,助力你快速搭建稳定、安全的微服务用户中心。

1. 前置认知:微服务中用户服务的核心价值与常见痛点

1.1 核心价值

用户服务作为微服务架构的认证中心与权限数据源,其核心价值体现在三个方面:

  1. 身份认证:验证用户身份合法性,为合法用户颁发访问凭证,拒绝非法访问;
  2. 权限管控:基于用户角色与权限,控制用户对各微服务接口的访问范围,实现精细化授权;
  3. 数据支撑:为其他业务服务提供用户基础数据(如用户信息、角色信息),支撑业务逻辑落地。

在微服务架构中,用户服务的核心地位可通过下图直观展示:

1.2 常见痛点

开发者在开发用户服务时,容易陷入以下几个痛点,导致服务无法满足企业级要求:

  1. 密码存储不安全:直接明文存储用户密码,或使用简单加密方式,存在泄露风险;
  2. JWT 配置不规范:密钥硬编码、令牌无过期策略、无刷新机制,导致认证体系脆弱;
  3. 权限控制粗糙:仅实现全局登录拦截,无法实现接口级、角色级的精细化权限控制;
  4. 无统一异常处理:认证、权限异常返回格式混乱,不利于前端对接;
  5. 状态化认证:使用 Session 保存用户状态,无法满足微服务集群的无状态要求。

2. 技术选型:构建企业级用户服务的技术栈清单

本文采用当前主流、生态完善的技术栈,确保用户服务的稳定性、可扩展性和安全性,具体选型如下:

技术领域 技术选型 选型理由
核心框架 Spring Boot 3.2 + Spring Cloud Alibaba 2023.0.1.0 主流微服务框架,生态完善,文档丰富,企业落地案例多
认证方案 JWT(JSON Web Token) 无状态认证,支持跨服务、跨域,适合微服务集群部署
安全框架 Spring Security 与 Spring 生态无缝整合,提供完整的认证、授权、拦截能力
数据持久层 MyBatis-Plus 3.5.5 简化 MyBatis 开发,提供 CRUD、分页、条件查询等便捷功能
数据库 MySQL 8.0 稳定、高效、开源,企业级应用的首选关系型数据库
参数校验 Hibernate Validator 8.0.1 支持注解式参数校验,简化请求参数合法性判断
工具类 Hutool 5.8.20 提供 JWT 操作、加密、日期处理等工具,简化重复开发
密码加密 BCrypt Spring Security 内置加密算法,支持加盐哈希,安全性高

3. 环境搭建:Spring Cloud 项目初始化与基础配置

3.1 项目初始化

创建 Spring Boot 项目(命名为 user-service),引入核心依赖,pom.xml 关键配置如下:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>user-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-service</name>
    
    <dependencies>
        <!-- Spring Web 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Security 安全框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <!-- MyBatis-Plus 数据持久层 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
        
        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Hibernate Validator 参数校验 -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>8.0.1.Final</version>
        </dependency>
        
        <!-- Hutool 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>
        
        <!-- Lombok 简化实体类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

3.2 基础配置

编写 application.yml 配置文件,配置数据库、MyBatis-Plus、JWT 基础参数(生产环境建议通过 Spring Cloud Config 集中配置):

bash 复制代码
server:
  port: 8080 # 服务端口

spring:
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/user_service?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
    username: root
    password: root123456
  # JWT 自定义配置(可根据业务调整)
  jwt:
    secret: spring-cloud-user-service-jwt-secret-2024 # JWT 签名密钥,生产环境需加密存储
    access-token-expire: 7200000 # 访问令牌过期时间:2小时(毫秒)
    refresh-token-expire: 604800000 # 刷新令牌过期时间:7天(毫秒)

# MyBatis-Plus 配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml # Mapper XML 文件路径
  type-aliases-package: com.example.userservice.entity # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(开发环境)

3.3 项目结构搭建

搭建清晰的项目目录结构,便于后续维护和扩展:

bash 复制代码
user-service/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── userservice/
│   │   │               ├── UserServiceApplication.java # 启动类
│   │   │               ├── entity/ # 实体类(User、Role等)
│   │   │               ├── mapper/ # Mapper 接口
│   │   │               ├── service/ # 业务层接口
│   │   │               │   └── impl/ # 业务层实现类
│   │   │               ├── controller/ # 控制层接口
│   │   │               ├── config/ # 配置类(Security、JWT等)
│   │   │               ├── util/ # 工具类(JWT工具、结果封装等)
│   │   │               └── exception/ # 异常处理(全局异常、认证异常等)
│   │   └── resources/
│   │       ├── mapper/ # Mapper XML 文件
│   │       ├── application.yml # 配置文件
│   │       └── db/ # 数据库脚本
│   └── test/ # 测试类
└── pom.xml # 依赖配置

4. 核心模块一:用户注册与登录(数据层 → 业务层 → 接口层)

用户注册与登录是用户服务的基础功能,核心要求是数据校验严格、密码存储安全、接口返回统一

4.1 数据模型设计

采用 RBAC 模型的核心实体,设计 user(用户表)和 role(角色表),并建立用户 - 角色多对多关联表 user_role(本文先实现核心功能,关联表后续扩展)。

4.1.1 数据库脚本

创建 user_service 数据库,执行以下 SQL 脚本:

sql 复制代码
-- 用户表
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名(唯一)',
  `password` varchar(100) NOT NULL COMMENT '加密后的密码',
  `nickname` varchar(50) DEFAULT NULL COMMENT '用户昵称',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号码',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `status` tinyint(1) DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称(如ADMIN、USER)',
  `role_desc` varchar(200) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_role_name` (`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
4.1.2 实体类编写

编写 User 实体类(使用 Lombok 简化代码):

java 复制代码
package com.example.userservice.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("user")
public class User {
    /**
     * 用户ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 用户名(唯一)
     */
    private String username;

    /**
     * 加密后的密码
     */
    private String password;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 状态:1-正常,0-禁用
     */
    private Integer status;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

4.2 数据层开发

基于 MyBatis-Plus 编写 UserMapper 接口,简化 CRUD 操作:

java 复制代码
package com.example.userservice.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.userservice.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
    /**
     * 根据用户名查询用户信息
     * @param username 用户名
     * @return 用户信息
     */
    User selectUserByUsername(String username);
}

编写 UserMapper.xml,实现根据用户名查询用户的 SQL:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.userservice.mapper.UserMapper">
    <select id="selectUserByUsername" resultType="com.example.userservice.entity.User">
        SELECT id, username, password, nickname, phone, email, status, create_time, update_time
        FROM user
        WHERE username = #{username}
    </select>
</mapper>

4.3 业务层开发

4.3.1 结果封装工具

创建统一结果封装类 Result,便于接口返回格式统一:

java 复制代码
package com.example.userservice.util;

import lombok.Data;

/**
 * 统一返回结果
 */
@Data
public class Result<T> {
    /**
     * 响应码:200-成功,500-失败
     */
    private Integer code;

    /**
     * 响应消息
     */
    private String msg;

    /**
     * 响应数据
     */
    private T data;

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }

    // 成功响应(有数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    // 失败响应
    public static <T> Result<T> fail(String msg) {
        return new Result<>(500, msg, null);
    }

    private Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}
4.3.2 业务层接口与实现

编写 UserService 接口:

java 复制代码
package com.example.userservice.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.userservice.entity.User;
import com.example.userservice.util.Result;

/**
 * 用户业务层接口
 */
public interface UserService extends IService<User> {
    /**
     * 用户注册
     * @param user 用户注册信息
     * @return 注册结果
     */
    Result<?> register(User user);

    /**
     * 用户登录
     * @param username 用户名
     * @param password 密码
     * @return 登录结果(含令牌)
     */
    Result<?> login(String username, String password);
}

编写 UserServiceImpl 实现类,核心处理注册数据校验、密码加密、登录验证:

java 复制代码
package com.example.userservice.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.userservice.entity.User;
import com.example.userservice.mapper.UserMapper;
import com.example.userservice.service.UserService;
import com.example.userservice.util.Result;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

/**
 * 用户业务层实现类
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Resource
    private UserMapper userMapper;

    /**
     * 密码加密器(Spring Security 内置 BCrypt)
     */
    @Resource
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public Result<?> register(User user) {
        // 1. 数据校验
        if (!StringUtils.hasText(user.getUsername()) || !StringUtils.hasText(user.getPassword())) {
            return Result.fail("用户名和密码不能为空");
        }

        // 2. 校验用户名是否已存在
        User existUser = userMapper.selectUserByUsername(user.getUsername());
        if (existUser != null) {
            return Result.fail("用户名已存在");
        }

        // 3. 密码加密(BCrypt 加盐哈希,不可逆)
        String encodePassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodePassword);

        // 4. 设置默认值
        if (user.getStatus() == null) {
            user.setStatus(1); // 默认正常状态
        }
        if (user.getNickname() == null) {
            user.setNickname(user.getUsername()); // 默认昵称等于用户名
        }

        // 5. 数据入库
        boolean saveResult = this.save(user);
        if (!saveResult) {
            return Result.fail("注册失败,请重试");
        }

        return Result.success("注册成功");
    }

    @Override
    public Result<?> login(String username, String password) {
        // 1. 数据校验
        if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
            return Result.fail("用户名和密码不能为空");
        }

        // 2. 查询用户信息
        User user = userMapper.selectUserByUsername(username);
        if (user == null) {
            return Result.fail("用户名不存在");
        }

        // 3. 校验密码(BCrypt 匹配加密后的密码)
        boolean passwordMatch = passwordEncoder.matches(password, user.getPassword());
        if (!passwordMatch) {
            return Result.fail("密码错误");
        }

        // 4. 校验用户状态
        if (user.getStatus() == 0) {
            return Result.fail("用户已被禁用,请联系管理员");
        }

        // 5. 后续将生成 JWT 令牌,此处先返回登录成功
        return Result.success("登录成功");
    }
}

4.4 接口层开发

编写 UserController,提供注册和登录接口,并使用 Hibernate Validator 进行参数校验:

java 复制代码
package com.example.userservice.controller;

import com.example.userservice.entity.User;
import com.example.userservice.service.UserService;
import com.example.userservice.util.Result;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 用户控制层接口
 */
@RestController
@RequestMapping("/api/user")
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 用户注册接口
     */
    @PostMapping("/register")
    public Result<?> register(@Valid User user) {
        return userService.register(user);
    }

    /**
     * 用户登录接口
     */
    @PostMapping("/login")
    public Result<?> login(@RequestParam String username, @RequestParam String password) {
        return userService.login(username, password);
    }
}

5. 核心模块二:JWT 认证体系搭建(生成 → 验证 → 刷新)

JWT 是实现微服务无状态认证的核心,本文基于 Hutool 工具类搭建完整的 JWT 认证体系,包括令牌生成、验证、刷新三大功能。

5.1 JWT 核心原理

JWT 由三部分组成,以 . 分隔:

  1. Header(头部):指定令牌类型和签名算法(如 HS256);
  2. Payload(载荷):存储用户核心信息(如用户 ID、用户名、角色),不可存储敏感信息(如密码);
  3. Signature(签名):使用头部指定的算法,结合 JWT 密钥对头部和载荷进行加密,防止令牌被篡改。

5.2 JWT 工具类实现

创建 JwtUtil 工具类,封装 JWT 令牌的生成、验证、解析功能:

java 复制代码
package com.example.userservice.util;

import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTCreator;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

/**
 * JWT 工具类
 */
@Component
public class JwtUtil {
    /**
     * JWT 签名密钥
     */
    @Value("${spring.jwt.secret}")
    private String jwtSecret;

    /**
     * 访问令牌过期时间
     */
    @Value("${spring.jwt.access-token-expire}")
    private long accessTokenExpire;

    /**
     * 刷新令牌过期时间
     */
    @Value("${spring.jwt.refresh-token-expire}")
    private long refreshTokenExpire;

    /**
     * 创建 JWTSigner
     */
    private JWTSigner getJWTSigner() {
        return JWTSignerUtil.hs256(jwtSecret.getBytes());
    }

    /**
     * 生成访问令牌
     * @param userId 用户ID
     * @param username 用户名
     * @return 访问令牌
     */
    public String generateAccessToken(Long userId, String username) {
        return generateToken(userId, username, accessTokenExpire);
    }

    /**
     * 生成刷新令牌
     * @param userId 用户ID
     * @param username 用户名
     * @return 刷新令牌
     */
    public String generateRefreshToken(Long userId, String username) {
        return generateToken(userId, username, refreshTokenExpire);
    }

    /**
     * 生成 JWT 令牌
     * @param userId 用户ID
     * @param username 用户名
     * @param expire 过期时间(毫秒)
     * @return JWT 令牌
     */
    private String generateToken(Long userId, String username, long expire) {
        // 当前时间
        Date now = new Date();
        // 过期时间
        Date expireDate = new Date(now.getTime() + expire);

        // 构建 JWT 载荷
        JWTCreator jwtCreator = JWT.create()
                .setHeader("typ", "JWT") // 令牌类型
                .setHeader("alg", "HS256") // 签名算法
                .setPayload("userId", userId) // 用户ID
                .setPayload("username", username) // 用户名
                .setIssuedAt(now) // 签发时间
                .setExpiresAt(expireDate); // 过期时间

        // 签名并返回令牌
        return jwtCreator.sign(getJWTSigner());
    }

    /**
     * 验证 JWT 令牌有效性
     * @param token JWT 令牌
     * @return 验证结果:true-有效,false-无效
     */
    public boolean validateToken(String token) {
        try {
            JWT jwt = JWT.parse(token);
            // 验证签名和过期时间
            JWTValidator.of(jwt).validateSignature(getJWTSigner()).validateDate();
            return true;
        } catch (Exception e) {
            // 令牌无效、过期、签名错误等均返回 false
            return false;
        }
    }

    /**
     * 解析 JWT 令牌,获取用户信息
     * @param token JWT 令牌
     * @return 用户信息(userId、username)
     */
    public Map<String, Object> parseToken(String token) {
        JWT jwt = JWT.parse(token);
        Map<String, Object> userInfo = new HashMap<>(2);
        userInfo.put("userId", jwt.getPayload("userId", Long.class));
        userInfo.put("username", jwt.getPayload("username", String.class));
        return userInfo;
    }
}

5.3 完善登录接口(返回 JWT 令牌)

修改 UserServiceImpllogin 方法,登录成功后生成访问令牌和刷新令牌并返回:

java 复制代码
// 注入 JwtUtil
@Resource
private JwtUtil jwtUtil;

@Override
public Result<?> login(String username, String password) {
    // 1. 数据校验(省略,同之前)
    if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
        return Result.fail("用户名和密码不能为空");
    }

    // 2. 查询用户信息(省略,同之前)
    User user = userMapper.selectUserByUsername(username);
    if (user == null) {
        return Result.fail("用户名不存在");
    }

    // 3. 校验密码(省略,同之前)
    boolean passwordMatch = passwordEncoder.matches(password, user.getPassword());
    if (!passwordMatch) {
        return Result.fail("密码错误");
    }

    // 4. 校验用户状态(省略,同之前)
    if (user.getStatus() == 0) {
        return Result.fail("用户已被禁用,请联系管理员");
    }

    // 5. 生成 JWT 访问令牌和刷新令牌
    String accessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername());
    String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());

    // 6. 封装返回结果
    Map<String, String> tokenMap = new HashMap<>(2);
    tokenMap.put("accessToken", accessToken);
    tokenMap.put("refreshToken", refreshToken);

    return Result.success(tokenMap);
}

5.4 JWT 认证过滤器实现

创建 JwtAuthenticationFilter,继承 OncePerRequestFilter,实现对请求的拦截和 JWT 令牌的验证:

java 复制代码
package com.example.userservice.config;

import com.example.userservice.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;

/**
 * JWT 认证过滤器
 */
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 1. 从请求头中获取 JWT 令牌
        String token = getTokenFromRequest(request);
        if (!StringUtils.hasText(token)) {
            // 无令牌,直接放行(后续 Security 会拦截未认证请求)
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 验证 JWT 令牌有效性
        if (!jwtUtil.validateToken(token)) {
            // 令牌无效,直接放行(后续 Security 会拦截未认证请求)
            filterChain.doFilter(request, response);
            return;
        }

        // 3. 解析令牌,获取用户信息
        Map<String, Object> userInfo = jwtUtil.parseToken(token);
        String username = (String) userInfo.get("username");

        // 4. 将用户信息存入 SecurityContext(表示用户已认证)
        if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 构建 UserDetails(此处简化,后续结合角色权限扩展)
            UserDetails userDetails = User.withUsername(username)
                    .password("") // 密码已加密,此处无需存储
                    .authorities(Collections.emptyList())
                    .build();

            // 构建认证令牌
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 存入 SecurityContext
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        // 5. 放行请求
        filterChain.doFilter(request, response);
    }

    /**
     * 从请求头中获取 JWT 令牌
     * @param request 请求对象
     * @return JWT 令牌
     */
    private String getTokenFromRequest(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7); // 去除 "Bearer " 前缀
        }
        return null;
    }
}

5.5 令牌刷新接口实现

UserController 中添加令牌刷新接口,通过有效的刷新令牌生成新的访问令牌:

java 复制代码
/**
 * 令牌刷新接口
 */
@PostMapping("/refresh")
public Result<?> refreshToken(@RequestParam String refreshToken) {
    // 1. 校验刷新令牌是否有效
    if (!jwtUtil.validateToken(refreshToken)) {
        return Result.fail("刷新令牌无效或已过期,请重新登录");
    }

    // 2. 解析刷新令牌,获取用户信息
    Map<String, Object> userInfo = jwtUtil.parseToken(refreshToken);
    Long userId = (Long) userInfo.get("userId");
    String username = (String) userInfo.get("username");

    // 3. 生成新的访问令牌
    String newAccessToken = jwtUtil.generateAccessToken(userId, username);

    // 4. 封装返回结果
    Map<String, String> tokenMap = new HashMap<>(1);
    tokenMap.put("accessToken", newAccessToken);

    return Result.success(tokenMap);
}

6. 核心模块三:基于 Spring Security 的 RBAC 权限控制

基于 Spring Security 整合 RBAC 模型,实现接口级、角色级的精细化权限控制,核心包括 Security 配置、自定义 UserDetailsService、权限注解使用三大步骤。

6.1 RBAC 权限模型原理

RBAC(基于角色的访问控制)模型的核心是用户 - 角色 - 权限的三层关联:

  1. 用户:系统的操作者,一个用户可拥有多个角色;
  2. 角色:权限的集合,一个角色可拥有多个权限;
  3. 权限:访问接口或资源的许可,一个权限可分配给多个角色。

RBAC 权限控制流程如下:

6.2 Spring Security 核心配置

创建 SecurityConfig,配置放行接口、添加 JWT 过滤器、关闭默认表单登录、开启方法级权限控制:

java 复制代码
package com.example.userservice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

/**
 * Spring Security 核心配置类
 */
@Configuration
@EnableWebSecurity // 启用 Web 安全
@EnableMethodSecurity // 启用方法级权限控制(支持 @PreAuthorize 等注解)
public class SecurityConfig {

    @Resource
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    /**
     * 密码加密器(BCrypt)
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 安全过滤链配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭 CSRF 防护(前后端分离、JWT 认证无需 CSRF)
                .csrf(csrf -> csrf.disable())
                // 关闭默认表单登录
                .formLogin(form -> form.disable())
                // 关闭默认 HTTP 基本认证
                .httpBasic(basic -> basic.disable())
                // 配置会话管理:无状态(不使用 Session)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置请求授权
                .authorizeHttpRequests(auth -> auth
                        // 放行注册、登录、刷新令牌接口(无需认证)
                        .requestMatchers("/api/user/register", "/api/user/login", "/api/user/refresh").permitAll()
                        // 其他所有请求均需认证
                        .anyRequest().authenticated()
                )
                // 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

6.3 自定义 UserDetailsService

创建 CustomUserDetailsService,实现 UserDetailsService 接口,查询用户信息及关联角色,为 Spring Security 提供认证和授权数据:

java 复制代码
package com.example.userservice.config;

import com.example.userservice.entity.User;
import com.example.userservice.mapper.UserMapper;
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 javax.annotation.Resource;
import java.util.Collections;

/**
 * 自定义 UserDetailsService(提供用户认证和授权数据)
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 查询用户信息
        User user = userMapper.selectUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 2. 校验用户状态(后续可扩展角色、权限查询)
        if (user.getStatus() == 0) {
            throw new UsernameNotFoundException("用户已被禁用");
        }

        // 3. 构建 UserDetails(此处简化,后续可添加角色和权限)
        // 实际项目中,需从 user_role、role_permission 表中查询用户的角色和权限
        return org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
                .password(user.getPassword())
                .authorities(Collections.singletonList("ROLE_USER")) // 默认赋予 USER 角色
                .build();
    }
}

6.4 接口级权限控制实现

UserController 中添加需要权限控制的接口,使用 @PreAuthorize 注解实现角色级权限控制:

java 复制代码
/**
 * 获取当前用户信息(仅 ROLE_USER 角色可访问)
 */
@GetMapping("/info")
@PreAuthorize("hasRole('USER')")
public Result<?> getUserInfo() {
    // 从 SecurityContext 中获取当前登录用户
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String username = authentication.getName();

    // 查询用户详细信息
    User user = userMapper.selectUserByUsername(username);
    if (user == null) {
        return Result.fail("用户信息不存在");
    }

    // 脱敏处理(不返回密码等敏感信息)
    user.setPassword(null);
    return Result.success(user);
}

/**
 * 测试管理员权限接口(仅 ROLE_ADMIN 角色可访问)
 */
@GetMapping("/admin/test")
@PreAuthorize("hasRole('ADMIN')")
public Result<?> adminTest() {
    return Result.success("管理员接口访问成功");
}

6.5 统一异常处理

创建 GlobalExceptionHandler,处理认证和权限异常,返回统一格式结果:

java 复制代码
package com.example.userservice.exception;

import com.example.userservice.util.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理认证异常(如未登录、令牌无效)
     */
    @ExceptionHandler(AuthenticationException.class)
    public Result<?> handleAuthenticationException(AuthenticationException e) {
        return Result.fail("认证失败:" + e.getMessage());
    }

    /**
     * 处理权限异常(如无权限访问接口)
     */
    @ExceptionHandler(AccessDeniedException.class)
    public Result<?> handleAccessDeniedException(AccessDeniedException e) {
        return Result.fail("权限不足,无法访问该接口");
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        return Result.fail("系统异常:" + e.getMessage());
    }
}

7. 实战测试:接口联调与权限验证

使用 Postman 或 ApiPost 进行接口联调,验证注册、登录、JWT 认证、权限控制是否生效,完整测试流程如下:

7.1 测试步骤 1:用户注册

  • 请求地址POST http://localhost:8080/api/user/register

  • 请求参数 (JSON):

    bash 复制代码
    {
        "username": "testuser",
        "password": "123456",
        "nickname": "测试用户",
        "phone": "13800138000",
        "email": "testuser@example.com"
    }
  • 预期结果 :返回 {"code":200,"msg":"操作成功","data":"注册成功"}

7.2 测试步骤 2:用户登录

  • 请求地址POST http://localhost:8080/api/user/login
  • 请求参数 (表单):username=testuser&password=123456
  • 预期结果 :返回包含 accessTokenrefreshToken 的结果,令牌有效。

7.3 测试步骤 3:获取用户信息(需认证)

  • 请求地址GET http://localhost:8080/api/user/info
  • 请求头Authorization: Bearer {accessToken}(替换为登录获取的访问令牌)
  • 预期结果:返回当前用户信息(密码脱敏),状态码 200。

7.4 测试步骤 4:测试管理员权限接口(无权限)

  • 请求地址GET http://localhost:8080/api/user/admin/test
  • 请求头Authorization: Bearer {accessToken}
  • 预期结果 :返回 {"code":500,"msg":"权限不足,无法访问该接口","data":null}

7.5 测试步骤 5:令牌刷新

  • 请求地址POST http://localhost:8080/api/user/refresh
  • 请求参数 (表单):refreshToken={refreshToken}(替换为登录获取的刷新令牌)
  • 预期结果 :返回新的 accessToken,可用于继续访问需要认证的接口。

8. 避坑指南:企业级落地的 5 个核心注意点

8.1 避坑 1:JWT 密钥硬编码与安全存储

问题 :将 JWT 密钥直接写在 application.yml 中,生产环境存在泄露风险。解决方案:生产环境通过 Spring Cloud Config 或密钥管理服务(如阿里云 KMS)集中存储并加密密钥,通过环境变量注入应用。

8.2 避坑 2:密码加密与校验不规范

问题 :自定义简单加密算法,或直接存储明文密码,安全性低。解决方案 :必须使用 Spring Security 内置的 BCryptPasswordEncoder,其支持自动加盐,哈希结果不可逆,安全性远高于自定义加密。

8.3 避坑 3:权限注解失效

问题 :使用 @PreAuthorize 注解后,无权限请求未被拦截,注解失效。解决方案 :忘记在 SecurityConfig 上添加 @EnableMethodSecurity 注解,开启方法级权限控制即可。

8.4 避坑 4:JWT 令牌存储敏感信息

问题 :在 JWT 载荷中存储密码、手机号等敏感信息,存在泄露风险。解决方案:JWT 载荷仅存储非敏感的核心用户信息(如用户 ID、用户名),敏感信息通过接口查询并脱敏返回。

8.5 避坑 5:无令牌过期重试机制

问题 :访问令牌过期后,直接返回异常,用户体验差。解决方案:前端实现令牌过期拦截,当收到 401 异常时,自动调用刷新令牌接口获取新令牌,重新发起请求,无需用户手动登录。

9. 总结与展望

9.1 核心总结

本文完整实现了一个企业级 Spring Cloud 用户服务,核心成果包括:

  1. 搭建了完整的用户注册与登录体系,实现了数据校验、密码加密、统一结果返回;
  2. 基于 JWT 构建了无状态认证体系,实现了令牌生成、验证、刷新三大核心功能;
  3. 基于 Spring Security 整合 RBAC 模型,实现了接口级、角色级的精细化权限控制;
  4. 提供了完整的实战测试流程和企业级落地避坑指南,确保服务可直接落地生产环境。

9.2 未来展望

后续可对该用户服务进行扩展,满足更复杂的企业级需求:

  1. 第三方登录集成:支持微信、支付宝、GitHub 等第三方登录,丰富登录方式;
  2. 短信 / 邮箱验证码:注册、登录、找回密码时添加验证码验证,提高安全性;
  3. 完整 RBAC 权限体系:扩展用户 - 角色 - 权限关联表,实现更精细化的权限管控;
  4. 微服务集成:将用户服务注册到 Nacos 注册中心,为其他微服务提供认证和权限数据;
  5. 用户信息脱敏与审计:实现用户信息脱敏返回,添加用户操作审计日志,满足合规要求。

点赞 + 收藏 + 关注,获取更多 Spring Cloud 微服务实战干货!有任何用户服务开发的问题,欢迎在评论区留言讨论~

写在最后

本文所有代码示例均可直接复现,已通过实际测试验证有效性。该用户服务作为微服务架构的核心基础,可直接扩展并集成到企业级微服务项目中,助力你快速搭建稳定、安全的微服务体系。

相关推荐
玹外之音5 小时前
Spring AI 实战:手把手教你构建支持多会话管理的智能聊天服务
java·spring
callJJ5 小时前
Spring Bean 生命周期详解——从出生到销毁,结合源码全程追踪
java·后端·spring·bean·八股文
怒放吧德德5 小时前
AsyncTool + SpringBoot:轻量级异步编排最佳实践
java·后端
毅炼6 小时前
Java 集合常见问题总结(1)
java·后端
知识即是力量ol6 小时前
口语八股——Spring 面试实战指南(一):核心概念篇、AOP 篇
java·spring·面试·aop·八股·核心概念篇
utmhikari6 小时前
【架构艺术】治理后端稳定性的一些实战经验
java·开发语言·后端·架构·系统架构·稳定性·后端开发
dfyx9996 小时前
Maven Spring框架依赖包
java·spring·maven
undefinedType6 小时前
Rails ActiveSupport::Cache 缓存存储详解
后端
茶杯梦轩7 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·服务器·后端