跳转至

常见问题 FAQ

🔐 认证相关

Q1: Token过期了怎么办?

A: SDK会自动处理Token过期的情况:

  1. 自动刷新模式(推荐)

    auth:
      token:
        auto-refresh: true
        refresh-threshold: 300  # Token过期前5分钟自动刷新
    

  2. 手动刷新

    @Service
    public class TokenService {
        @Autowired
        private TokenManager tokenManager;
    
        public void refreshToken() {
            TokenResponse newToken = tokenManager.refreshToken();
            // 保存新Token
        }
    }
    

  3. 前端处理

    // Axios拦截器自动刷新
    axios.interceptors.response.use(
      response => response,
      async error => {
        if (error.response?.status === 401 && error.response?.data?.code === 1003) {
          // Token过期,刷新Token
          const newToken = await refreshToken()
          // 重试原请求
          return axios(error.config)
        }
        return Promise.reject(error)
      }
    )
    

Q2: 如何实现单点登录(SSO)?

A: 使用OAuth 2.0授权码模式:

# 应用A配置
auth:
  oauth:
    grant-type: authorization_code
    redirect-uri: http://app-a.com/callback

# 应用B配置
auth:
  oauth:
    grant-type: authorization_code
    redirect-uri: http://app-b.com/callback

流程: 1. 用户访问应用A,重定向到认证中心登录 2. 登录成功后,认证中心返回授权码 3. 应用A使用授权码换取Token 4. 用户再访问应用B时,认证中心识别已登录,直接返回授权码 5. 实现单点登录

Q3: 支持第三方登录吗?

A: 支持,目前支持以下第三方登录:

// 微信登录
@PostMapping("/oauth/wechat")
public Result wechatLogin(@RequestParam String code) {
    return authService.wechatLogin(code);
}

// GitHub登录
@PostMapping("/oauth/github")
public Result githubLogin(@RequestParam String code) {
    return authService.githubLogin(code);
}

配置:

auth:
  oauth:
    wechat:
      app-id: your-app-id
      app-secret: your-app-secret
    github:
      client-id: your-client-id
      client-secret: your-client-secret

Q4: 如何实现记住我功能?

A: 使用长期有效的RefreshToken:

@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
    LoginResponse response = authService.login(
        request.getUsername(),
        request.getPassword(),
        request.isRememberMe()  // 记住我
    );

    if (request.isRememberMe()) {
        // RefreshToken有效期延长到30天
        response.setRefreshTokenExpire(30 * 24 * 3600);
    }

    return Result.success(response);
}

Q5: 如何强制用户下线?

A: 调用强制下线接口:

@Service
public class UserService {
    @Autowired
    private AuthClient authClient;

    public void forceLogout(Long userId) {
        // 方式一:通过API
        authClient.forceLogout(userId);

        // 方式二:直接操作Redis
        redisTemplate.delete("auth:online:user:" + userId);
        redisTemplate.delete("auth:user:" + userId);
    }
}

🔑 权限相关

Q6: 权限不生效怎么办?

A: 检查以下几点:

  1. 确认启用了权限验证

    auth:
      permission:
        enabled: true
    

  2. 检查注解位置

    // ❌ 错误:注解在接口上
    public interface UserService {
        @RequirePermission("user:read")
        void getUser();
    }
    
    // ✅ 正确:注解在实现类上
    @Service
    public class UserServiceImpl implements UserService {
        @RequirePermission("user:read")
        public void getUser() {
            // ...
        }
    }
    

  3. 检查权限字符串是否匹配

    // 数据库中的权限码:user:read
    // 注解中的权限码必须完全一致
    @RequirePermission("user:read")  // ✅ 正确
    @RequirePermission("user:view")  // ❌ 错误
    

  4. 清除权限缓存

    @Autowired
    private PermissionCache permissionCache;
    
    public void refreshPermission(Long userId) {
        permissionCache.evict(userId);
    }
    

Q7: 如何实现动态权限?

A: 实现自定义权限验证器:

@Component
public class DynamicPermissionEvaluator implements PermissionEvaluator {

    @Autowired
    private PermissionRepository permissionRepository;

    @Override
    public boolean hasPermission(UserInfo user, String permission) {
        // 从数据库实时查询权限
        List<String> permissions = permissionRepository.getUserPermissions(user.getUserId());
        return permissions.contains(permission);
    }
}

Q8: 如何实现数据权限?

A: 使用数据权限注解:

@Service
public class OrderService {

    // 只能查看自己部门的订单
    @DataPermission(type = DataScopeType.DEPT)
    public List<Order> getOrders() {
        // SQL会自动添加:WHERE dept_id = #{currentUserDeptId}
        return orderMapper.selectList();
    }

