框架使用的是RuoYi-Vue-Plus
使用手册如下
https://plus-doc.dromara.org/#/ruoyi-vue-plus/home
该框架已经封装好了小程序登录功能,只需要加入一些代码,代码如下。
1、pom.xml中加入
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
2、删除AuthController.java中的@ApiEncrypt注释
3、增加WeiXinService 小程序APP工具类
package org.dromara.web.service;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.system.domain.SysClient;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
/**
* 微信小程序API工具类
*
* @author Lion Li
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class WeiXinService {
private static WebClient webClient;
private void setWebClient(){
if(webClient == null){
webClient = WebClient.builder().baseUrl("https://api.weixin.qq.com").build();
}
}
public JSONObject weixinCode2Session(String code, SysClient client) {
setWebClient();
// 发送GET请求到/sns/jscode2session 端点
String response = webClient.get()
.uri("/sns/jscode2session?appid="+client.getClientKey()+"&secret="+client.getClientSecret()+"&js_code="+code+"&grant_type=authorization_code")
.retrieve()
.bodyToMono(String.class)
.block(); // 使用block()阻塞并等待结果
return JSONObject.parseObject(response);
}
}
4、编写XcxAuthStrategy
package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.model.XcxLoginBody;
import org.dromara.common.core.domain.model.XcxLoginUser;
import org.dromara.common.core.enums.UserStatus;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysClient;
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.SysSocialVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysSocialService;
import org.dromara.system.service.ISysUserService;
import org.dromara.web.domain.vo.LoginVo;
import org.dromara.web.service.IAuthStrategy;
import org.dromara.web.service.SysLoginService;
import org.dromara.web.service.WeiXinService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 邮件认证策略
*
* @author Michelle.Chung
*/
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {
private final SysLoginService loginService;
private final WeiXinService weiXinService;
private final ISysSocialService sysSocialService;
private final ISysUserService sysUserService;
@Override
public LoginVo login(String body, SysClient client) {
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
ValidatorUtils.validate(loginBody);
// xcxCode 为 小程序调用 wx.login 授权后获取
String xcxCode = loginBody.getXcxCode();
// 多个小程序识别使用
String appid = loginBody.getAppid();
// todo 以下自行实现
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
String openid = weiXinService.weixinCode2Session(xcxCode,client).getString("openid");
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUserVo user = loadUserByOpenid(openid);
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setTenantId(user.getTenantId());
loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName());
loginUser.setNickname(user.getNickName());
loginUser.setUserType(user.getUserType());
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
loginUser.setOpenid(openid);
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
model.setActiveTimeout(client.getActiveTimeout());
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
// 生成token
LoginHelper.login(loginUser, model);
LoginVo loginVo = new LoginVo();
loginVo.setAccessToken(StpUtil.getTokenValue());
loginVo.setExpireIn(StpUtil.getTokenTimeout());
loginVo.setClientId(client.getClientId());
loginVo.setOpenid(openid);
return loginVo;
}
private SysUserVo loadUserByOpenid(String openid) {
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
SysUserVo sysUserVo = sysUserService.selectUserByUserName(openid);
if (ObjectUtil.isNull(sysUserVo)) {
log.info("登录用户:{} 不存在.", openid);
SysUserBo sysUserBo = new SysUserBo();
sysUserBo.setUserName(openid);
sysUserBo.setNickName("微信小程序用户");
sysUserBo.setUserType("xcx_user");
sysUserBo.setStatus("0");
sysUserService.insertUser(sysUserBo);
return sysUserService.selectUserById(sysUserBo.getUserId());
} else if (UserStatus.DISABLE.getCode().equals(sysUserVo.getStatus())) {
log.info("登录用户:{} 已被停用.", openid);
return null;
}
return sysUserVo;
}
}
5、编写对应的拦截器,控制权限
package org.dromara.common.security.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.security.config.properties.SecurityProperties;
import org.dromara.common.security.handler.AllUrlHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 权限安全配置
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(SecurityProperties.class)
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {
private final SecurityProperties securityProperties;
/**
* 注册sa-token的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaInterceptor(handler -> {
AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
// 登录验证 -- 排除多个路径
SaRouter
// 获取所有的
.match(allUrlHandler.getUrls())
// 对未排除的路径进行检查
.check(() -> {
// 检查是否登录 是否有token
StpUtil.checkLogin();
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {
// token 无效
throw NotLoginException.newInstance(StpUtil.getLoginType(),
"-100", "客户端ID与Token不匹配",
StpUtil.getTokenValue());
}
// 有效率影响 用于临时测试
// if (log.isDebugEnabled()) {
// log.info("剩余有效时间: {}", StpUtil.getTokenTimeout());
// log.info("临时有效时间: {}", StpUtil.getTokenActivityTimeout());
// }
});
})).addPathPatterns("/**")
// 排除不需要拦截的路径
.excludePathPatterns(securityProperties.getExcludes());
}
}