用户认证授权与会话管理
OAuth 2.0 协议解析
OAuth 2.0是当前最流行的授权协议,广泛应用于第三方登录场景。理解其核心机制是构建安全认证系统的基础。
核心角色定义
mermaid
graph TD
A["资源拥有者<br/>Resource Owner"] --> B["客户端应用<br/>Client"]
B --> C["授权服务器<br/>Authorization Server"]
C --> D["资源服务器<br/>Resource Server"]
A -.->|拥有| E["受保护资源"]
D -.->|存储| E
B -.->|请求访问| E
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#E74C3C,color:#fff,rx:10,ry:10
style C fill:#9B59B6,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10
style E fill:#E67E22,color:#fff,rx:10,ry:10角色说明:
- 资源拥有者:通常是终端用户,拥有受保护资源的访问权限
- 客户端应用:需要获取用户资源的第三方应用
- 授权服务器:验证用户身份并颁发访问令牌
- 资源服务器:存储用户资源,验证访问令牌的有效性
四种授权模式对比
| 模式 | 适用场景 | 安全级别 | 实现复杂度 |
|---|---|---|---|
| 授权码模式 | Web应用 | 最高 | 较复杂 |
| 隐式模式 | 单页应用 | 中等 | 简单 |
| 密码模式 | 受信任应用 | 较低 | 简单 |
| 客户端凭证模式 | 服务间调用 | 高 | 最简单 |
授权码模式详解
授权码模式是最安全也是最常用的授权方式,微信、QQ等平台均采用此模式。
mermaid
sequenceDiagram
participant User as 用户
participant App as 第三方应用
participant Auth as 授权服务器
participant Resource as 资源服务器
User->>App: 1. 点击第三方登录
App->>Auth: 2. 重定向到授权页面<br/>(携带client_id, redirect_uri)
Auth->>User: 3. 展示授权确认页
User->>Auth: 4. 用户同意授权
Auth->>App: 5. 重定向回调地址<br/>(携带authorization_code)
App->>Auth: 6. 用code换取token<br/>(携带client_secret)
Auth->>App: 7. 返回access_token
App->>Resource: 8. 携带token请求资源
Resource->>App: 9. 返回用户信息集成第三方登录实现
以电商平台集成微信登录为例:
java
@RestController
@RequestMapping("/oauth")
public class OAuthController {
@Value("${wechat.appId}")
private String appId;
@Value("${wechat.appSecret}")
private String appSecret;
@Value("${wechat.redirectUri}")
private String redirectUri;
/**
* 发起微信授权
*/
@GetMapping("/wechat/authorize")
public void authorize(HttpServletResponse response) throws IOException {
String state = UUID.randomUUID().toString().replace("-", "");
// 缓存state用于回调验证
redisTemplate.opsForValue().set(
"oauth:state:" + state, "1", 5, TimeUnit.MINUTES);
String authUrl = "https://open.weixin.qq.com/connect/qrconnect?"
+ "appid=" + appId
+ "&redirect_uri=" + URLEncoder.encode(redirectUri, "UTF-8")
+ "&response_type=code"
+ "&scope=snsapi_login"
+ "&state=" + state;
response.sendRedirect(authUrl);
}
/**
* 微信授权回调
*/
@GetMapping("/wechat/callback")
public Result<LoginResult> callback(
@RequestParam String code,
@RequestParam String state) {
// 验证state防止CSRF攻击
String cachedState = redisTemplate.opsForValue()
.get("oauth:state:" + state);
if (cachedState == null) {
return Result.error("授权已过期,请重新登录");
}
// 用code换取access_token
WechatTokenResponse tokenResp = wechatClient.getAccessToken(
appId, appSecret, code);
// 获取用户信息
WechatUserInfo userInfo = wechatClient.getUserInfo(
tokenResp.getAccessToken(),
tokenResp.getOpenId());
// 处理用户绑定逻辑
User user = userService.bindOrCreateUser(userInfo);
// 生成本系统token
String token = jwtService.generateToken(user);
return Result.success(new LoginResult(token, user));
}
}用户拉黑与踢人下线
拉黑功能架构设计
用户拉黑是平台安全管理的重要功能,需要考虑实时性和性能的平衡。
mermaid
graph TD
A["拉黑功能"] --> B["单用户拉黑"]
A --> C["批量拉黑"]
B --> D["管理后台操作"]
C --> E["文件批量导入"]
C --> F["规则自动拉黑"]
D --> G{"存储方案"}
E --> G
F --> G
G --> H["独立黑名单表"]
G --> I["用户状态字段"]
H --> J["解耦性好"]
H --> K["支持扩展属性"]
I --> L["实现简单"]
I --> M["查询高效"]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style G fill:#9B59B6,color:#fff,rx:10,ry:10
style H fill:#27AE60,color:#fff,rx:10,ry:10
style I fill:#E67E22,color:#fff,rx:10,ry:10黑名单存储方案
方案一:独立黑名单表
适用于用户量大、拉黑规则复杂的场景:
sql
CREATE TABLE user_blacklist (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
reason VARCHAR(500) COMMENT '拉黑原因',
operator_id BIGINT COMMENT '操作人ID',
start_time DATETIME NOT NULL COMMENT '拉黑开始时间',
end_time DATETIME COMMENT '拉黑结束时间(NULL表示永久)',
status TINYINT DEFAULT 1 COMMENT '状态: 1-生效 0-失效',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id(user_id),
INDEX idx_status_end_time(status, end_time)
) COMMENT='用户黑名单表';方案二:用户表状态字段
适用于用户量不大、规则简单的场景:
sql
ALTER TABLE user ADD COLUMN is_blocked TINYINT DEFAULT 0
COMMENT '是否拉黑: 0-否 1-是';
ALTER TABLE user ADD COLUMN blocked_until DATETIME
COMMENT '拉黑截止时间';高性能黑名单校验
对于大规模用户系统,黑名单校验需要高性能支撑:
mermaid
graph LR
A["用户请求"] --> B{"布隆过滤器<br/>快速排除"}
B -->|可能存在| C{"Redis缓存<br/>精确判断"}
B -->|一定不存在| D["放行"]
C -->|命中| E["拦截"]
C -->|未命中| F{"数据库查询"}
F -->|存在| G["加入缓存"]
F -->|不存在| D
G --> E
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#E67E22,color:#fff,rx:10,ry:10
style C fill:#9B59B6,color:#fff,rx:10,ry:10
style E fill:#E74C3C,color:#fff,rx:10,ry:10
style D fill:#27AE60,color:#fff,rx:10,ry:10布隆过滤器实现:
java
@Component
public class BlacklistChecker {
@Autowired
private RedissonClient redissonClient;
@Autowired
private BlacklistMapper blacklistMapper;
private static final String BLOOM_KEY = "user:blacklist:bloom";
private static final String CACHE_PREFIX = "user:blacklist:";
/**
* 初始化布隆过滤器
*/
@PostConstruct
public void initBloomFilter() {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(BLOOM_KEY);
bloomFilter.tryInit(10000000L, 0.01); // 1000万容量,1%误判率
// 加载现有黑名单
List<Long> blackUserIds = blacklistMapper.selectAllBlockedUserIds();
for (Long userId : blackUserIds) {
bloomFilter.add(userId);
}
}
/**
* 检查用户是否在黑名单中
*/
public boolean isBlocked(Long userId) {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter(BLOOM_KEY);
// 布隆过滤器快速判断
if (!bloomFilter.contains(userId)) {
return false; // 一定不在黑名单
}
// 可能存在,查询Redis确认
String cacheKey = CACHE_PREFIX + userId;
Boolean cached = redisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(cached)) {
return true;
}
// 查询数据库
boolean blocked = blacklistMapper.isBlocked(userId);
if (blocked) {
redisTemplate.opsForValue().set(cacheKey, "1", 1, TimeUnit.HOURS);
}
return blocked;
}
}踢人下线实现
当用户被拉黑时,需要将其已登录的会话失效:
java
@Service
public class SessionKickService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String SESSION_PREFIX = "user:session:";
private static final String TOKEN_PREFIX = "user:token:";
/**
* 踢用户下线
*/
public void kickUser(Long userId) {
// 获取用户的所有会话
String sessionKey = SESSION_PREFIX + userId;
Set<Object> tokens = redisTemplate.opsForSet().members(sessionKey);
if (tokens != null && !tokens.isEmpty()) {
// 删除所有token
for (Object token : tokens) {
redisTemplate.delete(TOKEN_PREFIX + token);
}
// 清除会话记录
redisTemplate.delete(sessionKey);
}
log.info("用户{}已被踢下线,清除{}个会话", userId,
tokens != null ? tokens.size() : 0);
}
/**
* 踢指定设备下线
*/
public void kickDevice(Long userId, String deviceId) {
String sessionKey = SESSION_PREFIX + userId;
Set<Object> tokens = redisTemplate.opsForSet().members(sessionKey);
if (tokens != null) {
for (Object token : tokens) {
String tokenStr = token.toString();
// 解析token中的设备信息
TokenPayload payload = jwtService.parseToken(tokenStr);
if (deviceId.equals(payload.getDeviceId())) {
redisTemplate.delete(TOKEN_PREFIX + tokenStr);
redisTemplate.opsForSet().remove(sessionKey, tokenStr);
break;
}
}
}
}
}Token管理最佳实践
Access Token 与 Refresh Token
双Token机制能够在安全性和用户体验之间取得平衡:
mermaid
graph TD
A["用户登录"] --> B["颁发双Token"]
B --> C["Access Token<br/>有效期2小时"]
B --> D["Refresh Token<br/>有效期7天"]
C --> E["访问受保护资源"]
E --> F{"Token是否过期"}
F -->|未过期| G["正常访问"]
F -->|已过期| H["使用Refresh Token"]
H --> I{"Refresh Token有效"}
I -->|是| J["重新颁发Access Token"]
I -->|否| K["重新登录"]
J --> E
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10
style K fill:#E74C3C,color:#fff,rx:10,ry:10Token失效场景
除了自然过期,以下场景Token应该主动失效:
| 场景 | 处理方式 |
|---|---|
| 用户修改密码 | 使该用户所有Token失效 |
| 用户主动退出 | 使当前Token失效 |
| 账号被冻结 | 使该用户所有Token失效 |
| 取消授权 | 使对应应用的Token失效 |
| 安全事件 | 批量使相关Token失效 |
Token刷新实现
java
@RestController
@RequestMapping("/auth")
public class TokenController {
@Autowired
private TokenService tokenService;
/**
* 刷新访问令牌
*/
@PostMapping("/refresh")
public Result<TokenPair> refreshToken(
@RequestHeader("Refresh-Token") String refreshToken) {
// 验证Refresh Token
RefreshTokenPayload payload = tokenService.validateRefreshToken(refreshToken);
if (payload == null) {
return Result.error(401, "刷新令牌无效或已过期");
}
// 检查用户状态
User user = userService.getById(payload.getUserId());
if (user == null || user.getStatus() != UserStatus.NORMAL) {
return Result.error(403, "账号状态异常");
}
// 生成新的Token对
String newAccessToken = tokenService.generateAccessToken(user);
String newRefreshToken = tokenService.generateRefreshToken(user);
// 使旧的Refresh Token失效
tokenService.revokeRefreshToken(refreshToken);
return Result.success(new TokenPair(newAccessToken, newRefreshToken));
}
}单点登录集成
对于企业内部多系统场景,单点登录(SSO)是常见需求。可以借助成熟的SSO框架快速实现,如Sa-Token:
java
// Sa-Token配置示例
@Configuration
public class SaTokenConfig {
@Bean
public SaTokenDao saTokenDao() {
// 使用Redis存储
return new SaTokenDaoRedisJackson();
}
}
// 登录接口
@PostMapping("/login")
public SaResult login(@RequestBody LoginRequest request) {
// 验证用户名密码
User user = userService.validateCredentials(
request.getUsername(),
request.getPassword());
if (user != null) {
// Sa-Token登录
StpUtil.login(user.getId());
return SaResult.ok("登录成功");
}
return SaResult.error("用户名或密码错误");
}
// 踢人下线
@PostMapping("/kick/{userId}")
public SaResult kickOut(@PathVariable Long userId) {
StpUtil.kickout(userId);
return SaResult.ok("已踢下线");
}更新: 2025-12-06 17:30:33
原文: https://www.yuque.com/u22210564/zoxfmt/xl1kr2c6kao5m17y