常见问题 FAQ¶
🔐 认证相关¶
Q1: Token过期了怎么办?¶
A: SDK会自动处理Token过期的情况:
-
自动刷新模式(推荐)
-
手动刷新
-
前端处理
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: 检查以下几点:
-
确认启用了权限验证
-
检查注解位置
-
检查权限字符串是否匹配
-
清除权限缓存
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: 配置和使用多租户:
-
启用多租户
-
请求携带租户ID
-
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: 多种优化策略:
-
启用权限缓存
-
批量加载权限
-
使用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验证:
-
本地验证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; } } } -
使用Redis缓存用户信息
🔧 集成相关¶
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没有解决你的问题:
- 查看完整文档
- 查看GitHub Issues
- 加入技术交流群
- 发送邮件到:support@example.com
持续更新中... 如有其他问题,欢迎提Issue!