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与