    // 只能查看自己的订单
    @DataPermission(type = DataScopeType.SELF)
    public List<Order> getMyOrders() {
        // SQL会自动添加:WHERE user_id = #{currentUserId}
        return orderMapper.selectList();
    }

    // 自定义SQL过滤
    @DataPermission(
        type = DataScopeType.CUSTOM,
        sqlFilter = "region_id IN (SELECT region_id FROM user_region WHERE user_id = #{userId})"
    )
    public List<Order> getRegionOrders() {
        return orderMapper.selectList();
    }
}

Q9: 如何实现按钮级权限控制?

A: 前端配合后端权限标识:

后端:

@GetMapping("/user-info")
public Result getUserInfo() {
    UserInfo user = UserContext.getCurrentUser();
    return Result.success(user);  // 包含permissions列表
}

前端(Vue):

<template>
  <div>
    <!-- 方式一:v-if -->
    <el-button v-if="hasPermission('user:delete')" @click="deleteUser">
      删除
    </el-button>

    <!-- 方式二:自定义指令 -->
    <el-button v-permission="'user:delete'" @click="deleteUser">
      删除
    </el-button>
  </div>
</template>

<script setup>
import { useAuth } from '@/hooks/useAuth'

const { hasPermission } = useAuth()

// 自定义指令
app.directive('permission', {
  mounted(el, binding) {
    const permission = binding.value
    const permissions = store.getters.permissions

    if (!permissions.includes(permission)) {
      el.parentNode?.removeChild(el)
    }
  }
})
</script>

🏢 多租户相关

Q10: 如何启用多租户?

A: 配置和使用多租户:

  1. 启用多租户

    auth:
      tenant:
        enabled: true
        header-name: X-Tenant-Id
    

  2. 请求携带租户ID

    // 方式一:从请求头获取
    @GetMapping("/users")
    public Result getUsers(@RequestHeader("X-Tenant-Id") String tenantId) {
        // 自动根据租户ID过滤数据
    }
    
    // 方式二:SDK自动处理
    @GetMapping("/users")
    public Result getUsers() {
        String tenantId = UserContext.getTenantId();
        // 所有数据库查询自动添加 tenant_id 条件
    }
    

  3. MyBatis Plus配置

    @Configuration
    public class MyBatisPlusConfig {
    
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    
            // 添加租户拦截器
            TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
            tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
                @Override
                public Expression getTenantId() {
                    String tenantId = UserContext.getTenantId();
                    return new StringValue(tenantId);
                }
    
                @Override
                public boolean ignoreTable(String tableName) {
                    // 排除不需要租户隔离的表
                    return "sys_tenant".equals(tableName);
                }
            });
    
            interceptor.addInnerInterceptor(tenantInterceptor);
            return interceptor;
        }
    }
    

Q11: 如何切换租户?

A: 超级管理员可以切换租户:

@Service
public class TenantService {

    @RequireRole("SUPER_ADMIN")
    public void switchTenant(String targetTenantId) {
        // 切换租户
        TenantContext.setTenantId(targetTenantId);

        // 重新加载用户信息
        UserContext.refresh();
    }
}

🚀 性能相关

Q12: 如何优化权限验证性能?

A: 多种优化策略:

  1. 启用权限缓存

    auth:
      permission:
        cache-enabled: true
        cache-time: 1800  # 缓存30分钟
    

  2. 批量加载权限

    @Service
    public class PermissionService {
    
        // 用户登录时一次性加载所有权限
        @Cacheable(value = "user-permissions", key = "#userId")
        public List<String> loadAllPermissions(Long userId) {
            return permissionRepository.getUserAllPermissions(userId);
        }
    }
    

  3. 使用Redis缓存

    @Configuration
    public class CacheConfig {
    
        @Bean
        public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        }
    }
    

Q13: Token验证太慢怎么办?

A: 优化Token验证:

  1. 本地验证JWT(推荐)

    // 不需要每次都调用认证服务器
    // 使用JWT本地验证
    @Component
    public class JwtValidator {
    
        public boolean validate(String token) {
            try {
                Jws<Claims> claims = Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token);
                return !claims.getBody().getExpiration().before(new Date());
            } catch (Exception e) {
                return false;
            }
        }
    }
    

  2. 使用Redis缓存用户信息

    @Service
    public class UserCacheService {
    
        @Cacheable(value = "user-info", key = "#userId")
        public UserInfo getUserInfo(Long userId) {
            return authClient.getUserInfo(userId);
        }
    }
    

🔧 集成相关

Q14: 如何与Spring Cloud Gateway集成?

A: 在网关层统一认证:

@Configuration
public class GatewayAuthConfig {

    @Bean
    public GlobalFilter authFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // 提取Token
            String token = extractToken(request);

            if (token == null) {
                return unauthorized(exchange);
            }

