spring整合Sa-token+gateway实现微信无业务关联登录
1、RBAC是什么?
Role-Based Access Control,中文意思是:基于角色(Role)的访问控制。这是一种广泛应用于计算机系统和网络安全领域的访问控制模型。
简单来说,就是通过将权限分配给➡角色,再将角色分配给➡用户,来实现对系统资源的访问控制。一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。具体而言,RBAC模型定义了以下几个核心概念:
角色(Role):角色是指在系统中具有一组相关权限的抽象概念,代表了用户在特定上下文中的身份或职能,例如管理员、普通用户等。
权限(Permission):权限是指对系统资源进行操作的许可,如读取、写入、修改等。权限可以被分配给角色。
用户(User):用户是指系统的实际使用者,每个用户可以被分配一个或多个角色。
分配(Assignment):分配是指将角色与用户关联起来,以赋予用户相应的权限。
RBAC 认为授权实际上是Who 、What 、How 三元组之间的关系,也就是Who 对What 进行How 的操作,也就是“主体”对“客体”的操作。
Who:是权限的拥有者或主体(如:User,Role)。
What:是操作或对象(operation,object)。
How:具体的权限(Privilege,正向授权与负向授权)。
通过RBAC模型,可以实现灵活且易于管理的访问控制策略。管理员可以通过分配和调整角色,来管理用户的权限。这种角色层次结构可以帮助简化权限管理,并确保用户只有所需的权限。
RBAC模型广泛应用于系统安全、数据库管理、网络管理等领域,它提供了一种可扩展、可管理的访问控制机制,有助于保护系统资源免受未经授权的访问和潜在的安全威胁。
2、Sa-token介绍
官网文档:
https://sa-token.cc/index.html

引入依赖
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.38.0</version>
</dependency>
server:
# 端口
port: 8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
参考链接:https://sa-token.cc/doc.html#/start/example
整成redis:https://sa-token.cc/doc.html#/up/integ-redis

