前言
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);
}
我的见解定然仍有局限性, 欢迎各位前来补充