文章目录

  • 一、开发流程
    • 1.1 难点及其解决办法
      • 1.1.1 解决办法
        • 1.1.1.1 针对第一点:
        • 1.1.1.2 针对第二点:
  • 二、代码
    • 2.1 小程序
      • 2.1.1 app.js
    • 2.2 spring 后端代码
      • 2.2.1 pom.xml
      • 2.2.2 reaml 的代码
      • 2.2.3 sessionManager 代码
      • 2.2.4 ShiroConfig.java
      • 2.2.5 LoginController.java
      • 2.2.6 WeChatUtil.java
  • 三、最后的说明
    • 3.1 用户信息的管理
    • 3.2 用户登录成功后,通过 shiro 交互的页面例子我就不举例了。主要是注意 1.1.1.2 第一点内容就行。只要注意到这一点,其它便于浏览器上的 web 开发无异了

最近,我开发了一个微信小程序,后台使用 Spring 搭建,安全管理交给了 shiro。初次将二者结合开发,经验不足,不知如何把二者结合起来。在网上搜寻了一阵,查到的资料有限——给的不全,要么是代码臃肿不规范。因此,本博主决定此时抽出半日的空闲为后来者写上一篇完整的 微信小程序 + shiro 如何开发 的文章。

一、开发流程

首先,我们先看一下,微信官方推荐的登录流程,后序的开发就是按这个来的。

1.1 难点及其解决办法

仔细看这张图会发现,我们的难处有两点:

  1. 微信小程序不是 web 浏览器。它不会为我们自主的储存 shiro 的 JSessionId。需要我们自己储存
  2. 就算每次请求我们都能在 header 中带上 JSessionId ,我们服务器上的 shiro 也不能识别。需要我们对 request 做些修改

1.1.1 解决办法

1.1.1.1 针对第一点:

先简述下原理:

http 协议是无状态协议。我们并不能通过每次的请求知晓操作者是谁。为了解决这个问题,提出了 cookie 的概念,解决这个问题。
客户与服务器之间通过 sessionId 进行交流。

当然这也是 shiro 的原理。shiro 就是通过 request header 中的 JSessionId 识别用户的。

在调用我们的服务器根据用户的 openid 确认到给用户后,shiro 此时也完成了用户的登录,并把用户信息保存起来了,最后,controller 返回处理结果。(具体代码见下方)
我们在微信小程序端要获取到 JSeesionId ,并保存:
(下方代码在 app.js 的 onLounch() 方法里)

let JSessionId = response.header["Set-Cookie"].toString().split(';')[0].substring(11);wx.setStorageSync("JSessionId", JSessionId);

1.1.1.2 针对第二点:

在需要用户登录的地方,我们只要注意两点:

  1. wx.request() 向有登录要求的 URL 发送请求时,在 header 中带上该字段,代码如下:
wx.request({url: URL,header: {"Content-Type": "application/x-www-form-urlencoded","JSessionId": wx.getStorageSync('JSessionId')},data: {},success(response) {}
)};
  1. 自定义 sessionManager,并将其注入到 shiro 的 securityManager
  • 自定义 sessionManager
public class WeChatSessionManager extends DefaultWebSessionManager {public final static String HEADER_TOKEN_NAME = "JSessionId";private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";/*** 逻辑:*     如果请求头中有 JSessionId,就分析它;*     没有就调用父类的方法*/@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response){String JSessionId = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);if(JSessionId == null) {return super.getSessionId(request, response);} else {request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, JSessionId);log.info("JSessionId: {}", JSessionId);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);return JSessionId;}}
}
  • sessionManager 注入到 shiro 的 securityManager
    当然,你也可以根据自己的需求,对 sessionManager 做些自己的定义
