RuoYi集成OAuth2:第三方登录与Token管理

RuoYi集成OAuth2:第三方登录与Token管理

【免费下载链接】RuoYi :tada: (RuoYi)官方仓库 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用 【免费下载链接】RuoYi 项目地址: https://gitcode.com/gh_mirrors/ruoyi/RuoYi

引言:从传统登录到开放授权的演进

你是否还在为系统集成第三方登录而烦恼?用户抱怨注册流程繁琐,运维团队疲于管理多平台账号体系,安全审计面临跨系统身份追溯难题?本文将详细介绍如何在RuoYi框架中集成OAuth2.0协议,实现第三方登录与JWT令牌管理,彻底解决这些痛点。

读完本文你将获得:

  • 从零构建OAuth2.0认证体系的完整步骤
  • Shiro与OAuth2共存的无缝集成方案
  • 高性能JWT令牌管理的实现方法
  • 第三方登录用户与本地账号的关联策略
  • 生产环境下的安全配置最佳实践

技术选型与架构设计

OAuth2.0与Shiro的协同架构

RuoYi框架原生采用Shiro作为安全框架(当前版本1.13.0),其认证流程基于传统的用户名密码模式。为实现第三方登录,我们需要构建OAuth2.0认证层,形成"Shiro+OAuth2+JWT"的三层架构:

mermaid

核心依赖与版本兼容性

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攻击

性能优化建议

  1. 缓存用户信息
// 在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;
}
  1. 令牌批量失效
/**
 * 使用户所有令牌失效
 */
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的完整方案,包括架构设计、代码实现、配置部署和安全优化。通过这种方式,我们实现了:

  1. 多端统一认证:支持传统账号密码登录与第三方平台登录
  2. 无状态认证:基于JWT的令牌管理,提高系统可扩展性
  3. 安全可靠:完善的令牌生命周期管理与权限控制
  4. 用户体验优化:简化注册流程,降低用户流失率

未来可以进一步扩展:

  • 支持更多OAuth2提供商(微信、QQ、企业微信等)
  • 实现基于OAuth2的API授权,支持第三方应用接入
  • 集成OAuth2 introspection端点,实现令牌状态实时查询
  • 添加MFA(多因素认证),提升账号安全性

通过这套方案,你的系统将具备企业级的认证授权能力,为用户提供安全、便捷的登录体验,同时降低系统维护成本。立即行动,为你的RuoYi项目插上OAuth2的翅膀吧!


【免费下载链接】RuoYi :tada: (RuoYi)官方仓库 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用 【免费下载链接】RuoYi 项目地址: https://gitcode.com/gh_mirrors/ruoyi/RuoYi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值