集成redis后我们可以看到数据库中存储到的具体信息。
下面,我们整合进网关层,实现路由拦截的功能
参考文档:https://sa-token.cc/doc.html#/micro/gateway-auth
全局过滤器设计
import cn.dev33.satoken.context.SaHolder; import cn.dev33.satoken.reactor.filter.SaReactorFilter; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * [Sa-Token 权限认证] 配置类 * * @author click33 */ @Configuration public class SaTokenConfigure { // 注册 Sa-Token全局过滤器 @Bean public SaReactorFilter getSaReactorFilter() { return new SaReactorFilter() // 拦截地址 .addInclude("/**") /* 拦截全部path */ // 开放地址 // .addExclude("/favicon.ico") // 鉴权方法:每次访问进入 .setAuth(obj -> { System.out.println("-------- 前端访问path:" + SaHolder.getRequest().getRequestPath()); // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录 // SaRouter.match("/auth/**", "/auth/user/doLogin", r -> StpUtil.checkRole("admin")); SaRouter.match("/oss/**", r -> StpUtil.checkLogin()); SaRouter.match("/subject/subject/add", r -> StpUtil.checkPermission("subject:add")); SaRouter.match("/subject/**", r -> StpUtil.checkLogin()); }) ; } }
其中 subject:add为我们校验的权限

整合微信登录:
整体流程:用户扫公众号。发送消息:验证码。通过 api 回复一个随机数。存入 redis
redis 的主要结构,就是 openId (用户的id标识)加验证码
用户在验证码框输入之后,点击登录,进入我们的注册模块,同时关联角色和权限。就实现了网关的统一鉴权。
用户就可以进行操作,用户可以根据个人的 openId 来维护个人信息。
用户登录成功之后,返回 token,前端的所有请求都带着 token 就可以访问。
公众号测试号地址:
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login


token自定义
import com.jingdianjichi.wx.handler.WxChatMsgFactory;
import com.jingdianjichi.wx.handler.WxChatMsgHandler;
import com.jingdianjichi.wx.utils.MessageUtil;
import com.jingdianjichi.wx.utils.SHA1;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.message.Message;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Objects;
@RestController
@Slf4j
public class CallBackController {
private static final String token = "adwidhaidwoaid";
@Resource
private WxChatMsgFactory wxChatMsgFactory;
@RequestMapping("/test")
public String test() {
return "hello world";
}
/**
* 回调消息校验
*/
@GetMapping("callback")
public String callback(@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",
signature, timestamp, nonce, echostr);
String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");
if (signature.equals(shaStr)) {
return echostr;
}
return "unknown";
}
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam(value = "msg_signature", required = false) String msgSignature) {
log.info("接收到微信消息:requestBody:{}", requestBody);
Map<String, String> messageMap = MessageUtil.parseXml(requestBody);
String msgType = messageMap.get("MsgType");
String event = messageMap.get("Event") == null ? "" : messageMap.get("Event");
log.info("msgType:{},event:{}", msgType, event);
StringBuilder sb = new StringBuilder();
sb.append(msgType);
if (!StringUtils.isEmpty(event)) {
sb.append(".");
sb.append(event);
}
String msgTypeKey = sb.toString();
WxChatMsgHandler wxChatMsgHandler = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);
if (Objects.isNull(wxChatMsgHandler)) {
return "unknown";
}
String replyContent = wxChatMsgHandler.dealMsg(messageMap);
log.info("replyContent:{}", replyContent);
return replyContent;
}
}
随机验证码机制:
@Component
@Slf4j
public class ReceiveTextMsgHandler implements WxChatMsgHandler {
private static final String KEY_WORD = "验证码";
private static final String LOGIN_PREFIX = "loginCode";
@Resource
private RedisUtil redisUtil;
@Override
public WxChatMsgTypeEnum getMsgType() {
return WxChatMsgTypeEnum.TEXT_MSG;
}
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("接收到文本消息事件");
String content = messageMap.get("Content");
if (!KEY_WORD.equals(content)) {
return "";
}
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
Random random = new Random();
int num = random.nextInt(1000);
String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num));
redisUtil.setNx(numKey, fromUserName, 5L, TimeUnit.MINUTES);
String numContent = "您当前的验证码是:" + num + ", 5分钟内有效";
String replyContent = "<xml>\n" +
" <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +
" <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +
" <CreateTime>12345678</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA[" + numContent + "]]></Content>\n" +
"</xml>";
return replyContent;
}
}

接收与消息回弹


redisUtil.setNx(numKey, fromUserName, 5L, TimeUnit.MINUTES);
@RequestMapping("doLogin") public Result<SaTokenInfo> doLogin(@RequestParam("validCode") String validCode) { try { Preconditions.checkArgument(!StringUtils.isBlank(validCode), "验证码不能为空!"); return Result.ok(authUserDomainService.doLogin(validCode)); } catch (Exception e) { log.error("UserController.doLogin.error:{}", e.getMessage(), e); return Result.fail("用户登录失败"); } } @RequestMapping("isLogin") public String isLogin() { return "当前会话是否登录:" + StpUtil.isLogin(); } }
service设计
@Override public SaTokenInfo doLoginAdmin(String validCode) { String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode); String openId = redisUtil.get(loginKey); if (StringUtils.isBlank(openId)) { return null; } AuthUserBO authUserBO = new AuthUserBO(); authUserBO.setUserName(openId); this.registerAdmin(authUserBO); StpUtil.login(openId); SaTokenInfo tokenInfo = StpUtil.getTokenInfo(); return tokenInfo; }
然后将loginid传入注册service,实现用户自动注册
public Boolean register(AuthUserBO authUserBO) {
//校验用户是否存在
AuthUser existAuthUser = new AuthUser();
existAuthUser.setUserName(authUserBO.getUserName());
List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser);
if (existUser.size() > 0) {
return true;
}
AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO);
if (StringUtils.isNotBlank(authUser.getPassword())) {
authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt));
}
// if (StringUtils.isBlank(authUser.getPassword())) {
// authUser.setPassword(SaSecureUtil.md5BySalt("123456", salt));
// }
if (StringUtils.isBlank(authUser.getAvatar())) {
authUser.setAvatar("http://117.72.10.84:9000/user/icon/微信图片_20231203153718(1).png");
}
if (StringUtils.isBlank(authUser.getNickName())) {
authUser.setNickName("路过的游客一枚呀~");
}
authUser.setStatus(AuthUserStatusEnum.OPEN.getCode());
authUser.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
Integer count = authUserService.insert(authUser);
//建立一个初步的角色的关联
AuthRole authRole = new AuthRole();
authRole.setRoleKey(AuthConstant.NORMAL_USER);
AuthRole roleResult = authRoleService.queryByCondition(authRole);
Long roleId = roleResult.getId();
Long userId = authUser.getId();
AuthUserRole authUserRole = new AuthUserRole();
authUserRole.setUserId(userId);
authUserRole.setRoleId(roleId);
authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());
authUserRoleService.insert(authUserRole);
String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName());
List<AuthRole> roleList = new LinkedList<>();
roleList.add(authRole);
redisUtil.set(roleKey, new Gson().toJson(roleList));
AuthRolePermission authRolePermission = new AuthRolePermission();
authRolePermission.setRoleId(roleId);
List<AuthRolePermission> rolePermissionList = authRolePermissionService.
queryByCondition(authRolePermission);
List<Long> permissionIdList = rolePermissionList.stream()
.map(AuthRolePermission::getPermissionId).collect(Collectors.toList());
//根据roleId查权限
List<AuthPermission> permissionList = authPermissionService.queryByRoleList(permissionIdList);
String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());
redisUtil.set(permissionKey, new Gson().toJson(permissionList));
return count > 0;
}
public static final String NORMAL_USER = "normal_user";
public static final String ADMIN_USER = "admin_user";
}