@Configuration
public class ShiroConfig {/*** 其它代码* /@Beanpublic WeChatSessionManager sessionManager() {WeChatSessionManager weChatSessionManager = new WeChatSessionManager();return weChatSessionManager;}@Beanpublic SecurityManager securityManager(@Qualifier("realm") Realm realm,@Qualifier("sessionManager") SessionManager sessionManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(realm);securityManager.setSessionManager(sessionManager);return securityManager;}/*** 其它代码* /
}

二、代码

2.1 小程序

2.1.1 app.js

//app.js
App({globalData: {timeout: 30000,localhost: "http://localhost:8080/miniprogram",login: false,},/*** 小程序初始化*/onLaunch: function () {let p = this;wx.login({success(res) {if (res.code) {p.login(res.code);}},fail: () => wx.showModal({content: "获取 code 失败",showCancel: false})});},login(code) {let p = this;wx.request({url: p.globalData.localhost + "/noLogin/login",method: "POST",header: {"Content-Type": "application/x-www-form-urlencoded"},data: {code: code},success(response) {switch(response.data["data"]) {                  case "unregistered":wx.showModal({content: "您未注册,是否前往注册"});break;case "registered": p.globalData.login = true;let JSessionId = response.header["Set-Cookie"].toString().split(';')[0].substring(11);wx.setStorageSync("JSessionId", JSessionId);break;}},});        }
})

2.2 spring 后端代码

2.2.1 pom.xml

主要是 shiro 的 dependency,其它请根据自己的业务需求自行添加

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.7.0</version>
</dependency>

2.2.2 reaml 的代码

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import top.leeti.entity.User;
import top.leeti.service.UserService;
import top.leeti.util.PasswordUtil;import javax.annotation.Resource;@Slf4j
public class MyRealm extends AuthorizingRealm {@Resourceprivate UserService userService;@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)throws AuthenticationException {UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;User user = userService.getUserByOpenId(usernamePasswordToken.getUsername());if (user == null) {throw new AccountException();}ByteSource saltOfCredential = ByteSource.Util.bytes(user.getStuId());return new SimpleAuthenticationInfo(user, String.valueOf(usernamePasswordToken.getPassword()),saltOfCredential, getName());}
}

2.2.3 sessionManager 代码

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;/*** 自定义session管理器* 继承DefaultWebSessionManager,重写getSessionId方法*/
@Slf4j
public class WeChatSessionManager extends DefaultWebSessionManager {public final static String HEADER_TOKEN_NAME = "JSessionId";private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";/*** 逻辑:*     如果请求头中有 JSessionId,就分析它;*     没有就调用父类的方法*/@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response){String JSessionId = WebUtils.toHttp(request).getHeader(HEADER_TOKEN_NAME);if(JSessionId == null) {return super.getSessionId(request, response);} else {request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, JSessionId);log.info("JSessionId: {}", JSessionId);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);return JSessionId;}}}

2.2.4 ShiroConfig.java

import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.leeti.config.manager.WeChatSessionManager;
import top.leeti.realm.MyRealm;import java.util.HashMap;
import java.util.Map;@Configuration
public class ShiroConfig {@Beanpublic CredentialsMatcher credentialsMatcher() {HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("MD5");matcher.setHashIterations(1024);return matcher;}@Beanpublic Realm realm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher) {MyRealm realm = new MyRealm();realm.setCredentialsMatcher(credentialsMatcher);return new MyRealm();}@Beanpublic WeChatSessionManager sessionManager() {WeChatSessionManager weChatSessionManager = new WeChatSessionManager();return weChatSessionManager;}@Beanpublic SecurityManager securityManager(@Qualifier("realm") Realm realm,@Qualifier("sessionManager") SessionManager sessionManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(realm);securityManager.setSessionManager(sessionManager);return securityManager;}@Beanpublic ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 设置默认登录的 url(若登录失败,则转到此)shiroFilterFactoryBean.setLoginUrl("/app/noLogin/error");// 设置登录认证成功后默认转到的 url
//        shiroFilterFactoryBean.setSuccessUrl("/admin/index");// 设置权限认证失败时转到的 urlshiroFilterFactoryBean.setUnauthorizedUrl("/app/noLogin/noAccess");/** anon:匿名用户可访问* authc:认证用户可访问* user:使用rememberMe可访问* perms:对应权限可访问* roles[角色名]:对应角色权限可访问*///设置访问各 url 的权限Map<String, String> filterChain = new HashMap<>(5);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChain);return shiroFilterFactoryBean;}
}

