公司需要做一个用于内部员工的培训系统的小程序,之前只是了解过,但是并没有自己动手做过小程序,以下是记录自己这次的开发过程。
微信官方登陆文档
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
1、微信小程序端调用wx.login 获取一个code(这个code是变化的,即每次调用都会不同,导致session_key也会变化,所以不要频繁调用这个,一定要在登陆态过期再掉。怎么校验登陆态是否过期呢可以用wx.checkSession(Object object) 去检验当前session_key是否过期)
2、在服务器端可以调用auth.code2Session的接口去获取openid和session_key和unionid
3、妥善保存session_key 这个session_key在之后解密中可以用的到。用jwt 带上session_key和openid生成token(登陆态) 返回小程序
4、小程序端把token存入缓存中 wx.setStorageSync(string key, any data)
5、所有wx.request发送请求到后端的接口中,都把这个登陆态token 放入请求头中。
在app.js 封装一个 wx.request的公共方法用来像后端发请求.在这里统一把token取出来放到请求头中
6、后端做全局拦截器,处理token
例子:
const app = getApp()
const { http } = getApp()
onLoad: function() {
var _this = this
wx.checkSession({
success: (res) => {
// 判断是否有token,有token 说明通过了 学院 内部用户的验证
var token = wx.getStorageSync('accessToken')
if(token===null || token===undefined || token ==='') {
// 不含有token
_this.loginNoPhoneNumber()
}
},
fail: (res) => {
console.log("未登陆")
_this.loginNoPhoneNumber()
},
})
},
loginNoPhoneNumber () {
var _this = this
wx.login({
success (res) {
if (res.code) {
console.log(res.code)
//发起网络请求
http({
url:'client/weChatLogin',
data:{
code:res.code,
},
}).then(res => {
if (res.data.code === 1) {
let openId = res.data.data.openId
wx.setStorageSync('openId',openId)
if(res.data.data.accessToken) {
// 说明登陆成功
wx.setStorageSync('accessToken', res.data.data.accessToken)
} else {
// 获取手机号的弹窗
_this.setData({
show : true
})
}
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
},
getphonenumberMethod (e) {
console.log(e.detail.errMsg)
console.log(e.detail.iv)
console.log(e.detail.encryptedData)
var this_ = this
this_.setData({
iv : e.detail.iv,
encryptedData : e.detail.encryptedData
})
// 用手机号登陆
var openId_ = wx.getStorageSync('openId')
http({
url:'client/weChatLoginByPhoneNumber',
data:{
openId:openId_,
encryptedData:e.detail.encryptedData,
iv:e.detail.iv,
},
}).then(res => {
console.log(res.data)
if (res.data.code === 1) {
wx.setStorageSync('accessToken', res.data.data.accessToken)
this_.setData({
show:false
})
} else{
this_.setData({
show:false,
notInnerUser:true
})
}
})
},
index.wxml中代码
<van-popup show="{{ show }}" closeable
close-icon="close" bind:close="onClose" custom-style="width:600rpx;height:500rpx;text-align:center">
<View className='bg'>
<view class="bg_authorization">
<text class="header-title">
需要您的授权登录
text>
view>
<View class="bg_require">
<text>由于您是第一次登陆,所以我\n们需要获取到您的手机号,\n请点击下方按钮来获取手机号text>
View>
<view class="button" >
<van-button type="primary" open-type="getPhoneNumber" bind:getphonenumber="getphonenumberMethod">获取手机号码van-button>
view>
View>
van-popup>
<van-popup show="{{ notInnerUser }}"
close-icon="close" bind:close="onClose" custom-style="width:600rpx;height:500rpx;text-align:center">
<View className='bg'>
<view class="bg_authorization">
<text class="header-title">
对不起,您不是本公司员工
text>
view>
View>
van-popup>
app.js中代码示例:
baseURL:'http://192.168.0.186:9089/train-app/',
http(params){
return new Promise((resolve, reject)=>{
const accessToken = wx.getStorageSync('accessToken')
console.log(this.baseURL + params.url)
wx.request({
url: this.baseURL + params.url,
data:params.data,
header:{
accessToken: accessToken,
},
method: params.method||'POST',
dataType:params.dataType||'json',
success(res){
resolve(res)
},
fail(err){
reject(err)
}
})
})
},
这块逻辑是这样的,首先检测session是否过期,
1、过期了,就走登陆接口重新获取code,然后发送后端client/weChatLogin 。这个接口是根据code 掉code2Session 获取到session_key和openId。根据openId去用户表查询是不是内部用户。没查到说明该用户没有用小程序登陆成功过,没有存该用户openId到数据库,这个时候我们是不生成token的,需要让用户同意获取手机号。然后根据手机号判断是否内部用户,如果内部用户则保存openId和生成token
2、没过期,session没过期说明调用过wx.login 但是不一定已经认证过了内部用户,判断用户是不是通过了系统的认证,需要查询有没有token。没有token则 走登陆流程client/weChatLogin,去获取token。
client/weChatLogin 接口如果没用给token说明需要用户根据手机号去认证,则setData show为true来显示获取手机号的弹窗
只能是显示获取按钮,引导用户去点击授权。
当用户同意授权之后,会回调getphonenumberMethod 这个方法,然后我们在getphonenumberMethod 方法中 调用client/weChatLoginByPhoneNumber 这个接口去手机号登陆验证,验证成功会颁发token
在pom中添加 jwt框架 jjwt的依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
在配置文件中添加jwt配置
jwt:
# 加密密钥
secret: iwqjhda8232bjgh432[cicada-smile]
# token有效时长
expire: 3600
# header 名称
header: token
写jwt生成token的 工具类
/**
* @author ldt
* @version 1.0
* @date 2021/4/6 13:46
*/
@Service
public class Token {
private String secret;
@Value("${jwt.secret}")
public void setSecret(String secret_) {
secret = secret_;
}
private long expire;
@Value("${jwt.expire}")
public void setExpire(Long expire_) {
expire = expire_;
}
private String header;
@Value("${jwt.header}")
public void setHeader(String header_) {
header = header_;
}
/*
* 根据身份ID标识,生成Token
*/
public String getToken (SysUser user, String session_key){
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
Map<String,Object> maps = Maps.newHashMap();
maps.put("user",user);
maps.put("sessionKey",session_key);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(JSONObject.toJSONString(maps))
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/*
* 获取 Token 中注册信息
*/
public Claims getTokenClaim (String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
编写 登陆controller
@RestController
@RequestMapping("/client")
@Api(tags = "小程序登录")
public class WeChatLoginController {
@Autowired
private SysWeChatLoginService sysWeChatLoginService;
@PostMapping("/weChatLogin")
@ApiOperation(value = "小程序登陆")
public ResultVO login(@RequestBody ClientUserLoginDTO clientUserLoginDTO) {
return sysWeChatLoginService.weChatLogin(clientUserLoginDTO);
}
@PostMapping("/weChatLoginByPhoneNumber")
@ApiOperation(value = "小程序登陆")
public ResultVO loginByPhoneNumber(@RequestBody SysWeChatLoginDTO sysWeChatLoginDTO) {
return sysWeChatLoginService.weChatLoginByPhoneNumber(sysWeChatLoginDTO);
}
}
编写 登陆ServiceImpl
@Autowired
private Token token;
private static String appId;
@Value("${wx.appId}")
public void setAppId(String appIdNew) {
appId = appIdNew;
}
private static String appSecret;
@Value("${wx.appSecret}")
public void setAppSecret(String appSecretNew) {
appSecret = appSecretNew;
}
public static String session_key = "session_key";
public static String openid = "openid";
@Override
public ResultVO weChatLogin(ClientUserLoginDTO clientUserLoginDTO) {
// 1、根据code 获取 session_key 和 open_id
String url = String.format(WeChatConstantVar.code2Session, appId, appSecret, clientUserLoginDTO.getCode());
JSONObject jsonObject = AuthUtil.doGetJson(url);
Map resultMap = JSONObject.parseObject(JSONObject.toJSONString(jsonObject), Map.class);
String session_key_str = resultMap.get(session_key).toString();
String openid_str = resultMap.get(openid).toString();
// 很据appId 查询是否绑定过内部用户
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(SysUser::getOpenId, openid_str);
List<SysUser> sysUsers = sysUserMapper.selectList(queryWrapper);
// 将openId和sessionKey 进行绑定
RedisUtils.set(ConstantVar.USER_SESSION_KEY_PREFIX + openid_str, session_key_str);
WeChatLoginVO vo = new WeChatLoginVO();
if (CollectionUtils.isEmpty(sysUsers)) {
// 说明此openId未绑定过内部用户
} else {
// 说明此openId已经绑定过内部用户,直接下发token
String token = this.token.getToken(sysUsers.get(0), session_key_str);
vo.setAccessToken(token);
}
vo.setOpenId(openid_str);
return ResultVO.success(vo);
}
@Override
public ResultVO weChatLoginByPhoneNumber(SysWeChatLoginDTO sysWeChatLoginDTO) {
String openId = sysWeChatLoginDTO.getOpenId();
String session_key = RedisUtils.get(ConstantVar.USER_SESSION_KEY_PREFIX + openId).toString();
String phoneString = WxUtils.decryptData(sysWeChatLoginDTO.getEncryptedData(), session_key, sysWeChatLoginDTO.getIv());
JSONObject phoneObject = JSONObject.parseObject(phoneString);
String phoneNumber = phoneObject.getString("phoneNumber");
//查询手机是否存在
SysUser sysUser = sysUserMapper.checkUnique(phoneNumber);
if (sysUser==null) {
return ResultVO.fail("登录失败,查无此人");
}
sysUser.setOpenId(openId);
sysUserMapper.updateById(sysUser);
String token = this.token.getToken(sysUser , session_key);
WeChatLoginVO vo = new WeChatLoginVO();
vo.setOpenId(openId);
vo.setAccessToken(token);
return ResultVO.success(vo);
}
发送请求工具类AuthUtil
public class AuthUtil {
public static JSONObject doGetJson(String URL) {
JSONObject jsonObject = null;
HttpURLConnection conn = null;
InputStream is = null;
BufferedReader br = null;
StringBuilder result = new StringBuilder();
try {
//创建远程url连接对象
URL url = new URL(URL);
//通过远程url连接对象打开一个连接,强转成HTTPURLConnection类
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
//设置连接超时时间和读取超时时间
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
conn.setRequestProperty("Accept", "application/json");
//发送请求
conn.connect();
//通过conn取得输入流,并使用Reader读取
if (200 == conn.getResponseCode()) {
is = conn.getInputStream();
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
result.append(line);
System.out.println(line);
}
} else {
System.out.println("ResponseCode is an error code:" + conn.getResponseCode());
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (is != null) {
is.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
conn.disconnect();
}
jsonObject = JSONObject.parseObject(result.toString());
return jsonObject;
}
}
用于解密的工具类WxUtils
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.Security;
public class WxUtils {
public static String decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) {
return new String(
decryptOfDiyIV(
Base64.decode(encryptDataB64),
Base64.decode(sessionKeyB64),
Base64.decode(ivB64)
)
);
}
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding";
private static Key key;
private static Cipher cipher;
private static void init(byte[] keyBytes) {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
// 转化成JAVA的密钥格式
key = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
try {
// 初始化cipher
cipher = Cipher.getInstance(ALGORITHM_STR, "BC");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密钥
* @param ivs 自定义对称解密算法初始向量 iv
* @return 解密后的字节数组
*/
private static byte[] decryptOfDiyIV(byte[] encryptedData, byte[] keyBytes, byte[] ivs) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
}
LoginInterceptor.java
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private Token tokenObj;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.verificationAdminToken(request, response);
}
private boolean verificationAdminToken(HttpServletRequest request, HttpServletResponse response)throws Exception{
log.info(new Date() + "进入了登录拦截器");
String requestURI = request.getRequestURI();
log.info("进入了登录拦截器uri:" + requestURI);
String token = request.getHeader(Constant.ACCESS_TOKEN);
if(TextUtils.isEmpty(token)){
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONObject.toJSONString(ResultCode.UNAUTHORIZED));
return false;
}
Claims tokenClaim = tokenObj.getTokenClaim(token);
String subject = tokenClaim.getSubject();
Map obj = JSONObject.parseObject(subject,Map.class);
SysUser user = (SysUser)obj.get("user");
CurSysUser curUser = new CurSysUser();
curUser.setId(user.getId());
curUser.setUserName(user.getUserName());
curUser.setLoginName(user.getLoginName());
curUser.setCompanyId(user.getCompanyId());
curUser.setDeptId(user.getDeptId());
if(curUser == null){
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONObject.toJSONString(ResultCode.UNAUTHORIZED));
return false;
}
SysUserThread.setUser(curUser);
RedisUtils.set(token, curUser, Constant.ADMINUSER_TOKEN_EXPIRATION_DATE_S, TimeUnit.SECONDS);
return true;
}
}
拦截器的配置类MvcConf.java, 需要对登陆接口 排除token认证
@Configuration
public class MvcConf extends WebMvcConfigurationSupport {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/**/*swagger*/**")
.excludePathPatterns("/train/*swagger*/**")
.excludePathPatterns("/swagger.html")
.excludePathPatterns("/v2/api-docs")
.excludePathPatterns("/oss/encryptParam")
.excludePathPatterns("/client/weChatLogin")
.excludePathPatterns("/client/weChatLoginByPhoneNumber");
}
/**
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("swagger-ui.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}
用户ThreadLocal 线程类, 在拦截其中我们已经把user信息放入了用户线程 SysUserThread.setUser(curUser);
这样我们在代码中可以直接通过SysUserThread .getUser 来获取当前登录人的信息
public class SysUserThread {
private static ThreadLocal<CurSysUser> local = new ThreadLocal<CurSysUser>();
/**
* 得到当前登录用户
*
* @return user | null
*/
public static CurSysUser getUser(){
return SysUserThread.local.get();
}
/**
* 设置登录用户
*
* @param user user
*/
public static void setUser(CurSysUser user){
SysUserThread.local.set(user);
}
/**
* 清理当前用户
*/
public static void removeUser(){
SysUserThread.local.remove();
}
}
大概就已经实现了 ,内部用户登陆的一套逻辑。 有不明白的或者可以优化的地方欢迎大家一起讨论
,如果有帮助 帮忙点个赞吧