RuoYi集成OAuth2:第三方登录与Token管理
引言:从传统登录到开放授权的演进
你是否还在为系统集成第三方登录而烦恼?用户抱怨注册流程繁琐,运维团队疲于管理多平台账号体系,安全审计面临跨系统身份追溯难题?本文将详细介绍如何在RuoYi框架中集成OAuth2.0协议,实现第三方登录与JWT令牌管理,彻底解决这些痛点。
读完本文你将获得:
- 从零构建OAuth2.0认证体系的完整步骤
- Shiro与OAuth2共存的无缝集成方案
- 高性能JWT令牌管理的实现方法
- 第三方登录用户与本地账号的关联策略
- 生产环境下的安全配置最佳实践
技术选型与架构设计
OAuth2.0与Shiro的协同架构
RuoYi框架原生采用Shiro作为安全框架(当前版本1.13.0),其认证流程基于传统的用户名密码模式。为实现第三方登录,我们需要构建OAuth2.0认证层,形成"Shiro+OAuth2+JWT"的三层架构:
核心依赖与版本兼容性
在pom.xml中添加OAuth2与JWT相关依赖:
<!-- OAuth2核心依赖 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- HTTP客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
版本兼容性说明:
- Shiro 1.13.0与Spring Security OAuth2可共存,但需注意过滤器链顺序
- JJWT 0.9.1支持JWS/JWE标准,满足基本安全需求
- Spring Cloud OpenFeign用于简化第三方平台API调用
数据库设计与扩展
OAuth2所需表结构设计
现有RuoYi用户体系需扩展以下表结构以支持OAuth2认证:
-- 用户第三方账号关联表
CREATE TABLE sys_user_oauth (
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
user_id bigint(20) NOT NULL COMMENT '系统用户ID',
provider varchar(20) NOT NULL COMMENT '第三方平台标识(gitee/github/wechat)',
provider_user_id varchar(64) NOT NULL COMMENT '第三方用户唯一标识',
access_token varchar(255) DEFAULT NULL COMMENT '访问令牌',
refresh_token varchar(255) DEFAULT NULL COMMENT '刷新令牌',
expires_in int(11) DEFAULT NULL COMMENT '令牌过期时间(秒)',
create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_provider (provider, provider_user_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号关联';
-- OAuth2客户端配置表
CREATE TABLE oauth_client_details (
client_id varchar(64) NOT NULL COMMENT '客户端ID',
resource_ids varchar(256) DEFAULT NULL COMMENT '资源ID集合',
client_secret varchar(256) DEFAULT NULL COMMENT '客户端密钥',
scope varchar(256) DEFAULT NULL COMMENT '权限范围',
authorized_grant_types varchar(256) DEFAULT NULL COMMENT '授权类型',
web_server_redirect_uri varchar(256) DEFAULT NULL COMMENT '重定向URI',
authorities varchar(256) DEFAULT NULL COMMENT '权限集合',
access_token_validity int(11) DEFAULT NULL COMMENT '访问令牌有效期(秒)',
refresh_token_validity int(11) DEFAULT NULL COMMENT '刷新令牌有效期(秒)',
additional_information varchar(4096) DEFAULT NULL COMMENT '附加信息',
autoapprove varchar(256) DEFAULT NULL COMMENT '是否自动授权',
PRIMARY KEY (client_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OAuth2客户端配置';
数据模型扩展
创建对应的实体类SysUserOauth.java:
package com.ruoyi.system.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class SysUserOauth extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long id;
private Long userId;
private String provider;
private String providerUserId;
private String accessToken;
private String refreshToken;
private Integer expiresIn;
}
认证流程改造
Shiro与OAuth2过滤器链整合
修改ShiroConfig.java,添加OAuth2过滤器并调整过滤顺序:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 添加OAuth2过滤器
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("oauth2", new OAuth2AuthenticationFilter()); // 自定义OAuth2过滤器
filters.put("jwt", new JwtAuthenticationFilter()); // JWT认证过滤器
// 保留原有过滤器
filters.put("onlineSession", onlineSessionFilter());
filters.put("syncOnlineSession", syncOnlineSessionFilter());
// ...其他过滤器
shiroFilterFactoryBean.setFilters(filters);
// 调整URL过滤规则
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// OAuth2回调地址放行
filterChainDefinitionMap.put("/oauth2/callback/*", "anon");
// JWT认证地址
filterChainDefinitionMap.put("/api/**", "jwt");
// 保留原有规则
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
// ...其他规则
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
登录服务扩展
改造SysLoginService.java,添加OAuth2认证逻辑:
@Autowired
private OAuth2Service oauth2Service;
/**
* OAuth2第三方登录
*/
public SysUser oauth2Login(String provider, String code) {
// 1. 通过授权码获取第三方用户信息
OAuth2UserInfo userInfo = oauth2Service.getOAuth2UserInfo(provider, code);
// 2. 检查是否已关联本地用户
SysUserOauth sysUserOauth = userOauthService.selectByProviderAndUid(provider, userInfo.getProviderUserId());
if (sysUserOauth != null) {
// 已关联,直接获取本地用户
SysUser user = userService.selectUserById(sysUserOauth.getUserId());
validateUserStatus(user);
recordLoginInfo(user.getUserId());
return user;
}
// 3. 未关联,检查是否存在同邮箱用户
String email = userInfo.getEmail();
SysUser user = userService.selectUserByEmail(email);
if (user != null) {
// 存在同邮箱用户,自动关联
userOauthService.insertUserOauth(provider, userInfo, user.getUserId());
recordLoginInfo(user.getUserId());
return user;
}
// 4. 自动注册新用户
user = autoRegisterUser(userInfo);
userOauthService.insertUserOauth(provider, userInfo, user.getUserId());
recordLoginInfo(user.getUserId());
return user;
}
/**
* 自动注册用户
*/
private SysUser autoRegisterUser(OAuth2UserInfo userInfo) {
SysUser user = new SysUser();
user.setLoginName("oauth_" + userInfo.getProviderUserId());
user.setUserName(userInfo.getNickname());
user.setEmail(userInfo.getEmail());
user.setAvatar(userInfo.getAvatar());
user.setPassword(Md5Utils.hash(user.getLoginName() + "123456")); // 默认密码
user.setStatus(UserStatus.NORMAL.getCode());
user.setUserType(UserConstants.REGISTER_USER_TYPE);
user.setCreateBy("system");
userService.insertUser(user);
// 分配默认角色
userRoleService.insertUserRole(user.getUserId(), new Long[]{2L}); // 普通用户角色
return user;
}
/**
* 用户状态校验
*/
private void validateUserStatus(SysUser user) {
if (user == null) {
throw new UserNotExistsException();
}
if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
throw new UserDeleteException();
}
if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
throw new UserBlockedException();
}
}
JWT令牌管理
JWT工具类实现
创建JwtUtils.java:
package com.ruoyi.common.utils.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private long expire;
private Key getSignKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成JWT令牌
*/
public String generateToken(Long userId, String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + expire * 1000);
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析JWT令牌
*/
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 验证令牌是否过期
*/
public boolean isTokenExpired(String token) {
Date expiration = parseToken(token).getExpiration();
return expiration.before(new Date());
}
/**
* 从令牌中获取用户ID
*/
public Long getUserIdFromToken(String token) {
return Long.valueOf(parseToken(token).get("userId").toString());
}
}
JWT过滤器实现
创建JwtAuthenticationFilter.java:
public class JwtAuthenticationFilter extends AuthenticatingFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private SysUserService userService;
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
String token = getTokenFromRequest(request);
if (StringUtils.isNotBlank(token) && !jwtUtils.isTokenExpired(token)) {
return new JwtToken(token);
}
return null;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String token = getTokenFromRequest(request);
if (StringUtils.isBlank(token)) {
AjaxResult ajaxResult = AjaxResult.error("请先登录");
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
if (jwtUtils.isTokenExpired(token)) {
AjaxResult ajaxResult = AjaxResult.error("令牌已过期,请重新登录");
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
return executeLogin(request, response);
}
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
// 登录成功,设置用户信息到上下文
JwtToken jwtToken = (JwtToken) token;
Claims claims = jwtUtils.parseToken(jwtToken.getPrincipal().toString());
Long userId = Long.valueOf(claims.get("userId").toString());
SysUser user = userService.selectUserById(userId);
ShiroUtils.setUser(user);
return true;
}
private String getTokenFromRequest(ServletRequest request) {
String bearerToken = ((HttpServletRequest) request).getHeader("Authorization");
if (StringUtils.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
第三方平台集成
GitHub登录实现
创建GitHubOAuth2ServiceImpl.java:
@Service("github")
public class GitHubOAuth2ServiceImpl implements OAuth2Service {
@Value("${oauth2.github.client-id}")
private String clientId;
@Value("${oauth2.github.client-secret}")
private String clientSecret;
@Value("${oauth2.github.redirect-uri}")
private String redirectUri;
private static final String ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
private static final String USER_INFO_URL = "https://api.github.com/user";
@Override
public OAuth2UserInfo getOAuth2UserInfo(String code) {
// 1. 获取访问令牌
Map<String, String> params = new HashMap<>();
params.put("client_id", clientId);
params.put("client_secret", clientSecret);
params.put("code", code);
params.put("redirect_uri", redirectUri);
String response = HttpUtils.sendPost(ACCESS_TOKEN_URL, params);
Map<String, String> tokenMap = parseQueryString(response);
// 2. 获取用户信息
String accessToken = tokenMap.get("access_token");
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "token " + accessToken);
ResponseEntity<String> userResponse = HttpUtils.sendGet(USER_INFO_URL, headers);
// 3. 解析用户信息
JSONObject jsonObject = JSON.parseObject(userResponse.getBody());
OAuth2UserInfo userInfo = new OAuth2UserInfo();
userInfo.setProvider("github");
userInfo.setProviderUserId(jsonObject.getString("id"));
userInfo.setNickname(jsonObject.getString("name"));
userInfo.setEmail(jsonObject.getString("email"));
userInfo.setAvatar(jsonObject.getString("avatar_url"));
return userInfo;
}
private Map<String, String> parseQueryString(String queryString) {
Map<String, String> map = new HashMap<>();
String[] params = queryString.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
map.put(keyValue[0], keyValue.length > 1 ? keyValue[1] : "");
}
return map;
}
}
配置与部署
应用配置
在application.yml中添加OAuth2配置:
# OAuth2配置
oauth2:
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
redirect-uri: http://localhost:8080/oauth2/callback/github
gitee:
client-id: your-gitee-client-id
client-secret: your-gitee-client-secret
redirect-uri: http://localhost:8080/oauth2/callback/gitee
# JWT配置
jwt:
secret: your-256-bit-secret-key-here # 建议使用32位随机字符串
expire: 3600 # 令牌有效期(秒),建议1小时
前端改造
修改登录页面login.html,添加第三方登录按钮:
<div class="oauth2-login">
<div class="title">第三方登录</div>
<div class="oauth2-buttons">
<a href="/oauth2/authorize/github" class="btn-github">
<i class="fa fa-github"></i> GitHub登录
</a>
<a href="/oauth2/authorize/gitee" class="btn-gitee">
<i class="fa fa-gitee"></i> Gitee登录
</a>
</div>
</div>
<style>
.oauth2-login {
margin-top: 20px;
text-align: center;
}
.title {
margin-bottom: 10px;
color: #666;
}
.oauth2-buttons a {
display: inline-block;
width: 120px;
padding: 8px 0;
margin: 0 10px;
border-radius: 4px;
color: #fff;
text-decoration: none;
}
.btn-github {
background-color: #24292e;
}
.btn-gitee {
background-color: #c71d23;
}
</style>
安全与性能优化
令牌安全策略
| 安全措施 | 实现方法 | 风险降低 |
|---|---|---|
| 短期令牌 | 设置access_token有效期为1小时 | 降低令牌被盗用风险 |
| 刷新令牌 | 实现refresh_token机制,有效期7天 | 减少用户登录频率 |
| 签名算法 | 使用HS256/RS256算法,密钥定期轮换 | 防止令牌被篡改 |
| HTTPS传输 | 全站HTTPS,防止中间人攻击 | 防止令牌被窃听 |
| 存储安全 | 前端使用HttpOnly Cookie存储令牌 | 防止XSS攻击 |
性能优化建议
- 缓存用户信息:
// 在ShiroConfig中配置缓存
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache/ehcache-shiro.xml");
return em;
}
// 在UserRealm中启用缓存
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUser user = (SysUser) principals.getPrimaryPrincipal();
Long userId = user.getUserId();
// 缓存key
String cacheKey = ShiroConstants.CACHE_AUTHORIZATION + userId;
// 从缓存获取
AuthorizationInfo authorizationInfo = (AuthorizationInfo) cacheManager.getCache(SysConstants.SYS_AUTH_CACHE).get(cacheKey);
if (authorizationInfo == null) {
// 缓存未命中,查询数据库
authorizationInfo = authorizationInfoService.getAuthorizationInfo(userId);
// 存入缓存
cacheManager.getCache(SysConstants.SYS_AUTH_CACHE).put(cacheKey, authorizationInfo);
}
return authorizationInfo;
}
- 令牌批量失效:
/**
* 使用户所有令牌失效
*/
public void invalidateUserTokens(Long userId) {
// 1. 更新用户令牌版本
userService.updateTokenVersion(userId);
// 2. 清除缓存中的权限信息
String cacheKey = ShiroConstants.CACHE_AUTHORIZATION + userId;
cacheManager.getCache(SysConstants.SYS_AUTH_CACHE).remove(cacheKey);
}
常见问题与解决方案
跨域问题
问题:第三方登录回调时出现跨域错误。
解决方案:配置CORS过滤器:
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}
令牌吊销
问题:用户修改密码后,旧令牌仍能使用。
解决方案:实现令牌版本控制:
// 在用户表添加token_version字段
ALTER TABLE sys_user ADD COLUMN token_version INT DEFAULT 0 COMMENT '令牌版本号';
// 在JwtUtils添加版本验证
public boolean validateTokenVersion(String token) {
Claims claims = parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());
Integer tokenVersion = Integer.valueOf(claims.get("version").toString());
SysUser user = userService.selectUserById(userId);
return user.getTokenVersion().equals(tokenVersion);
}
总结与展望
本文详细介绍了在RuoYi框架中集成OAuth2.0的完整方案,包括架构设计、代码实现、配置部署和安全优化。通过这种方式,我们实现了:
- 多端统一认证:支持传统账号密码登录与第三方平台登录
- 无状态认证:基于JWT的令牌管理,提高系统可扩展性
- 安全可靠:完善的令牌生命周期管理与权限控制
- 用户体验优化:简化注册流程,降低用户流失率
未来可以进一步扩展:
- 支持更多OAuth2提供商(微信、QQ、企业微信等)
- 实现基于OAuth2的API授权,支持第三方应用接入
- 集成OAuth2 introspection端点,实现令牌状态实时查询
- 添加MFA(多因素认证),提升账号安全性
通过这套方案,你的系统将具备企业级的认证授权能力,为用户提供安全、便捷的登录体验,同时降低系统维护成本。立即行动,为你的RuoYi项目插上OAuth2的翅膀吧!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