2.2.5 LoginController.java

package top.leeti.controller.nologin;import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import top.leeti.entity.result.Result;
import top.leeti.myenum.ResultCodeEnum;
import top.leeti.util.WechatUtil;import java.util.Map;@Slf4j
@RestController
@RequestMapping("/miniprogram/noLogin/")
public class LoginController {@PostMapping("login")public String login(@RequestParam String code) {Map<String, String> map = WechatUtil.acquireSessionKeyAndOpenId(code);String openId = map.get("openId");Result<String> result = null;if (openId == null) {result = new Result<>(null, "获取openId失败", null, false);} else {UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(openId, "");Subject currentUser = SecurityUtils.getSubject();try{currentUser.login(usernamePasswordToken);result = new Result<>(null, null, "registered", true);} catch(AccountException accountException) {result = new Result<>(null, null, "unregistered", false);return JSON.toJSONString(result);}}return JSON.toJSONString(result);}
}

2.2.6 WeChatUtil.java

package top.leeti.util;import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;import java.util.Hashtable;
import java.util.Map;@Slf4j
public class WechatUtil {private static final String APP_ID = "wxf4eeba633c89a51f";private static final String APP_SECRET = "8a7de97a3189c29e3faf87275e3449ee";private static final String GRANT_TYPE = "authorization_code";/*** 像微信服务器发送请求,获取 sessionKey、openId* @param code*        本次登录请求的小程序传来的标识(保证传来的 code 绝不为空或是空字符串)* @return*        Map key:sessionKey、openId。如果不能正确地获取到 sessionKey、openId,*        返回的 map 中,值为 null。*/public static Map<String, String> acquireSessionKeyAndOpenId(String code){RestTemplate restTemplate = new RestTemplate();Map<String, String> map = new Hashtable<>();try{String url = String.format("https://api.weixin.qq.com/sns/jscode2session" +"?appid=%s&secret=%s&js_code=%s&grant_type=%s", APP_ID, APP_SECRET, code, GRANT_TYPE);HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);ResponseEntity<String> response = restTemplate.getForEntity( url, String.class);String[] results = response.getBody().split("\"");map.put("sessionKey", results[4]);map.put("openId", results[7]);} catch (Exception e){map.put("sessionKey", null);map.put("openId", null);}return map;}
}

三、最后的说明

3.1 用户信息的管理

我这里是把 openid 保存到了数据库。
因为每个小程序的中每个用户的 openid 具有唯一性,我就把它作为判断用户是否注册的凭证

3.2 用户登录成功后,通过 shiro 交互的页面例子我就不举例了。主要是注意 1.1.1.2 第一点内容就行。只要注意到这一点,其它便于浏览器上的 web 开发无异了

