spring security 6 spring cloud 解决方案

前言

spring-cloud-security已经停止维护, 对于曾经在spring-cloud-gateway中集成security的cloud项目来说, 升级改造是个巨大的问题, 是选择弃用security还是强制转换使用低版本呢?

其实在以前使用@PreAuthorize等权限注解的时候, 我就充满疑惑, cloud项目中, 如果在gateway中做权限校验, 这类注解是无法使用的, 那是否可以下每个子项目中都集成统一的security封装模块, 结合redis之类的分布式缓存, 每一个子项目都具有独立并且一致的校验规则.

我想spring的开发人员也是这么想的, 才停止维护cloud-security

权限模块

以我自己写的一个测试项目为例, auth模块就是security功能

所以, 如图中红框所示, security模块独立出来, 使用feign调用方式(或者httpExchange)调用user用户信息模块, user模块也可以直接引入security模块, 自己调用自己

security模块和传统写法没有区别, 都包括登录, 退出, token/cookie以及权限校验

大致流程图如上图所示

扩展

1. 用户权限的动态化管理

1.1.1 痛点

用户权限以及禁用状态之类的信息, 都被保存在缓存中, 管理员修改用户权限数据时, 无法及时修改缓存中的用户信息, 导致权限更新不及时

1.1.2 已有方案

网上能找到的方案分为两类, 一类是定义一个修改标记集合, 用户每次请求都先查询一下是否被修改, 如果有修改标记则更细信息. 另一类是每次请求都查询数据库获取最新数据

上述两类都似乎不太合理, 是否有更好的解决方案

1.1.3 我的方案1

如果你使用的是无状态token, 上述两种方案中第一种方案或许能够满足使用. 如果你和我一样使用redis/local等有状态方案, 可以尝试使用: 在用户登录之后拿取一次用户信息, 将请求的sessionid保存在用户id的集合中, 类似

{
    "sessionId:userId:1" : ["aaaa-aaaa-aaaa-aaaa"],
    "sessionId:userId:2" : ["bbbb-bbbb-bbbb-bbbb", "cccc-cccc-cccc-cccc"]
}

核心代码如下

    @GetMapping
    @ApiOperation("获取信息")
    public R<TokenUser> selfInfo(HttpServletRequest request) {
        TokenUser userInfo = SecurityUtils.getSessionUserNoException();
        if (null == userInfo) {
            return R.ok(new TokenUser());
        }
        // 将sessionId 保存到集合中
        SecurityUtils.setUserOnline(userInfo.getId(), request.getRequestedSessionId());
        return R.ok(userInfo.simple());
    }

如此一来, 修改用户信息时, 可以遍历用户id对应的sessionid集合, 修改对应的数据

比如我使用的是redis作为缓存, 修改信息的核心代码如下

import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.FlushMode;
import org.springframework.session.Session;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

public class SecurityUtils {

    /**
     * 更新用户信息
     * @param userId 用户id
     * @param tokenUser 新的用户信息
     */
    public static void setSessionUserByUserId(Long userId, TokenUser tokenUser) {
        // 获取用户的sessionid集合
        Set<String> sessionIds = getUserOnline(userId);
        if (CollectionUtils.isEmpty(sessionIds)) {
            return;
        }
        for (String sessionId : sessionIds) {

            // 如果使用的是本地缓存, 则使用MapSessionRepository
            // 可能获取不到, 使用RedisSessionRepository也行, 主要看SessionRepository接口的实现bean
            RedisIndexedSessionRepository sessionRepository = SpringUtil.getBean(RedisIndexedSessionRepository.class);

            sessionRepository.setFlushMode(FlushMode.IMMEDIATE);
            Session session = sessionRepository.findById(sessionId);

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    tokenUser,
                    tokenUser.getPassword(),
                    tokenUser.getAuthorities());

            SecurityContext securityContext = new SecurityContextImpl(authentication);

            session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);
            sessionRepository.setFlushMode(FlushMode.ON_SAVE);
        }
    }
}
1.1.4 我的方案2
    public static void setSessionUserByUserId(Long userId, TokenUser tokenUser) {
        // 遍历所有前缀为spring:session:sessions:的redis key, 循环判断是否为修改的用户信息
        Set<String> keys = stringRedisTemplate.keys(SESSION_FORMAT + "*");
        assert keys != null;
        RedisSessionRepository sessionRepository = SpringUtil.getBean(RedisSessionRepository.class);
        sessionRepository.setFlushMode(FlushMode.IMMEDIATE);
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                tokenUser,
                tokenUser.getPassword(),
                tokenUser.getAuthorities());
        for (String key : keys) {
            String sessionId = key.substring(key.lastIndexOf(":"));
            Session session = sessionRepository.findById(sessionId);
            SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT);
            TokenUser cacheUser = (TokenUser) securityContext.getAuthentication().getPrincipal();

            if (cacheUser.getId().equals(userId)) {
                securityContext.setAuthentication(authentication);
                session.setAttribute(SPRING_SECURITY_CONTEXT, securityContext);
            }
        }
        sessionRepository.setFlushMode(FlushMode.ON_SAVE);

    }

我的见解定然仍有局限性, 欢迎各位前来补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值