            // 验证Token
            return validateToken(token)
                .flatMap(userInfo -> {
                    // 将用户信息传递给下游服务
                    ServerHttpRequest newRequest = request.mutate()
                        .header("X-User-Id", userInfo.getUserId().toString())
                        .header("X-Username", userInfo.getUsername())
                        .header("X-Roles", String.join(",", userInfo.getRoles()))
                        .build();

                    return chain.filter(exchange.mutate().request(newRequest).build());
                })
                .onErrorResume(e -> unauthorized(exchange));
        };
    }
}

Q15: 如何与Feign集成传递Token?

A: 配置Feign拦截器:

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            // 获取当前请求的Token
            ServletRequestAttributes attributes = (ServletRequestAttributes) 
                RequestContextHolder.getRequestAttributes();

            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                String token = request.getHeader("Authorization");

                // 传递给下游服务
                if (token != null) {
                    template.header("Authorization", token);
                }

                // 传递租户ID
                String tenantId = request.getHeader("X-Tenant-Id");
                if (tenantId != null) {
                    template.header("X-Tenant-Id", tenantId);
                }
            }
        };
    }
}

Q16: 前端如何处理Token刷新?

A: 使用拦截器自动刷新:

// axios配置
import axios from 'axios'

let isRefreshing = false
let requests = []

axios.interceptors.response.use(
  response => response,
  async error => {
    const { config, response } = error

    // Token过期
    if (response?.status === 401 && response?.data?.code === 1003) {
      if (!isRefreshing) {
        isRefreshing = true

        try {
          // 刷新Token
          const refreshToken = localStorage.getItem('refresh_token')
          const res = await axios.post('/api/v1/auth/refresh', { refreshToken })

          const { accessToken, refreshToken: newRefreshToken } = res.data.data
          localStorage.setItem('access_token', accessToken)
          localStorage.setItem('refresh_token', newRefreshToken)

          // 重试所有等待的请求
          requests.forEach(cb => cb(accessToken))
          requests = []

          // 重试当前请求
          config.headers['Authorization'] = `Bearer ${accessToken}`
          return axios(config)
        } catch (e) {
          // 刷新失败,跳转登录
          localStorage.clear()
          window.location.href = '/login'
        } finally {
          isRefreshing = false
        }
      } else {
        // 正在刷新Token,将请求加入队列
        return new Promise(resolve => {
          requests.push(token => {
            config.headers['Authorization'] = `Bearer ${token}`
            resolve(axios(config))
          })
        })
      }
    }

    return Promise.reject(error)
  }
)

📱 其他问题

Q17: 如何实现验证码功能?

A: 使用内置验证码API:

// 后端
@GetMapping("/captcha")
public Result getCaptcha() {
    CaptchaResponse captcha = authService.generateCaptcha();
    return Result.success(captcha);
}

@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
    // 验证验证码
    if (!authService.validateCaptcha(request.getCaptchaKey(), request.getCaptcha())) {
        return Result.error("验证码错误");
    }

    // 登录逻辑
    return Result.success(authService.login(request.getUsername(), request.getPassword()));
}
// 前端
const getCaptcha = async () => {
  const res = await axios.get('/api/v1/auth/captcha')
  captchaKey.value = res.captchaKey
  captchaImage.value = res.captchaImage
}

const login = async () => {
  await axios.post('/api/v1/auth/login', {
    username: username.value,
    password: password.value,
    captchaKey: captchaKey.value,
    captcha: captcha.value
  })
}

Q18: 如何实现登录失败锁定?

A: 配置安全策略:

auth:
  security:
    login-fail-max-count: 5      # 最大失败次数
    login-fail-lock-time: 900    # 锁定时间(秒)
    login-fail-reset-time: 3600  # 失败计数重置时间(秒)

Q19: 支持哪些数据库?

A: 理论上支持所有关系型数据库:

  • MySQL 8.0+ (推荐)
  • PostgreSQL 12+
  • Oracle 11g+
  • SQL Server 2016+
  • MariaDB 10.5+

只需修改数据库连接配置和SQL方言。

Q20: 如何备份和恢复数据?

A: 定期备份数据库:

# 备份
mysqldump -u root -p auth_db > backup_$(date +%Y%m%d).sql

# 恢复
mysql -u root -p auth_db < backup_20240101.sql

# 定时备份(crontab)
0 2 * * * /usr/bin/mysqldump -u root -ppassword auth_db > /backup/auth_$(date +\%Y\%m\%d).sql

📞 获取帮助

如果以上FAQ没有解决你的问题:

  1. 查看完整文档
  2. 查看GitHub Issues
  3. 加入技术交流群
  4. 发送邮件到:support@example.com

持续更新中... 如有其他问题,欢迎提Issue!