微信小程序 + shiro 实现登录(安全管理) —— 保姆级教学相关推荐

  1. 微信小程序抓包反编译保姆级教程

    文章目录 1.基本采集流程 2.采集流程 2.1 抓包 2.1.1 抓包背景介绍 2.1.2 使用工具 2.1.3 具体抓包流程 2.2 wxapkg包获取 2.2.1 wxapkg包获取背景介绍 2 ...

  2. 微信小程序如何进行登录授权和获取用户信息

    微信小程序如何进行登录授权和获取用户信息

  3. 微信小程token_微信小程序开发之登录换取token

    本文将带你了解微信小程序开发之登录换取token,希望本文对大家学微信有所帮助 前言:这次主要是介绍些业务逻辑,技术点倒是没有多少.不过在开发中,优秀的编程思路同样是非常值得学习的. 最近小程序可以说 ...

  4. 小程序登录本地服务器,微信小程序实现用户登录模块服务器搭建

    我选用的是node.js来搭建服务器,没有安装的小伙伴可以参考我的node.js其他博客. 服务器安装与配置 初始化项目,将会自动创建package.json配置文件. npm init -y 安装E ...

  5. 微信小程序获取手机号登录流程

    微信小程序获取手机号登录流程 首先前端使用wx.login 获取code wx.login({success(res) {if (res.code) {that.setData({code: res. ...

  6. 微信小程序的详细登录(上)

    前段时间发布了一个微信小程序的简单登录,那段时间我一直在忙着项目,有一天,我清闲下来准备进入小程序群里面看一下来着,刚好有人问问题了,我一看这哥们的问题好像是我写的东西啊,我感觉是时候秀一波了,是时候 ...

  7. 微信小程序的安全登录

    一.微信小程序的安全登录 让用户登录,标识用户和获取用户信息,以用户为核心提供服务,是大部分小程序都会做的事情.我们今天就来了解下在小程序中,如何做用户登录,以及如何去维护这个登录后的会话(Sessi ...

  8. 微信小程序 如何保持登录状态

    问题 由于wx.request()发起的每次请求对于服务器来说都是不同的会话(wx.request()请求是先经过微信服务器再到达我们的服务器),这样会导致后续请求都相当于未登录的状态. 解决方案 将 ...

  9. 微信小程序之授权登录

    微信小程序之授权登录 之前微信授权登录时是直接可以通过getUserInfo接口 弹出授权弹窗.由于微信官方修改了 getUserInfo 接口,所以现在无法实现一进入微信小程序就弹出授权窗口,只能通 ...

  10. 微信小程序获取手机号登录流程(个人开发者账号不支持)

    微信小程序获取手机号登录流程 所需条件 1. 非个人开发者账号 2. AppID+AppSecret 流程思路 **注意:** 代码实现 常见问题 所需条件 1. 非个人开发者账号 获取手机号文档 这 ...

最新文章

  1. React-router总结
  2. 如何获取ubuntu源码包里面的源码进行编译
  3. 全球服务器内存芯片市场规模,2020年全球存储芯片行业市场现状分析,中国是全球最主要的消费国「图」...
  4. react 使用rewired_create-react-app 通过 react-app-rewired 添加 webpack 的 alias
  5. 泛微OA7.0下载7.1下载
  6. STEP 7-MicroWIN SMART 上传时搜索不到PLC
  7. matlab投资组合权重,【原创】投资组合风险-收益关系的Matlab实现
  8. DCGM-Exporter 安装 显卡监控 Prometheus
  9. PNG-的IDAT解析
  10. 人——Web3的新平台
  11. 华为设备vlan配置命令
  12. 骑士VS热火直播NBA常规赛骑士VS热火直播12月3日骑士VS热火视频直播
  13. 实验室里的AI激情:腾讯优图的升级修炼之路
  14. 从前端角度浅谈如何做好网站的SEO优化
  15. 极客日报:三星嘲讽iPhone13:120Hz高刷我们早用上了;华为撤回对OPPO欧洲专利的异议;淘宝搜索崩了登上热搜
  16. 专家称北京上海未来十余年房价已被透支
  17. python所有单词简写和意义_简写数字成单词Python
  18. 新年礼物送什么好?五款高颜值的蓝牙耳机盘点
  19. python图片自动校正流量_利用python西电流量自动查询脚本
  20. 《科研伦理与学术规范》 全部 习题_答案 2020年秋

热门文章

  1. 快速入门一个简单的情感分类项目
  2. 如何用CSS实现div元素高度相对于整个屏幕100%
  3. openSUSE Leap 15.2 和 Tumbleweed KDE Plasma 在ThinkPad X61上安装,自带了有线、无线网卡和显卡驱动
  4. 【日语口语词典学习】第0002页
  5. Linux运维工程师常见面试题(一)
  6. select 显示”请选择“,怎样在选择之后消失的问题
  7. 实战:k8s之Longhorn备份恢复-2022.2.26
  8. 卸载 Notepad++ !事实已证明,它更牛逼……
  9. 三菱plc传送文件到服务器,三菱Q系列PLC通过FTP文件传输案例介绍
  10. 【渝粤教育】电大中专计算机网络基础作业 题库