微信网页登录
- 微信网页登录流程(来源:微信官方文档)
如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。
关于UnionID机制
1、请注意,网页授权获取用户基本信息也遵循UnionID机制。即如果开发者有在多个公众号,或在公众号、移动应用之间统一用户帐号的需求,需要前往微信开放平台(open.weixin.qq.com)绑定公众号后,才可利用UnionID机制来满足上述需求。
2、UnionID机制的作用说明:如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的unionid来区分用户的唯一性,因为同一用户,对同一个微信开放平台下的不同应用(移动应用、网站应用和公众帐号),unionid是相同的。
关于特殊场景下的静默授权
1、上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知;
2、对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。
具体而言,网页授权流程分为四步:
1、引导用户进入授权页面同意授权,获取code
2、通过code换取网页授权access_token(与基础支持中的access_token不同)
3、如果需要,开发者可以刷新网页授权access_token,避免过期
4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)
2 第二步:通过code换取网页授权access_token
4 第四步:拉取用户信息(需scope为 snsapi_userinfo)
- 代码实现
通过前端页面,获取code并携带code跳转到指定位置
在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。
尤其注意:由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问
<script src="../plugins/jQuery/jquery-2.2.3.min.js"></script> <script src="../plugins/bootstrap/js/bootstrap.min.js"></script> <script src="../plugins/iCheck/icheck.min.js"></script> <script src="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script> <script> var obj = new WxLogin({ self_redirect:false, id:"login_container", appid: "wx3bdb1192c22883f3", scope: "snsapi_login", redirect_uri: "http://note.java.itcast.cn/weixinlogin.do" }); </script> |
用户同意授权后
如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。
code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
- 代码解析(Controller)
第二步:通过code换取网页授权access_token
首先请注意,这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。
尤其注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。
请求方法
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
- 微信登录Controller(一)
@RequestMapping("/weixinlogin") public String weixinLogin(String code) { //1.根据code、appid和secret来获取openid和access_token String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect"; Map<String, Object> map = HttpUtils.sendGet(url); Object access_token = map.get("access_token"); Object openid = map.get("openid"); //根据openid去查询数据库中有没有这个用户,如果有就登录成功,没有跳转到绑定页面 User user = userService.findByOId(String.valueOf(openid)); if (user == null) { //如果user为空---跳转到绑定id的界面 /* request.setAttribute("openid", openid);*/ request.getSession().setAttribute("openid", openid); return "forward:/WeiXinLogin.jsp"; } else { //2.将用户名密码封装成token(令牌)对象 MyUsernamePasswordToken token = new MyUsernamePasswordToken(String.valueOf(openid)); System.out.println(String.valueOf(openid)); //3.获取subject对象 Subject subject = SecurityUtils.getSubject(); //4.调用subject的login方法,传入令牌 subject.login(token); System.out.println(token.getOpenid()); //7.根据用户查询权限模块列表,放入session中 List<Module> moduleList = moduleService.findModulesByLoginUser(user); session.setAttribute("modules", moduleList); //8.转发到 后台主页 return "home/main"; } } |
- 为openid为空的扫码用户绑定一个账号密码
Controller
@RequestMapping(name = "微信绑定登录", value = "/WXlogin") public String ToLogin(String email, String password) { //0.判断用户名和密码是否为空 if (StringUtils.isBlank(email) || StringUtils.isBlank(password)) { request.setAttribute("error", "用户名密码不能为空"); return "forward:/login.jsp"; } //将openid保存到用户的openid字段 //根据邮箱账号获取用户 User user = userService.findByEmail(email); System.out.println(user.getEmail()); //从request中获取openid String openid = (String) request.getSession().getAttribute("openid"); System.out.println(openid); user.setOpenid(openid); userService.update(user); //2.将用户名密码封装成token(令牌)对象 MyUsernamePasswordToken token = new MyUsernamePasswordToken(email, password); //3.获取subject对象 Subject subject = SecurityUtils.getSubject(); //4.调用subject的login方法,传入令牌 subject.login(token); //5.若没有报异常就证明登陆成功了,若有异常就证明失败了 //6.从shiro获取用户对象,放入session中 User loginUser = (User) subject.getPrincipal(); session.setAttribute("loginUser", loginUser); //7.根据用户查询权限模块列表,放入session中 List<Module> moduleList = moduleService.findModulesByLoginUser(loginUser); session.setAttribute("modules", moduleList); //8.转发到 后台主页 return "home/main"; } |
Jsp页面
到此微信登录功能就基本完成了
- 第三方登录跳过shiro验证
在我们做微信网页授权登录时,如果有shiro验证功能,shiro会拦截微信登录页面,我们要做的是,如果扫码用户之前没有绑定用户,就跳转到绑定页面,绑定页面的shiro验证和思路之前一样
如果用户已经绑定过了,shiro就要放行这个用户,下面是具体代码实现
- Xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <description>Shiro与Spring整合</description> <!--缓存相关--> <!--使用reids做为缓存--> <!--<bean id="redisManager" class="org.crazycake.shiro.RedisManager"> <property name="host" value="127.0.0.1:6379"></property> </bean> <bean id="cacheManager" class="org.crazycake.shiro.RedisCacheManager"> <property name="redisManager" ref="redisManager"></property> </bean>--> <!--使用的是内置的内存缓存--> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 引用自定义的realm --> <property name="realm" ref="authRealm"/> <!--注入缓存管理器--> <property name="cacheManager" ref="cacheManager"></property> </bean> <!-- 自定义Realm域的编写 --> <bean id="authRealm" class="cn.itcast.web.shiro.AuthRealm"> <!-- 注入自定义的密码比较器 --> <property name="credentialsMatcher" ref="customerCredentialsMatcher"></property> </bean> <!-- 自定义的密码比较器 --> <bean id="customerCredentialsMatcher" class="cn.itcast.web.shiro.CustomCredentialsMatcher"></bean> <!--把自定义的过滤器交给spring管理--> <bean id="myPerMissionsFilter" class="cn.itcast.web.shiro.MyPerMissionsFilter"/> <!-- filter-name这个名字的值来自于web.xml中filter的名字 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <!-- 身份认证失败后 登陆页面 --> <property name="loginUrl" value="/login.jsp"></property> <!-- 权限验证失败后 --> <property name="unauthorizedUrl" value="/unauthorized.jsp"></property> <!--将自定义的过滤器交给shiro框架管理 且起个名字--> <property name="filters"> <map> <!--key就是自定义过滤器的名字--> <entry key="myperms" value-ref="myPerMissionsFilter"/> </map> </property> <property name="filterChainDefinitions"> <!-- /**代表下面的多级目录也过滤 --> <value> /system/dept/list.do = myperms["部门管理","删除部门"] <!--访问某一个连接的时候必须有perms后面设置的所有权限才可以访问--> <!-- /system/dept/list.do = perms["部门管理"]--> <!--/system/dept/list.do = perms["部门管理","删除部门"]--> /index.jsp* = anon /login.jsp* = anon /WeiXinLogin.jsp* = anon /weixinlogin.do =anon /WXlogin.do = anon /login* = anon /logout* = logout /css/** = anon /img/** = anon /plugins/** = anon /make/** = anon /** = authc /*.* = authc </value> </property> </bean> </beans> |
- 新建一个 MyUsernamePasswordToken
public class MyUsernamePasswordToken extends UsernamePasswordToken { private String openid; public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public MyUsernamePasswordToken() { super(); } /*免密登录*/ /*学shiro的时候遇到个坑,直接拿token.getPassword()做判断就会直接抛出异常,这里做下笔记,以免忘了*/ public MyUsernamePasswordToken(String openid) { super("","",false,null); this.openid = openid; } /*账号密码登录*/ public MyUsernamePasswordToken(String username, String password) { super(username, password); } public MyUsernamePasswordToken(String username, String password, boolean rememberMe, String host, String openid) { super(username, password, rememberMe, host); this.openid = openid; } } |
- 改造密码比较器
/* 自定义的密码比较器 建议继承 SimpleCredentialsMatcher 重写密码比较的方法 */ public class CustomCredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { MyUsernamePasswordToken myUsernamePasswordToken = (MyUsernamePasswordToken) token; String openid = myUsernamePasswordToken.getOpenid(); if (openid != null) { return true; } else { //1.获取令牌中用户名和密码 /* UsernamePasswordToken t = (UsernamePasswordToken) token;*/ String email = myUsernamePasswordToken.getUsername(); String password = new String(myUsernamePasswordToken.getPassword()); //2.获取身份认证信息中的数据库密码 String dbPassword = (String) info.getCredentials(); //3.返回俩密码的比较信息(若一致,返回为true;若不一致返回false,底层报异常) return dbPassword.equals(Encrypt.md5(password, email) } }} |
- 改造realm域
* 自定义的realm域: 用来获取用户的登陆信息和用户的权限信息 建议继承AuthorizingRealm */ public class AuthRealm extends AuthorizingRealm { @Autowired UserService userService; @Autowired ModuleService moduleService; @Override //权限验证,获取当前用户的所有权限信息 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //1. 获取用户 User loginUser = (User) principals.getPrimaryPrincipal(); //2. 调用moduleService获取用户的权限模块列表 List<Module> moduleList = moduleService.findModulesByLoginUser(loginUser); //3. 将权限模块列表转成权限验证信息对象中需要的数据 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> set = new HashSet<>(); //遍历模块列表,将每个模块的名字放入set中 for (Module module : moduleList) { set.add(module.getName()); } //给info对象设置模块的set集合 info.setStringPermissions(set); System.out.println(set); return info; } @Override //身份认证的操作,获取身份认证信息 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.从令牌中获取用户名 /* UsernamePasswordToken t = (UsernamePasswordToken) token;*/ MyUsernamePasswordToken t = (MyUsernamePasswordToken) token; String openid = t.getOpenid(); if (openid!=null){ User user = userService.findByOId(openid); return new SimpleAuthenticationInfo(user,openid,this.getName()); }else { String email = t.getUsername(); //2.通过用户名获取对象 User loginUser = userService.findByEmail(email); //3.判断对象是否为空, //若为空返回null,底层会报异常 //若不为空,将用户对象,数据库密码,realm域的名字封装成身份认证信息返回 return null == loginUser ? null : new SimpleAuthenticationInfo(loginUser, loginUser.getPassword(), this.getName()); } } } |
- 工具类
public class HttpUtils { public static Map<String,Object> sendGet(String url) { Map map = new HashMap(); try{ //创建HttpClient对象 CloseableHttpClient client = HttpClients.createDefault() ; //创建get请求 HttpGet get = new HttpGet(url); //发送get请求 CloseableHttpResponse response = client.execute(get) ; HttpEntity entity = response.getEntity() ; String json = EntityUtils.toString(entity,"utf-8"); System.out.println(json); map = JSON.parseObject(json,Map.class); }catch (Exception e) { e.printStackTrace(); } return map; } } |
- Pom文件
<!-- fastjson start --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.1.37</version> </dependency> <!-- fastjson end --> |