微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统

微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统

公司需要做一个用于内部员工的培训系统的小程序,之前只是了解过,但是并没有自己动手做过小程序,以下是记录自己这次的开发过程。

文档层解析

微信官方登陆文档
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统_第1张图片
1、微信小程序端调用wx.login 获取一个code(这个code是变化的,即每次调用都会不同,导致session_key也会变化,所以不要频繁调用这个,一定要在登陆态过期再掉。怎么校验登陆态是否过期呢可以用wx.checkSession(Object object) 去检验当前session_key是否过期)

2、在服务器端可以调用auth.code2Session的接口去获取openid和session_key和unionid
微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统_第2张图片
3、妥善保存session_key 这个session_key在之后解密中可以用的到。用jwt 带上session_key和openid生成token(登陆态) 返回小程序
4、小程序端把token存入缓存中 wx.setStorageSync(string key, any data)
微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统_第3张图片

5、所有wx.request发送请求到后端的接口中,都把这个登陆态token 放入请求头中。
在app.js 封装一个 wx.request的公共方法用来像后端发请求.在这里统一把token取出来放到请求头中

6、后端做全局拦截器,处理token

代码层解析

1、在小程序index.js的onLoad方法中 做登陆检测

例子:

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来显示获取手机号的弹窗

在微信小程序中,想要获取用户手机号的方式:
微信小程序登陆 + 基于JWT的Token 验证 + 内部用户登陆系统_第4张图片

只能是显示获取按钮,引导用户去点击授权。

当用户同意授权之后,会回调getphonenumberMethod 这个方法,然后我们在getphonenumberMethod 方法中 调用client/weChatLoginByPhoneNumber 这个接口去手机号登陆验证,验证成功会颁发token

2、后台 登陆接口

在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;
    } 
}

3、后台拦截器

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();
    }

}

大概就已经实现了 ,内部用户登陆的一套逻辑。 有不明白的或者可以优化的地方欢迎大家一起讨论
,如果有帮助 帮忙点个赞吧

你可能感兴趣的:(微信小程序登陆,技术,java,小程序,jwt)