baoshiwei
2025-03-21 18e919bde3d925ee76fe29c7a6621c2716b1e4e4
feat(social): 添加 Keycloak社交登录支持

- 新增 Keycloak 认证请求类 AuthKeycloakRequest 和枚举类 AuthKeycloakSource
- 在 SocialUtils 中添加 Keycloak 相关方法
- 修改 SocialAuthStrategy 以支持 Keycloak 登录
- 更新 SysSocialService 接口和实现类,增加 selectByUserId 方法- 在 SysLoginService 中添加 Keycloak 登出逻辑
- 更新应用配置文件,添加 Keycloak 相关配置
已添加2个文件
已修改10个文件
359 ■■■■■ 文件已修改
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-dev.yml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-prod.yml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
script/docker/redis/conf/redis.conf 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java
@@ -6,6 +6,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.baomidou.lock.annotation.Lock4j;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -49,6 +51,12 @@
@Slf4j
@Service
public class SysLoginService {
    @Value("${justauth.type.keycloak.server-url}")
    private String keycloakServerUrl;
    @Value("${justauth.type.keycloak.realm}")
    private String keycloakRealm;
    @Value("${user.password.maxRetryCount}")
    private Integer maxRetryCount;
@@ -118,6 +126,26 @@
                TenantHelper.clearDynamic();
            }
            recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
            // æ–°å¢žKeycloak登出逻辑
                Long userId = loginUser.getUserId();
                SysSocialVo social = sysSocialService.selectByUserId(userId);
                if (social == null) {
                    return;
                }
                String logoutUrl = keycloakServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/logout";
            HttpRequest request = HttpRequest.get(logoutUrl)
                .form("refresh_token", social.getRefreshToken())
                .form("id_token_hint", social.getIdToken());
                HttpResponse response = request.execute();
                if (response.isOk()) {
                    System.out.println("1234");
                }
        } catch (NotLoginException ignored) {
        } finally {
            try {
ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java
@@ -10,9 +10,11 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
@@ -21,8 +23,13 @@
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.keycloak.AuthKeycloakRequest;
import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysSocial;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.bo.SysSocialBo;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysSocialVo;
import org.dromara.system.domain.vo.SysUserVo;
@@ -59,15 +66,34 @@
     */
    @Override
    public LoginVo login(String body, SysClientVo client) {
        SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
        ValidatorUtils.validate(loginBody);
        AuthResponse<AuthUser> response = SocialUtils.loginAuth(
        // å¦‚æžœbodyp字符串中包含login_type字段,则将body转为password登录
        AuthUser authUserData = null;
        String tenantId = null;
        if (body.contains("login_type")) {
            PasswordLoginBody passwordLoginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
            tenantId = passwordLoginBody.getTenantId();
            ValidatorUtils.validate(passwordLoginBody);
            AuthKeycloakRequest authRequest = SocialUtils.getAuthKeyloakRequest("keycloak", socialProperties);
            AuthToken accessToken = authRequest.getAccessToken(passwordLoginBody);
            authUserData = authRequest.getUserInfo(accessToken);
        } else {
            SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
            tenantId =loginBody.getTenantId();
                ValidatorUtils.validate(loginBody);
            AuthResponse<AuthUser> response = SocialUtils.loginAuth(
                loginBody.getSource(), loginBody.getSocialCode(),
                loginBody.getSocialState(), socialProperties);
        if (!response.ok()) {
            throw new ServiceException(response.getMsg());
            if (!response.ok()) {
                throw new ServiceException(response.getMsg());
            }
            authUserData = response.getData();
        }
        AuthUser authUserData = response.getData();
        if ("GITEE".equals(authUserData.getSource())) {
            // å¦‚用户使用 gitee ç™»å½•顺手 star ç»™ä½œè€…一点支持 æ‹’绝白嫖
            HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
@@ -77,6 +103,47 @@
                    .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
                    .executeAsync();
        }
        if ("KEYCLOAK".equals(authUserData.getSource())) {
            // æ–°å¢žKEYCLOAK用户自动创建逻辑
            String authId = authUserData.getSource() + authUserData.getUuid();
            List<SysSocialVo> list = sysSocialService.selectByAuthId(authId);
            if (CollUtil.isEmpty(list)) {
                // è‡ªåŠ¨åˆ›å»ºæ–°ç”¨æˆ·
                SysUser newUser = new SysUser();
                newUser.setUserName(authUserData.getUsername());
                newUser.setEmail(authUserData.getEmail());
                newUser.setNickName(authUserData.getNickname());
                newUser.setPassword("Initial123@"); // åˆå§‹å¯†ç éœ€ç¬¦åˆå®‰å…¨ç­–ç•¥
                newUser.setStatus(SystemConstants.NORMAL);
                userMapper.insert(newUser); // å‡è®¾å­˜åœ¨æ’入方法
                // åˆ›å»ºç¤¾äº¤ç»‘定记录
                SysSocialBo newSocial = new SysSocialBo();
                newSocial.setUserId(newUser.getUserId());
                newSocial.setUserName(newUser.getUserName());
                newSocial.setAuthId(authId);
                newSocial.setSource(authUserData.getSource());
                newSocial.setTenantId(newUser.getTenantId());
                newSocial.setOpenId(authUserData.getUuid());
                newSocial.setAccessToken(authUserData.getToken().getAccessToken());
                newSocial.setRefreshToken(authUserData.getToken().getRefreshToken());
                newSocial.setIdToken(authUserData.getToken().getIdToken());
                sysSocialService.insertByBo(newSocial); // éœ€ç¡®ä¿æœåŠ¡æœ‰æ–°å¢žæ–¹æ³•
                // é‡æ–°æŸ¥è¯¢ç¡®ä¿æ•°æ®å¯ç”¨
                list = sysSocialService.selectByAuthId(authId);
            } else {
                // æ›´æ–°ç¤¾äº¤ç»‘定记录
                SysSocialBo socialBo = new SysSocialBo();
                socialBo.setId(list.get(0).getId());
                socialBo.setAccessToken(authUserData.getToken().getAccessToken());
                socialBo.setRefreshToken(authUserData.getToken().getRefreshToken());
                socialBo.setIdToken(authUserData.getToken().getIdToken());
                sysSocialService.updateByBo(socialBo);
            }
        }
        List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
        if (CollUtil.isEmpty(list)) {
@@ -84,7 +151,8 @@
        }
        SysSocialVo social;
        if (TenantHelper.isEnable()) {
            Optional<SysSocialVo> opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(loginBody.getTenantId()));
            String finalTenantId = tenantId;
            Optional<SysSocialVo> opt = StreamUtils.findAny(list, x -> x.getTenantId().equals(finalTenantId));
            if (opt.isEmpty()) {
                throw new ServiceException("对不起,你没有权限登录当前租户!");
            }
ruoyi-admin/src/main/resources/application-dev.yml
@@ -49,17 +49,17 @@
          driverClassName: com.mysql.cj.jdbc.Driver
          # jdbc æ‰€æœ‰å‚数配置参考 https://lionli.blog.csdn.net/article/details/122018562
          # rewriteBatchedStatements=true æ‰¹å¤„理优化 å¤§å¹…提升批量插入更新删除性能(对数据库有性能损耗 ä½¿ç”¨æ‰¹é‡æ“ä½œåº”考虑性能问题)
          url: jdbc:mysql://192.168.12.240:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
          username: root
          password: 123456
        # ä»Žåº“数据源
        slave:
          lazy: true
          type: ${spring.datasource.type}
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
          username:
          password:
#        slave:
#          lazy: true
#          type: ${spring.datasource.type}
#          driverClassName: com.mysql.cj.jdbc.Driver
#          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
#          username:
#          password:
#        oracle:
#          type: ${spring.datasource.type}
#          driverClassName: oracle.jdbc.OracleDriver
@@ -193,6 +193,14 @@
  # å‰ç«¯å¤–网访问地址
  address: http://localhost:80
  type:
    keycloak:
      # keycloak æœåŠ¡å™¨åœ°å€
      server-url: https://lanbaosystem.shlanbao.cn:8443
      realm: lanbao
      client-id: DataCapture
      client-secret: kplisa4lJHEIM6knqefVbxln85QbA5NX
      redirect-uri: ${justauth.address}/social-callback
      scopes: [openid, email, phone, profile]
    maxkey:
      # maxkey æœåŠ¡å™¨åœ°å€
      # æ³¨æ„ å¦‚下均配置均不需要修改 maxkey å·²ç»å†…置好了数据
ruoyi-admin/src/main/resources/application-prod.yml
@@ -193,8 +193,16 @@
--- # ä¸‰æ–¹æŽˆæƒ
justauth:
  # å‰ç«¯å¤–网访问地址
  address: http://localhost:80
  address: http://192.168.0.23:80
  type:
    keycloak:
      # keycloak æœåŠ¡å™¨åœ°å€
      server-url: https://lanbaosystem.shlanbao.cn:8443
      realm: lanbao
      client-id: DataCapture
      client-secret: kplisa4lJHEIM6knqefVbxln85QbA5NX
      redirect-uri: ${justauth.address}/social-callback
      scopes: [openid, email, phone, profile]
    maxkey:
      # maxkey æœåŠ¡å™¨åœ°å€
      # æ³¨æ„ å¦‚下均配置均不需要修改 maxkey å·²ç»å†…置好了数据
ruoyi-admin/src/main/resources/application.yml
@@ -1,11 +1,11 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi-Vue-Plus
  name: Lanbao_QMS
  # ç‰ˆæœ¬
  version: ${revision}
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2024
  copyrightYear: 2025
captcha:
  enable: false
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java
@@ -72,4 +72,6 @@
     */
    private List<String> scopes;
    private String realm;
}
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
package org.dromara.common.social.keycloak;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.xkcoding.http.support.HttpHeader;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import org.dromara.common.core.domain.model.PasswordLoginBody;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
 * Keycloak OAuth2 è®¤è¯è¯·æ±‚
 */
public class AuthKeycloakRequest extends AuthDefaultRequest {
    public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.keycloak.server-url");
    public static final String REALM = SpringUtils.getProperty("justauth.type.keycloak.realm");
    public AuthKeycloakRequest(AuthConfig config) {
        super(config, AuthKeycloakSource.KEYCLOAK);
    }
    public AuthKeycloakRequest(AuthConfig config, AuthStateCache authStateCache) {
        super(config, AuthKeycloakSource.KEYCLOAK, authStateCache);
    }
    public  AuthToken getAccessToken(PasswordLoginBody loginBody) {
        HttpRequest request = HttpRequest.post(SERVER_URL + "/realms/" + REALM + "/protocol/openid-connect/token")
            .form("grant_type", "password")
            .form("client_id", config.getClientId())
            .form("client_secret", config.getClientSecret())
            .form("username", loginBody.getUsername())
            .form("password", loginBody.getPassword())
            .form("scope", "openid");
        HttpResponse response = request.execute();
        Dict object = JsonUtils.parseMap(response.body());
        if (object.containsKey("error")) {
            throw new AuthException(object.getStr("error_description"));
        }
        if (object.containsKey("message")) {
            throw new AuthException(object.getStr("message"));
        }
        return AuthToken.builder()
            .accessToken(object.getStr("access_token"))
            .refreshToken(object.getStr("refresh_token"))
            .idToken(object.getStr("id_token"))
            .tokenType(object.getStr("token_type"))
            .expireIn(object.getInt("expires_in"))
            .build();
    }
    @Override
    public AuthToken getAccessToken(AuthCallback authCallback) {
        String body = doPostAuthorizationCode(authCallback.getCode());
        Dict object = JsonUtils.parseMap(body);
        if (object.containsKey("error")) {
            throw new AuthException(object.getStr("error_description"));
        }
        if (object.containsKey("message")) {
            throw new AuthException(object.getStr("message"));
        }
        return AuthToken.builder()
            .accessToken(object.getStr("access_token"))
            .refreshToken(object.getStr("refresh_token"))
            .idToken(object.getStr("id_token"))
            .tokenType(object.getStr("token_type"))
            .expireIn(object.getInt("expires_in"))
            .build();
    }
    @Override
    public AuthUser getUserInfo(AuthToken authToken) {
        String body = doGetUserInfo(authToken);
        Dict object = JsonUtils.parseMap(body);
        if (object.containsKey("error")) {
            throw new AuthException(object.getStr("error_description"));
        }
        if (object.containsKey("message")) {
            throw new AuthException(object.getStr("message"));
        }
        return AuthUser.builder()
            .uuid(object.getStr("sub"))
            .username(object.getStr("preferred_username"))
            .nickname(object.getStr("name"))
            .email(object.getStr("email"))
            .token(authToken)
            .source(this.source.toString())
            .build();
    }
    @Override
    protected String doPostAuthorizationCode(String code) {
        HttpRequest request = HttpRequest.post(source.accessToken())
            .header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
            .form("grant_type", "authorization_code")
            .form("code", code)
            .form("redirect_uri", config.getRedirectUri());
        HttpResponse response = request.execute();
        return response.body();
    }
    @Override
    protected String doGetUserInfo(AuthToken authToken) {
        try {
            return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
                .add("Content-Type", "application/json")
                .add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    @Override
    public String authorize(String state) {
        return UrlBuilder.fromBaseUrl(super.authorize(state))
            .queryParam("scope", StrUtil.join("%20", config.getScopes()))
            .build();
    }
}
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package org.dromara.common.social.keycloak;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
public enum AuthKeycloakSource implements AuthSource {
    KEYCLOAK {
        /**
         * æŽˆæƒçš„api
         */
        @Override
        public String authorize() {
            return String.format("%s/realms/%s/protocol/openid-connect/auth", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM);
        }
        @Override
        public String accessToken() {
            return String.format("%s/realms/%s/protocol/openid-connect/token", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM);
        }
        @Override
        public String userInfo() {
            return String.format("%s/realms/%s/protocol/openid-connect/userinfo", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM);
        }
        public String logout() {
            return String.format("%s/realms/%s/protocol/openid-connect/logout", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM);
        }
        @Override
        public Class<? extends AuthDefaultRequest> getTargetClass() {
            return AuthKeycloakRequest.class;
        }
    }
}
ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java
@@ -10,6 +10,7 @@
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.keycloak.AuthKeycloakRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest;
@@ -66,8 +67,23 @@
            case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);
            case "maxkey" -> new AuthMaxKeyRequest(builder.build(), STATE_CACHE);
            case "topiam" -> new AuthTopIamRequest(builder.build(), STATE_CACHE);
            case "keycloak" -> new AuthKeycloakRequest(builder.build(), STATE_CACHE);
            default -> throw new AuthException("未获取到有效的Auth配置");
        };
    }
    public static AuthKeycloakRequest getAuthKeyloakRequest(String source, SocialProperties socialProperties) {
        SocialLoginConfigProperties obj = socialProperties.getType().get(source);
        if (ObjectUtil.isNull(obj)) {
            throw new AuthException("不支持的第三方登录类型");
        }
        AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
            .clientId(obj.getClientId())
            .clientSecret(obj.getClientSecret())
            .redirectUri(obj.getRedirectUri())
            .scopes(obj.getScopes());
        return new AuthKeycloakRequest(builder.build(), STATE_CACHE);
    }
}
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java
@@ -49,5 +49,5 @@
     */
    List<SysSocialVo> selectByAuthId(String authId);
    SysSocialVo selectByUserId(Long userId);
}
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java
@@ -109,4 +109,11 @@
        return baseMapper.selectVoList(new LambdaQueryWrapper<SysSocial>().eq(SysSocial::getAuthId, authId));
    }
    @Override
    public SysSocialVo selectByUserId(Long userId) {
        SysSocialVo socialVo = baseMapper.selectVoOne(new LambdaQueryWrapper<SysSocial>().eq(SysSocial::getUserId, userId));
        return socialVo;
    }
}
script/docker/redis/conf/redis.conf
@@ -1,11 +1,11 @@
# redis å¯†ç 
requirepass ruoyi123
#requirepass ruoyi123
# key ç›‘听器配置
# notify-keyspace-events Ex
# é…ç½®æŒä¹…化文件存储路径
dir /redis/data
dir /data/redis/data
# é…ç½®rdb
# 15分钟内有至少1个key被更改则进行快照
save 900 1