登录成功后,用户表存入了我们测试号的唯一id。
List<Long> permissionIdList = rolePermissionList.stream()
.map(AuthRolePermission::getPermissionId).collect(Collectors.toList());
//根据roleId查权限
List<AuthPermission> permissionList = authPermissionService.queryByRoleList(permissionIdList);
String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());
redisUtil.set(permissionKey, new Gson().toJson(permissionList));
return count > 0;
获取用户权限信息存入redis
AuthRole authRole = new AuthRole(); authRole.setRoleKey(AuthConstant.NORMAL_USER); AuthRole roleResult = authRoleService.queryByCondition(authRole); Long roleId = roleResult.getId(); Long userId = authUser.getId(); AuthUserRole authUserRole = new AuthUserRole(); authUserRole.setUserId(userId); authUserRole.setRoleId(roleId); authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode()); authUserRoleService.insert(authUserRole); String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName()); List<AuthRole> roleList = new LinkedList<>(); roleList.add(authRole); redisUtil.set(roleKey, new Gson().toJson(roleList));
角色信息存入redis


回到我们的网关层,获取redis中相应的权限信息
/**
* 自定义权限验证接口扩展
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private RedisUtil redisUtil;
private String authPermissionPrefix = "auth.permission";
private String authRolePrefix = "auth.role";
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return getAuth(loginId.toString(), authPermissionPrefix);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return getAuth(loginId.toString(), authRolePrefix);
}
private List<String> getAuth(String loginId, String prefix) {
System.out.println(loginId+" &&"+prefix);
String authKey = redisUtil.buildKey(prefix, loginId.toString());
String authValue = redisUtil.get(authKey);
if (StringUtils.isBlank(authValue)) {
return Collections.emptyList();
}
List<String> authList = new LinkedList<>();
if (authRolePrefix.equals(prefix)) {
List<AuthRole> roleList = new Gson().fromJson(authValue, new TypeToken<List<AuthRole>>() {
}.getType());
authList = roleList.stream().map(AuthRole::getRoleKey).collect(Collectors.toList());
} else if (authPermissionPrefix.equals(prefix)) {
List<AuthPermission> permissionList = new Gson().fromJson(authValue, new TypeToken<List<AuthPermission>>() {
}.getType());
authList = permissionList.stream().map(AuthPermission::getPermissionKey).collect(Collectors.toList());
}
return authList;
}
}
通过

请求中包含token值传递给网关层,网关层根据token获取用户的loginId


然后根据用户ID与

浙公网安备 33010602011771号