4、搭建微服务认证中心(microservice-auth)
作者 | Java匠艺
日期 | 2026年农历丙午年正月廿一
系列 | Spring Boot 3.2.5 + Spring Cloud + Spring Cloud Alibaba 微服务实战
🎯 本章目标
在微服务架构中,认证中心是安全防护的第一道防线。今天,我将手把手教你搭建一个功能完整、安全可靠的认证中心。无论你是架构师还是开发工程师,这篇文章都将为你提供从零到一的完整解决方案。
📁 认证中心架构设计
架构图┌─────────────────────────────────────────────────────┐│ 认证中心架构图 │├─────────────────────────────────────────────────────┤│ 客户端 → 网关 → 认证中心 → 业务服务 → 数据库/Redis │└─────────────────────────────────────────────────────┘│ │ │ │ ││ │ ├─────────┼───────────┤│ │ │ 用户管理 │ 角色管理 ││ │ │ 权限管理 │ Token管理 ││ │ │ 验证码 │ 登录记录 ││ │ └─────────┴───────────┘│ ││ └── 统一认证入口│└── 多端接入
核心功能模块
-
用户认证:登录、注册、注销
-
权限管理:角色、权限分配
-
Token管理:JWT生成、验证、刷新
-
安全防护:验证码、登录限制
-
会话管理:分布式会话
🚀 第一步:项目环境准备
1.1 创建认证中心模块
在父项目的pom.xml中已经定义好了microservice-auth模块,现在我们创建完整的项目结构:
# 创建认证中心目录结构mkdir -p microservice-auth/src/{main,test}/java/com/tech/authmkdir -p microservice-auth/src/main/resources/{mapper,sql}
1.2 数据库设计
创建auth_service.sql:
-- 创建数据库CREATE DATABASE IF NOT EXISTS `tech_auth` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE `tech_auth`;-- 用户表CREATE TABLE `sys_user` (`id` bigint NOT NULL COMMENT '主键ID',`username` varchar(50) NOT NULL COMMENT '用户名',`password` varchar(255) NOT NULL COMMENT '密码',`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',`phone` varchar(20) DEFAULT NULL COMMENT '手机号',`email` varchar(100) DEFAULT NULL COMMENT '邮箱',`avatar` varchar(500) DEFAULT NULL COMMENT '头像',`gender` tinyint DEFAULT '0' COMMENT '性别:0-未知 1-男 2-女',`birthday` datetime DEFAULT NULL COMMENT '生日',`last_login_ip` varchar(50) DEFAULT NULL COMMENT '最后登录IP',`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',`status` tinyint DEFAULT '1' COMMENT '状态:1-启用 0-禁用 2-锁定',`dept_id` bigint DEFAULT NULL COMMENT '部门ID',`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` tinyint DEFAULT '0' COMMENT '删除标志:0-未删除 1-已删除',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`),UNIQUE KEY `uk_username` (`username`),UNIQUE KEY `uk_phone` (`phone`),UNIQUE KEY `uk_email` (`email`),KEY `idx_dept_id` (`dept_id`),KEY `idx_status` (`status`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';-- 角色表CREATE TABLE `sys_role` (`id` bigint NOT NULL COMMENT '主键ID',`role_code` varchar(50) NOT NULL COMMENT '角色编码',`role_name` varchar(50) NOT NULL COMMENT '角色名称',`description` varchar(200) DEFAULT NULL COMMENT '角色描述',`sort` int DEFAULT '0' COMMENT '排序',`enabled` tinyint DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用',`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` tinyint DEFAULT '0' COMMENT '删除标志:0-未删除 1-已删除',PRIMARY KEY (`id`),UNIQUE KEY `uk_role_code` (`role_code`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';-- 权限表CREATE TABLE `sys_permission` (`id` bigint NOT NULL COMMENT '主键ID',`parent_id` bigint DEFAULT '0' COMMENT '父权限ID',`permission_code` varchar(100) NOT NULL COMMENT '权限编码',`permission_name` varchar(50) NOT NULL COMMENT '权限名称',`type` varchar(20) NOT NULL COMMENT '权限类型:MENU-菜单 BUTTON-按钮 API-接口',`path` varchar(200) DEFAULT NULL COMMENT '权限路径',`component` varchar(200) DEFAULT NULL COMMENT '组件路径',`icon` varchar(100) DEFAULT NULL COMMENT '图标',`sort` int DEFAULT '0' COMMENT '排序',`enabled` tinyint DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用',`hidden` tinyint DEFAULT '0' COMMENT '是否隐藏:0-显示 1-隐藏',`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`is_deleted` tinyint DEFAULT '0' COMMENT '删除标志:0-未删除 1-已删除',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`),UNIQUE KEY `uk_permission_code` (`permission_code`),KEY `idx_parent_id` (`parent_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';-- 用户角色关联表CREATE TABLE `sys_user_role` (`user_id` bigint NOT NULL COMMENT '用户ID',`role_id` bigint NOT NULL COMMENT '角色ID',PRIMARY KEY (`user_id`,`role_id`),KEY `idx_role_id` (`role_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';-- 角色权限关联表CREATE TABLE `sys_role_permission` (`role_id` bigint NOT NULL COMMENT '角色ID',`permission_id` bigint NOT NULL COMMENT '权限ID',PRIMARY KEY (`role_id`,`permission_id`),KEY `idx_permission_id` (`permission_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表';-- 初始化数据INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `real_name`, `phone`, `email`, `status`, `remark`) VALUES(1, 'admin', '$2a$10$E9J4Y5jM4k7jV8J5V6mZ8u5V7M6J5K4L3J2H1G0F9E8D7C6B5A4V3C2B1A0', '管理员', '系统管理员', '13800138000', 'admin@tech.com', 1, '超级管理员');INSERT INTO `sys_role` (`id`, `role_code`, `role_name`, `description`, `sort`) VALUES(1, 'ROLE_ADMIN', '管理员', '系统管理员,拥有所有权限', 1),(2, 'ROLE_USER', '普通用户', '普通用户,拥有基本权限', 2);INSERT INTO `sys_permission` (`id`, `parent_id`, `permission_code`, `permission_name`, `type`, `path`, `component`, `sort`) VALUES(1, 0, 'system:manage', '系统管理', 'MENU', '/system', 'Layout', 1),(2, 1, 'system:user:list', '用户管理', 'MENU', '/system/user', 'system/user/index', 1),(3, 2, 'system:user:query', '用户查询', 'API', NULL, NULL, 1),(4, 2, 'system:user:add', '用户新增', 'API', NULL, NULL, 2),(5, 2, 'system:user:edit', '用户编辑', 'API', NULL, NULL, 3),(6, 2, 'system:user:delete', '用户删除', 'API', NULL, NULL, 4);-- 分配用户角色INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);-- 分配角色权限INSERT INTO `sys_role_permission` (`role_id`, `permission_id`) VALUES(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6),(2, 3);
📦 第二步:核心代码实现
pom文件
<?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.0http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.tech</groupId><artifactId>microservice-parent</artifactId><version>1.0.0</version></parent><artifactId>microservice-auth</artifactId><packaging>jar</packaging><name>microservice-auth</name><description>微服务认证中心(精简版)</description><dependencies><!-- ========== Spring基础依赖 ========== --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- ========== Spring Security ========== --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- ========== 数据库相关 ========== --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!-- ========== Redis缓存 ========== --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- ========== 公共模块 ========== --><dependency><groupId>com.tech</groupId><artifactId>microservice-common</artifactId><version>1.0.0</version></dependency><!-- ========== JWT认证 ========== --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><scope>runtime</scope></dependency><!-- ========== 验证码 ========== --><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency><!-- ========== 工具类 ========== --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
bootstrap.yml
spring:application:name: microservice-authprofiles:active: devcloud:nacos:config:server-addr: localhost:8848file-extension: yamldiscovery:server-addr: localhost:8848
application.yml
# 服务配置server:port: 8081servlet:context-path: /auth-service# 数据库配置spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/tech_auth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456hikari:maximum-pool-size: 20# Redis配置redis:host: localhostport: 6379database: 0# 安全配置security:user:name: adminpassword: admin123# JWT配置jwt:secret: tech2026authsecretkey1234567890expiration: 7200refresh-expiration: 2592000header: Authorizationprefix: Bearer# 验证码配置captcha:width: 130height: 48length: 4expire-seconds: 300# MyBatis Plus配置mybatis-plus:mapper-locations: classpath*:/mapper/**/*.xmltype-aliases-package: com.tech.auth.domain.entityconfiguration:map-underscore-to-camel-case: trueglobal-config:db-config:id-type: ASSIGN_ID
AuthApplication.java
package com.tech.auth;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** 认证中心启动类*/@SpringBootApplication@EnableDiscoveryClientpublic class AuthApplication {public static void main(String[] args) {SpringApplication.run(AuthApplication.class, args);}}
2.1 实体类实现
User.java
package com.tech.auth.domain.entity;import com.baomidou.mybatisplus.annotation.*;import com.tech.auth.enums.GenderEnum;import com.tech.auth.enums.UserStatusEnum;import com.tech.common.domain.entity.BaseEntity;import lombok.Data;import lombok.EqualsAndHashCode;import java.time.LocalDateTime;@Data@EqualsAndHashCode(callSuper = true)@TableName("sys_user")public class User extends BaseEntity {@TableField("username")private String username;@TableField("password")private String password;@TableField("nickname")private String nickname;@TableField("real_name")private String realName;@TableField("phone")private String phone;@TableField("email")private String email;@TableField("avatar")private String avatar;@TableField("gender")private GenderEnum gender = GenderEnum.UNKNOWN;@TableField("birthday")private LocalDateTime birthday;@TableField("last_login_ip")private String lastLoginIp;@TableField("last_login_time")private LocalDateTime lastLoginTime;@TableField("status")private UserStatusEnum status = UserStatusEnum.ENABLED;@TableField("dept_id")private Long deptId;@TableField("remark")private String remark;}
2.2 安全配置
SecurityConfig.java(核心安全配置)
package com.tech.auth.config;import com.tech.auth.security.filter.JwtAuthenticationFilter;import com.tech.auth.security.handler.*;import lombok.RequiredArgsConstructor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;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.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.CorsConfigurationSource;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import java.util.Arrays;@Configuration@EnableWebSecurity@EnableMethodSecurity(prePostEnabled = true)@RequiredArgsConstructorpublic class SecurityConfig {private final UserDetailsService userDetailsService;private final JwtAuthenticationFilter jwtAuthenticationFilter;private final AccessDeniedHandlerImpl accessDeniedHandler;private final AuthenticationEntryPointImpl authenticationEntryPoint;/*** 密码加密器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证提供者*/@Beanpublic DaoAuthenticationProvider authenticationProvider() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userDetailsService);provider.setPasswordEncoder(passwordEncoder());provider.setHideUserNotFoundExceptions(false);return provider;}/*** 认证管理器*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}/*** 安全过滤器链*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 禁用CSRF和Session.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 认证配置.authenticationProvider(authenticationProvider())// 异常处理.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler).and()// 授权配置.authorizeHttpRequests(authorize -> authorize// 公开接口.antMatchers("/auth/login","/auth/register","/auth/captcha/**","/auth/refresh-token",// Swagger文档"/doc/**","/webjars/**","/v3/api-docs/**","/swagger-ui/**","/swagger-resources/**",// 健康检查"/actuator/**").permitAll()// 其他所有接口需要认证.anyRequest().authenticated())// 添加JWT过滤器.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)// 跨域配置.cors().configurationSource(corsConfigurationSource());return http.build();}/*** 跨域配置*/@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOriginPatterns(Arrays.asList("*"));configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));configuration.setAllowedHeaders(Arrays.asList("*"));configuration.setAllowCredentials(true);configuration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);return source;}}
2.3 JWT工具类
JwtTokenProvider.java
package com.tech.auth.security.component;import com.tech.auth.security.constant.SecurityConstant;import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import io.jsonwebtoken.security.Keys;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.stereotype.Component;import javax.crypto.SecretKey;import java.nio.charset.StandardCharsets;import java.util.Date;import java.util.HashMap;import java.util.Map;import java.util.function.Function;import java.util.stream.Collectors;@Slf4j@Componentpublic class JwtTokenProvider {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;@Value("${jwt.refresh-expiration}")private Long refreshExpiration;/*** 生成Access Token*/public String generateAccessToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();claims.put(SecurityConstant.CLAIM_KEY_USERNAME, userDetails.getUsername());claims.put(SecurityConstant.CLAIM_KEY_AUTHORITIES,userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));claims.put(SecurityConstant.CLAIM_KEY_CREATED, new Date());return generateToken(claims, expiration);}/*** 生成Access Token*/public String generateAccessToken(String username, Long userId, String... authorities) {Map<String, Object> claims = new HashMap<>();claims.put(SecurityConstant.CLAIM_KEY_USER_ID, userId);claims.put(SecurityConstant.CLAIM_KEY_USERNAME, username);if (authorities != null && authorities.length > 0) {claims.put(SecurityConstant.CLAIM_KEY_AUTHORITIES, authorities);}claims.put(SecurityConstant.CLAIM_KEY_CREATED, new Date());return generateToken(claims, expiration);}/*** 生成Refresh Token*/public String generateRefreshToken(String username) {Map<String, Object> claims = new HashMap<>();claims.put(SecurityConstant.CLAIM_KEY_USERNAME, username);claims.put(SecurityConstant.CLAIM_KEY_CREATED, new Date());return generateToken(claims, refreshExpiration);}/*** 生成Token*/private String generateToken(Map<String, Object> claims, Long expiration) {return Jwts.builder().setClaims(claims).setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)).signWith(getSecretKey(), SignatureAlgorithm.HS512).compact();}/*** 验证Token*/public boolean validateToken(String token) {try {Claims claims = getClaimsFromToken(token);Date expiration = claims.getExpiration();return !expiration.before(new Date());} catch (Exception e) {log.error("Token验证失败: {}", e.getMessage());return false;}}/*** 从Token中获取用户名*/public String getUsernameFromToken(String token) {return getClaimFromToken(token, claims -> claims.get(SecurityConstant.CLAIM_KEY_USERNAME, String.class));}/*** 从Token中获取用户ID*/public Long getUserIdFromToken(String token) {return getClaimFromToken(token, claims -> claims.get(SecurityConstant.CLAIM_KEY_USER_ID, Long.class));}/*** 从Token中获取Claims*/public Claims getClaimsFromToken(String token) {return Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token).getBody();}/*** 获取Token剩余有效时间(秒)*/public Long getRemainingSeconds(String token) {try {Claims claims = getClaimsFromToken(token);Date expiration = claims.getExpiration();long remainingMillis = expiration.getTime() - System.currentTimeMillis();return remainingMillis > 0 ? remainingMillis / 1000 : 0;} catch (Exception e) {return 0L;}}/*** 获取密钥*/private SecretKey getSecretKey() {return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));}private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {Claims claims = getClaimsFromToken(token);return claimsResolver.apply(claims);}}
2.4 认证服务实现
AuthServiceImpl.java(核心业务逻辑)
package com.tech.auth.service.impl;import cn.hutool.core.util.IdUtil;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.tech.auth.domain.dto.LoginDTO;import com.tech.auth.domain.dto.RegisterDTO;import com.tech.auth.domain.entity.User;import com.tech.auth.domain.vo.LoginVO;import com.tech.auth.domain.vo.TokenVO;import com.tech.auth.enums.UserStatusEnum;import com.tech.auth.mapper.UserMapper;import com.tech.auth.security.component.JwtTokenProvider;import com.tech.auth.security.constant.SecurityConstant;import com.tech.auth.service.AuthService;import com.tech.auth.service.CaptchaService;import com.tech.auth.service.PermissionService;import com.tech.auth.util.PasswordEncoder;import com.tech.common.constant.CacheConstant;import com.tech.common.exception.BusinessException;import com.tech.common.utils.IpUtil;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;import java.time.LocalDateTime;import java.util.List;import java.util.concurrent.TimeUnit;@Slf4j@Service@RequiredArgsConstructorpublic class AuthServiceImpl implements AuthService {private final UserMapper userMapper;private final JwtTokenProvider jwtTokenProvider;private final PasswordEncoder passwordEncoder;private final CaptchaService captchaService;private final PermissionService permissionService;private final RedisTemplate<String, Object> redisTemplate;private final HttpServletRequest request;@Override@Transactional(rollbackFor = Exception.class)public LoginVO login(LoginDTO loginDTO) {// 1. 验证验证码if (!captchaService.verifyCaptcha(loginDTO.getCaptchaKey(), loginDTO.getCaptcha())) {throw new BusinessException("验证码错误或已过期");}// 2. 检查登录尝试次数String loginAttemptKey = SecurityConstant.LOGIN_ATTEMPT_KEY + loginDTO.getUsername();Integer attemptCount = (Integer) redisTemplate.opsForValue().get(loginAttemptKey);if (attemptCount != null && attemptCount >= SecurityConstant.MAX_LOGIN_ATTEMPTS) {throw new BusinessException("登录失败次数过多,请稍后再试");}// 3. 查询用户LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUsername, loginDTO.getUsername()).ne(User::getStatus, UserStatusEnum.DELETED.getCode());User user = userMapper.selectOne(wrapper);if (user == null) {incrementLoginAttempt(loginAttemptKey);throw new BusinessException("用户名或密码错误");}// 4. 检查用户状态if (UserStatusEnum.DISABLED.equals(user.getStatus())) {throw new BusinessException("账户已被禁用");}if (UserStatusEnum.LOCKED.equals(user.getStatus())) {throw new BusinessException("账户已被锁定");}// 5. 验证密码if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {incrementLoginAttempt(loginAttemptKey);throw new BusinessException("用户名或密码错误");}// 6. 密码正确,清除登录尝试计数redisTemplate.delete(loginAttemptKey);// 7. 更新登录信息String ip = IpUtil.getClientIp(request);user.setLastLoginIp(ip);user.setLastLoginTime(LocalDateTime.now());userMapper.updateById(user);// 8. 生成TokenString accessToken = jwtTokenProvider.generateAccessToken(user.getUsername(), user.getId());String refreshToken = jwtTokenProvider.generateRefreshToken(user.getUsername());// 9. 获取用户权限List<String> permissions = permissionService.getUserPermissions(user.getId());List<String> roles = permissionService.getUserRoles(user.getId());// 10. 将Token加入缓存String tokenKey = CacheConstant.USER_TOKEN_CACHE + user.getId();redisTemplate.opsForValue().set(tokenKey, accessToken, SecurityConstant.TOKEN_EXPIRE, TimeUnit.SECONDS);// 11. 返回登录结果LoginVO loginVO = new LoginVO();loginVO.setUserId(user.getId());loginVO.setUsername(user.getUsername());loginVO.setNickname(user.getNickname());loginVO.setAccessToken(accessToken);loginVO.setRefreshToken(refreshToken);loginVO.setExpiresIn(SecurityConstant.TOKEN_EXPIRE);loginVO.setRoles(roles);loginVO.setPermissions(permissions);loginVO.setLastLoginIp(ip);loginVO.setLastLoginTime(LocalDateTime.now());log.info("用户 {} 登录成功", user.getUsername());return loginVO;}@Override@Transactional(rollbackFor = Exception.class)public Long register(RegisterDTO registerDTO) {// 1. 验证验证码if (!captchaService.verifyCaptcha(registerDTO.getCaptchaKey(), registerDTO.getCaptcha())) {throw new BusinessException("验证码错误或已过期");}// 2. 验证两次密码是否一致if (!registerDTO.getPassword().equals(registerDTO.getConfirmPassword())) {throw new BusinessException("两次输入的密码不一致");}// 3. 检查用户名是否已存在LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUsername, registerDTO.getUsername());if (userMapper.selectCount(wrapper) > 0) {throw new BusinessException("用户名已存在");}// 4. 检查手机号是否已存在if (StringUtils.hasText(registerDTO.getPhone())) {wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getPhone, registerDTO.getPhone());if (userMapper.selectCount(wrapper) > 0) {throw new BusinessException("手机号已存在");}}// 5. 检查邮箱是否已存在if (StringUtils.hasText(registerDTO.getEmail())) {wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getEmail, registerDTO.getEmail());if (userMapper.selectCount(wrapper) > 0) {throw new BusinessException("邮箱已存在");}}// 6. 创建用户User user = new User();user.setUsername(registerDTO.getUsername());user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));user.setNickname(registerDTO.getNickname());user.setPhone(registerDTO.getPhone());user.setEmail(registerDTO.getEmail());user.setStatus(UserStatusEnum.ENABLED);userMapper.insert(user);log.info("用户 {} 注册成功,用户ID: {}", registerDTO.getUsername(), user.getId());return user.getId();}@Overridepublic void logout(String token) {if (!StringUtils.hasText(token)) {return;}try {// 从Token中获取用户IDLong userId = jwtTokenProvider.getUserIdFromToken(token);if (userId != null) {// 将Token加入黑名单String tokenKey = CacheConstant.USER_TOKEN_CACHE + userId;redisTemplate.delete(tokenKey);log.info("用户 {} 已注销", userId);}} catch (Exception e) {log.error("用户注销失败", e);}}@Overridepublic TokenVO refreshToken(String refreshToken) {// 验证Refresh Tokenif (!jwtTokenProvider.validateToken(refreshToken)) {throw new BusinessException("Refresh Token无效或已过期");}// 从Refresh Token中获取用户名String username = jwtTokenProvider.getUsernameFromToken(refreshToken);if (!StringUtils.hasText(username)) {throw new BusinessException("Refresh Token解析失败");}// 查询用户LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUsername, username).eq(User::getStatus, UserStatusEnum.ENABLED.getCode());User user = userMapper.selectOne(wrapper);if (user == null) {throw new BusinessException("用户不存在或已被禁用");}// 生成新的Access TokenString newAccessToken = jwtTokenProvider.generateAccessToken(user.getUsername(), user.getId());// 更新Token缓存String tokenKey = CacheConstant.USER_TOKEN_CACHE + user.getId();redisTemplate.opsForValue().set(tokenKey, newAccessToken, SecurityConstant.TOKEN_EXPIRE, TimeUnit.SECONDS);// 返回新的TokenTokenVO tokenVO = new TokenVO();tokenVO.setAccessToken(newAccessToken);tokenVO.setRefreshToken(refreshToken);tokenVO.setExpiresIn(SecurityConstant.TOKEN_EXPIRE);log.info("用户 {} 刷新Token成功", username);return tokenVO;}/*** 增加登录尝试次数*/private void incrementLoginAttempt(String key) {Integer count = (Integer) redisTemplate.opsForValue().get(key);if (count == null) {count = 1;} else {count++;}redisTemplate.opsForValue().set(key, count, SecurityConstant.LOGIN_ATTEMPT_EXPIRE, TimeUnit.SECONDS);// 如果超过最大尝试次数,锁定账户if (count >= SecurityConstant.MAX_LOGIN_ATTEMPTS) {String lockKey = SecurityConstant.LOGIN_LOCK_KEY + key.substring(key.lastIndexOf(":") + 1);redisTemplate.opsForValue().set(lockKey, "locked", SecurityConstant.LOGIN_LOCK_EXPIRE, TimeUnit.SECONDS);log.warn("用户登录失败次数过多,账户已被锁定: {}", key);}}}
2.5 验证码服务
CaptchaServiceImpl.java
package com.tech.auth.service.impl;import com.github.whvcse.easy.captcha.support.CaptchaType;import com.tech.auth.service.CaptchaService;import com.tech.common.constant.CacheConstant;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Slf4j@Service@RequiredArgsConstructorpublic class CaptchaServiceImpl implements CaptchaService {private final RedisTemplate<String, Object> redisTemplate;@Value("${captcha.type:arithmetic}")private String captchaType;@Value("${captcha.width:130}")private int width;@Value("${captcha.height:48}")private int height;@Value("${captcha.length:2}")private int length;@Value("${captcha.expire-seconds:300}")private long expireSeconds;@Overridepublic String generateCaptcha(String key) {// 生成验证码String captcha = generateRandomCode();// 存储到RedisString cacheKey = CacheConstant.CAPTCHA_CACHE + key;redisTemplate.opsForValue().set(cacheKey, captcha, expireSeconds, TimeUnit.SECONDS);log.debug("生成验证码: key={}, code={}", key, captcha);return captcha;}@Overridepublic boolean verifyCaptcha(String key, String code) {if (key == null || code == null) {return false;}// 从Redis获取验证码String cacheKey = CacheConstant.CAPTCHA_CACHE + key;String storedCode = (String) redisTemplate.opsForValue().get(cacheKey);if (storedCode == null) {log.warn("验证码已过期: key={}", key);return false;}// 验证验证码(不区分大小写)boolean success = storedCode.equalsIgnoreCase(code);// 验证成功后删除验证码if (success) {redisTemplate.delete(cacheKey);log.debug("验证码验证成功: key={}", key);} else {log.warn("验证码验证失败: key={}, input={}, stored={}", key, code, storedCode);}return success;}@Overridepublic void clearCaptcha(String key) {String cacheKey = CacheConstant.CAPTCHA_CACHE + key;redisTemplate.delete(cacheKey);}/*** 生成随机验证码*/private String generateRandomCode() {switch (captchaType.toLowerCase()) {case "arithmetic":return generateArithmeticCode();case "chinese":return generateChineseCode();default:return generateRandomStringCode();}}/*** 生成算术验证码*/private String generateArithmeticCode() {int a = (int) (Math.random() * 10) + 1;int b = (int) (Math.random() * 10) + 1;int result = a + b;return String.valueOf(result);}/*** 生成中文验证码*/private String generateChineseCode() {String[] chineseChars = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};int num = (int) (Math.random() * 100);if (num < 10) {return chineseChars[num];} else if (num < 20) {return "十" + (num % 10 == 0 ? "" : chineseChars[num % 10]);} else {return chineseChars[num / 10] + "十" + (num % 10 == 0 ? "" : chineseChars[num % 10]);}}/*** 生成随机字符串验证码*/private String generateRandomStringCode() {String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";StringBuilder code = new StringBuilder();for (int i = 0; i < length; i++) {code.append(chars.charAt((int) (Math.random() * chars.length())));}return code.toString();}}
2.6 控制器
AuthController.java
package com.tech.auth.controller;import com.tech.auth.domain.dto.LoginDTO;import com.tech.auth.domain.dto.RegisterDTO;import com.tech.auth.domain.vo.LoginVO;import com.tech.auth.domain.vo.TokenVO;import com.tech.auth.service.AuthService;import com.tech.common.domain.vo.ApiResult;import io.swagger.v3.oas.annotations.Operation;import io.swagger.v3.oas.annotations.tags.Tag;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.validation.Valid;@Slf4j@Tag(name = "认证管理")@RestController@RequestMapping("/auth")@RequiredArgsConstructorpublic class AuthController {private final AuthService authService;private final HttpServletRequest request;@Operation(summary = "用户登录")@PostMapping("/login")public ApiResult<LoginVO> login(@Valid @RequestBody LoginDTO loginDTO) {LoginVO loginVO = authService.login(loginDTO);return ApiResult.success("登录成功", loginVO);}@Operation(summary = "用户注册")@PostMapping("/register")public ApiResult<Long> register(@Valid @RequestBody RegisterDTO registerDTO) {Long userId = authService.register(registerDTO);return ApiResult.success("注册成功", userId);}@Operation(summary = "用户注销")@PostMapping("/logout")public ApiResult<Void> logout() {String token = getTokenFromRequest();authService.logout(token);return ApiResult.success("注销成功");}@Operation(summary = "刷新Token")@PostMapping("/refresh-token")public ApiResult<TokenVO> refreshToken(@RequestParam String refreshToken) {TokenVO tokenVO = authService.refreshToken(refreshToken);return ApiResult.success("Token刷新成功", tokenVO);}@Operation(summary = "获取验证码")@GetMapping("/captcha")public ApiResult<String> getCaptcha(@RequestParam String key) {String captcha = authService.getCaptcha(key);return ApiResult.success(captcha);}/*** 从请求中获取Token*/private String getTokenFromRequest() {String bearerToken = request.getHeader("Authorization");if (bearerToken != null && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}}
📊 第三步:测试认证中心
3.1 启动认证中心
# 进入认证中心模块cd microservice-auth
# 编译打包mvn clean package
# 运行java -jar target/microservice-auth-1.0.0.jar
3.2 API测试
测试1:获取验证码
curl "http://localhost:8081/auth-service/auth/captcha?key=login:123456"
测试2:用户注册
curl -X POST "http://localhost:8081/auth-service/auth/register" \-H "Content-Type: application/json" \-d '{"username": "testuser","password": "Test@123456","confirmPassword": "Test@123456","nickname": "测试用户","phone": "13800138001","email": "test@example.com","captcha": "123456","captchaKey": "captcha:register:abc123"}'
测试3:用户登录
curl -X POST "http://localhost:8081/auth-service/auth/login" \-H "Content-Type: application/json" \-d '{"username": "testuser","password": "Test@123456","captcha": "123456","captchaKey": "captcha:login:abc123"}'
响应示例:
{"code": 200,"message": "登录成功","data": {"userId": 1,"username": "testuser","nickname": "测试用户","accessToken": "eyJhbGciOiJIUzUxMiJ9...","refreshToken": "eyJhbGciOiJIUzUxMiJ9...","tokenType": "Bearer","expiresIn": 7200,"roles": ["ROLE_USER"],"permissions": ["system:user:query"],"lastLoginIp": "127.0.0.1","lastLoginTime": "2026-01-01 12:00:00"},"timestamp": 1735718400000}
测试4:访问受保护接口
# 使用Token访问curl -X GET "http://localhost:8081/auth-service/users/1" \-H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9..."
测试5:刷新Token
curl -X POST "http://localhost:8081/auth-service/auth/refresh-token" \-H "Content-Type: application/x-www-form-urlencoded" \-d "refreshToken=eyJhbGciOiJIUzUxMiJ9..."
🔐 第四步:安全增强配置
4.1 密码策略配置
PasswordPolicyConfig.java
package com.tech.auth.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;@Configurationpublic class PasswordPolicyConfig {/*** 密码加密器配置*/@Beanpublic PasswordEncoder passwordEncoder() {// 强度设置为10,兼顾安全性和性能return new BCryptPasswordEncoder(10);}/*** 密码策略验证*/public static boolean validatePassword(String password) {if (password == null || password.length() < 8) {return false;}// 检查是否包含数字if (!password.matches(".*\\d.*")) {return false;}// 检查是否包含小写字母if (!password.matches(".*[a-z].*")) {return false;}// 检查是否包含大写字母if (!password.matches(".*[A-Z].*")) {return false;}// 检查是否包含特殊字符if (!password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*")) {return false;}return true;}}
4.2 登录安全配置
LoginSecurityConfig.java
package com.tech.auth.config;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;@Configurationpublic class LoginSecurityConfig {@Value("${security.login.max-attempts:5}")private int maxLoginAttempts;@Value("${security.login.lock-duration:1800}")private int lockDuration;@Value("${security.session.timeout:1800}")private int sessionTimeout;/*** 获取最大登录尝试次数*/public int getMaxLoginAttempts() {return maxLoginAttempts;}/*** 获取账户锁定时间(秒)*/public int getLockDuration() {return lockDuration;}/*** 获取会话超时时间(秒)*/public int getSessionTimeout() {return sessionTimeout;}}
📈 第五步:监控和日志
5.1 登录日志记录
LoginLogAspect.java
package com.tech.auth.aspect;import com.tech.auth.domain.entity.User;import com.tech.common.utils.IpUtil;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;import java.time.LocalDateTime;@Slf4j@Aspect@Componentpublic class LoginLogAspect {/*** 登录成功日志*/@AfterReturning(pointcut = "execution(* com.tech.auth.service.impl.AuthServiceImpl.login(..))",returning = "result")public void afterLoginSuccess(JoinPoint joinPoint, Object result) {try {Object[] args = joinPoint.getArgs();if (args.length > 0 && args[0] instanceof com.tech.auth.domain.dto.LoginDTO) {com.tech.auth.domain.dto.LoginDTO loginDTO = (com.tech.auth.domain.dto.LoginDTO) args[0];String username = loginDTO.getUsername();HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = IpUtil.getClientIp(request);String userAgent = request.getHeader("User-Agent");// 记录登录成功日志log.info("登录成功 - 用户: {}, IP: {}, 时间: {}, User-Agent: {}",username, ip, LocalDateTime.now(), userAgent);// 这里可以保存到数据库// loginLogService.saveSuccessLog(username, ip, userAgent);}} catch (Exception e) {log.error("记录登录成功日志失败", e);}}/*** 登录失败日志*/@AfterThrowing(pointcut = "execution(* com.tech.auth.service.impl.AuthServiceImpl.login(..))",throwing = "exception")public void afterLoginFailure(JoinPoint joinPoint, Exception exception) {try {Object[] args = joinPoint.getArgs();if (args.length > 0 && args[0] instanceof com.tech.auth.domain.dto.LoginDTO) {com.tech.auth.domain.dto.LoginDTO loginDTO = (com.tech.auth.domain.dto.LoginDTO) args[0];String username = loginDTO.getUsername();HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = IpUtil.getClientIp(request);String userAgent = request.getHeader("User-Agent");String errorMsg = exception.getMessage();// 记录登录失败日志log.warn("登录失败 - 用户: {}, IP: {}, 时间: {}, 原因: {}, User-Agent: {}",username, ip, LocalDateTime.now(), errorMsg, userAgent);// 这里可以保存到数据库// loginLogService.saveFailureLog(username, ip, errorMsg, userAgent);}} catch (Exception e) {log.error("记录登录失败日志失败", e);}}}
🎯 第六步:集成测试
6.1 单元测试
AuthServiceTest.java
package com.tech.auth.service;import com.tech.auth.AuthApplication;import com.tech.auth.domain.dto.LoginDTO;import com.tech.auth.domain.dto.RegisterDTO;import com.tech.auth.domain.vo.LoginVO;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.annotation.Rollback;import org.springframework.transaction.annotation.Transactional;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest(classes = AuthApplication.class)@Transactional@Rollbackclass AuthServiceTest {@Autowiredprivate AuthService authService;@Testvoid testRegisterAndLogin() {// 1. 注册用户RegisterDTO registerDTO = new RegisterDTO();registerDTO.setUsername("testuser");registerDTO.setPassword("Test@123456");registerDTO.setConfirmPassword("Test@123456");registerDTO.setNickname("测试用户");registerDTO.setPhone("13800138001");registerDTO.setEmail("test@example.com");registerDTO.setCaptcha("123456");registerDTO.setCaptchaKey("test:register");Long userId = authService.register(registerDTO);assertNotNull(userId);// 2. 用户登录LoginDTO loginDTO = new LoginDTO();loginDTO.setUsername("testuser");loginDTO.setPassword("Test@123456");loginDTO.setCaptcha("123456");loginDTO.setCaptchaKey("test:login");LoginVO loginVO = authService.login(loginDTO);assertNotNull(loginVO);assertEquals("testuser", loginVO.getUsername());assertNotNull(loginVO.getAccessToken());assertNotNull(loginVO.getRefreshToken());}@Testvoid testLoginWithWrongPassword() {LoginDTO loginDTO = new LoginDTO();loginDTO.setUsername("admin");loginDTO.setPassword("wrongpassword");loginDTO.setCaptcha("123456");loginDTO.setCaptchaKey("test:login");assertThrows(Exception.class, () -> {authService.login(loginDTO);});}}
📊 认证中心功能总结
|
功能模块 |
实现技术 |
特点 |
|---|---|---|
|
用户认证 |
Spring Security + JWT |
无状态认证,支持分布式部署 |
|
密码加密 |
BCryptPasswordEncoder |
强度可调,安全性高 |
|
权限管理 |
RBAC模型 |
支持角色、权限多级授权 |
|
验证码 |
Redis缓存 |
多种类型,防暴力破解 |
|
登录保护 |
Redis计数 |
防止暴力破解,自动锁定 |
|
会话管理 |
JWT + Redis |
分布式会话,Token自动续期 |
|
安全审计 |
AOP切面 |
完整登录日志记录 |
|
跨域支持 |
CORS配置 |
支持前后端分离部署 |
|
API文档 |
Knife4j |
在线API文档,调试支持 |
🔮 下期预告
第五章:搭建用户服务(microservice-user)
我们将实现:
-
✅ 用户信息管理
-
✅ 用户资料维护
-
✅ 头像上传
-
✅ 用户统计
-
✅ 消息通知
-
✅ 积分系统
💡 最佳实践建议
-
安全第一
-
使用强密码策略
-
启用HTTPS
-
定期更换JWT密钥
-
实施多因素认证
-
-
性能优化
-
合理设置Token过期时间
-
使用Redis缓存权限信息
-
数据库索引优化
-
连接池配置
-
-
监控告警
-
监控登录失败次数
-
记录异常登录行为
-
设置Token使用告警
-
审计日志保存
-
-
可扩展性
-
支持多端登录
-
支持第三方登录
-
支持单点登录
-
支持OAuth2.0
-
技术要点:Spring Security、JWT、Redis、MySQL、RBAC
安全等级:企业级安全标准
适用场景:电商、金融、企业管理系统
马年奔腾,安全护航! 🎉
认证中心是微服务架构的安全基石,一个设计良好的认证系统能够让整个架构更加稳固可靠。希望这篇文章能够帮助你快速搭建自己的认证中心!
点赞、收藏、转发,让更多开发者受益!
关注我,获取更多微服务实战教程!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)