理论:
1.获得微信官方的网址
2.使用OAuth2.0
3.登陆需要三步
获得验证
返回一个网站
获得授权
对象
获得用户
对象
然后登陆的时候会和我们数据库中的袁勇绑定,如果没有就创建,有就关联
操作:
1.更改项目配置
在最后一行添加
127.0.0.1 bugtracker.itsource.cn
这是你去官网注册获得的域名这样才能把二维码返回过来,使用回调函数
2.引入依赖
<!--httpclient的依赖:--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency> <!--处理json的包 https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
3.登陆步骤
写一个封装好的
WxConstants把那些固定的常量封装成一个类,方便我们修改等等
package cn.jiedada.crm.web.wechart; /*这是微信提供的必要字段 只有通过这些方式才能获得我们所有的 * */ public class WxConstants { public final static String APPID = "wxd853562a0548a7d0"; //用户授权后微信的回调域名,当我们获得该值的时候会调用该方法 public final static String CALLBACK="http://bugtracker.itsource.cn/callback"; public final static String SCOPE = "snsapi_login"; public final static String APPSECRET = "4a5d5615f93f24bdba2ba8534642dbb6"; //微信上获取code的地址(这里的 // appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect) //需要我们前台在调用的时候经行拼接REDIRECT_URI= public final static String CODEURL = "https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect"; //微信上获取at的地址 public final static String ACCESSTOKEURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code"; //微信上获取用户信息的地址 public final static String USERINFOURL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"; }
因为这些都是我们必须要的字段而且不能够修改所以这样操作
因为我们获取code,at,用户信息是在微信官方获得的所以我们需要发送请求,所以我们封装一个类来发送请求
HttpClientUtils
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jiedada; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.util.Iterator; import java.util.List; import java.util.Map; /*因为我们使用微信登陆的时候会发送请求所以要使用 * */ public class HttpClientUtils { /** * http请求工具类,post请求 * * @param url url * @param params json字符串的参数 * @return * @throws Exception */ public static String httpPost(String url, String params) throws Exception { // 创建httpClient对象 DefaultHttpClient defaultHttpClient = null; try { defaultHttpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Content-Type", "application/json;charset=ut-8"); if (params != null) { System.out.println("请求参数:" + params); // 设置请求参数 HttpEntity httpEntity = new StringEntity(params, "utf-8"); httpPost.setEntity(httpEntity); } // 执行post请求,并得到相应结果 HttpResponse httpResponse = defaultHttpClient.execute(httpPost); if (httpResponse.getStatusLine().getStatusCode() != 200) { String errorLog = "请求失败,errorCode:" + httpResponse.getStatusLine().getStatusCode(); throw new Exception(url + errorLog); } // 解析结果 HttpEntity responseEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(responseEntity, "utf-8"); System.out.println("请求结果:" + responseStr); return responseStr; } catch (ClientProtocolException e) { e.printStackTrace(); throw e; } catch (IOException e) { e.printStackTrace(); throw e; } finally { if (defaultHttpClient != null) defaultHttpClient.getConnectionManager().shutdown(); } } /** * http请求工具类,get请求 * * @param url 请求地址:可以已经带参数(?),也可以没有带参数,在params中传过来 * @param params 参数:值支持字符串和list * @return * @throws Exception */ public static String httpGet(String url, Map<String, Object> params) throws Exception { DefaultHttpClient defaultHttpClient = null; try { defaultHttpClient = new DefaultHttpClient(); if (params != null) { // 参数的拼接 StringBuilder stringBuilder = new StringBuilder(); Iterator<String> iterator = params.keySet().iterator(); String key; while (iterator.hasNext()) { key = iterator.next(); Object val = params.get(key); if (val instanceof List) { // 如果是list,则遍历拼接 List v = (List) val; for (Object o : v) { stringBuilder.append(key).append("=").append(o.toString()).append("&"); } } else { // 字符串:直接拼接 stringBuilder.append(key).append("=").append(val.toString()).append("&"); } } // 删除最后一个& stringBuilder.deleteCharAt(stringBuilder.length() - 1); if (url.indexOf("?") > 0) { // url地址本身包含? url = url + "&" + stringBuilder.toString(); } else { url = url + "?" + stringBuilder.toString(); } } System.out.println("请求地址:" + url); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("Content-Type", "application/json;charset=ut-8"); // 执行 HttpResponse httpResponse = defaultHttpClient.execute(httpGet); if (httpResponse.getStatusLine().getStatusCode() != 200) { String errorLog = "请求失败,errorCode:" + httpResponse.getStatusLine().getStatusCode(); throw new Exception(url + errorLog); } // 解析结果 HttpEntity responseEntity = httpResponse.getEntity(); String responseStr = EntityUtils.toString(responseEntity, "utf-8"); System.out.println("请求结果:" + responseStr); return responseStr; } catch (ClientProtocolException e) { e.printStackTrace(); throw e; } catch (IOException e) { e.printStackTrace(); throw e; } finally { if (defaultHttpClient != null) defaultHttpClient.getConnectionManager().shutdown(); } } }
下面我们来获取值
获得登陆是的二维码在登陆页面中这是后台中登陆的代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@RequestMapping(value = "/login",method = RequestMethod.GET) @ResponseBody public AjaxResoult wecharLogin(Model model){ String wxLoginUrl = WxConstants.CODEURL.replaceAll("APPID", WxConstants.APPID) .replaceAll("REDIRECT_URI", WxConstants.CALLBACK) .replaceAll("SCOPE", WxConstants.SCOPE); AjaxResoult ajaxResoult = new AjaxResoult(); ajaxResoult.setWxLoginUrl(wxLoginUrl); return ajaxResoult; }
前台登陆时的代码
点击登陆
<el-form-item> <el-button type="primary" @click="getWxLoginUrl">微信登陆</el-button> </el-form-item>
getWxLoginUrl(event){ this.$http.get('/login').then((res)=>{ console.debug(res) ; let { msg, success,wxLoginUrl, resultObj } = res.data; if (!success) { this.$message({ message: msg, type: 'error' }); } else { // this.$router.push({ path: '/wxLoginUrl' }); window.location.href=wxLoginUrl } }) },
点击的时候发送请求
会出现图片
当使用手机扫码的时候会调到一个叫callback的方法中去
这是你去官网注册获得的域名这样才能把二维码返回过来,使用回调函数
callback方法(这里面我们需要处理的为获得at,和用户信息,判断用户是否扫过码,如果扫过就判断是否关联上了一个,如果没有就添加一个微信表的数据,并且调转到绑定页面)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/* * 回调处理 * 需要获得at * 和userinfo * */ @RequestMapping(value = "/callback",method = RequestMethod.GET) public String wecharLogin(String code,String state) throws Exception { //获得at的url String atUrl = WxConstants.ACCESSTOKEURL.replaceAll("APPID", WxConstants.APPID) .replaceAll("SECRET", WxConstants.APPSECRET) .replaceAll("CODE", code); //获得at的url然后发送请求 String atRespone = HttpClientUtils.httpGet(atUrl, null); System.out.println("atRespone" + atRespone); //因为获得user info的条件是 // USERINFOURL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID"; //需要access_token和openid JSONObject atParse = (JSONObject) JSON.parse(atRespone); System.out.println("atParse" + atParse); String access_token = atParse.getString("access_token"); String openid = atParse.getString("openid"); //获得用户信息 String userUrl = WxConstants.USERINFOURL.replaceAll("ACCESS_TOKEN", access_token) .replaceAll("OPENID", openid); //发送请求获得用户信息 String userInfo = HttpClientUtils.httpGet(userUrl, null); System.out.println("userInfo" + userInfo); //转化为JSONObject对象 JSONObject userInfoObj = (JSONObject) JSON.parse(userInfo); //取出其中的值 String useropenid = userInfoObj.getString("openid"); String nickname = userInfoObj.getString("nickname"); String unionid = userInfoObj.getString("unionid"); WeChart weChart = weChartService.findByopenid(useropenid); if (weChart == null) { //如果不存在就设置值进去 WeChart weChart1 = new WeChart(); weChart1.setOpenid(useropenid); weChart1.setNickname(nickname); weChart1.setUnionid(unionid); weChartService.save(weChart1); //跳转注册页面 /*AjaxResoult ajaxResoult = new AjaxResoult(); ajaxResoult.setBindUrl("redirect:/bind.jsp?openid="+useropenid);*/ return "redirect:http://localhost:8080/#/Register?openid="+useropenid; } else { //存在直接登陆并且把数据绑定过去 WeChart weChart2 = weChartService.findByopenid(useropenid); if (weChart2.getEmployee_id() != null) { //通过员工id找到员工的username设置login Employee employee = employeeService.findOne(weChart2.getEmployee_id()); //登陆当前用户 Subject subject = SecurityUtils.getSubject(); //使用自己的token Map<String,Object> result = new HashMap<>(); //除了返回登录成功与否,还要把登录的用户返回前端 AjaxResoult ajaxResoult = new AjaxResoult(); //登录用户 MyUsernamePasswordToken token = new MyUsernamePasswordToken(employee.getUsername()); subject.login(token); Serializable tokenid = subject.getSession().getId(); //跳转登陆页面 //还是返回ajaxResoult上面的自己在前台拼装就可以了 return "redirect:http://localhost:8080/#/echarts?tokenid="+tokenid; } else { //跳转注册页面 /*AjaxResoult ajaxResoult = new AjaxResoult(); ajaxResoult.setBindUrl("redirect:/bind.jsp?openid="+useropenid);*/ return "redirect:http://localhost:8080/#/Register?openid="+useropenid; } } }
当第一次没登陆的时候我们需要跳转到登陆页面
因为我们做了登陆权限,只能让登录页面访问,所以我们在main.js中添加了一个钩子函数
router.beforeEach((to, from, next) => { if (to.path == '/login') { sessionStorage.removeItem('token'); } //如果session没有token信息,跳转到登陆页面 let token = sessionStorage.getItem('token'); //获得请求栏上的地址 let url = window.location.href; //获得后台传过来的openid的值(获取http://localhost:8080/#/echarts=?tokenid如这里的tokenid) let openid = url.split("=")[1]; //xxx //判断是否放行 if (!token && to.path != '/login') { //判断是否是后台穿过来的,放行注册页面 if(openid && to.path == '/Register'){ next(); } //放行我们已经注册了的用户登陆问题 else if(openid && to.path == '/echarts'){ sessionStorage.setItem("token",openid); next(); } else { next({ path: '/login' }) } } else { next() } });
这是这个页面的值这个页面
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div> <!--:model="tenant" 数据双向绑定--> <!--ref="tenantForm" id="tenantForm",给form去一个名字--> <!--:rules="formRules" 校验规则--> <el-form :model="employee" ref="tenantForm" :rules="formRules" label-position="left" label-width="100px" class="demo-ruleForm login-container"> <h3 class="title">用户关联</h3> <el-form-item prop="companyName"label="用户名称"> <el-input type="text" v-model="employee.username" auto-complete="off" placeholder="请输入公司名称!"></el-input> </el-form-item> <el-form-item prop="companyNum" label="用户密码"> <el-input type="text" v-model="employee.password" auto-complete="off" placeholder="请输入座机!"></el-input> </el-form-item> <el-form-item style="100%;"> <el-button type="primary" style="100%;" @click.native.prevent="settledIn" >入驻</el-button> </el-form-item> </el-form> </div> </template> <script> export default { data() { //elementui提供自定义验证 value 当前这个框 var validatePass2 = (rule, value, callback) => { console.log(value); //确认密码 底层提供给我们 if (value === '') { callback(new Error('请再次输入密码')); } else if (value !== this.employee.password) { callback(new Error('两次输入密码不一致!')) } else { callback();//表示通过 } } return { keyword:'', mapDialogVisibale:false, //employee:tenant 为了做数据表单校验不要嵌套对象 employee: { username:'', password:'', openid:'', }, formRules: { username: [ { required: true, message: '请输入用户名称!', trigger: 'blur' } ], password: [ { required: true, message: '请输入用户密码!', trigger: 'blur' } ], comfirmPassword: [ {required: true,validator: validatePass2, trigger: 'blur' } //自定义校验规则 ] } }; }, methods: { selectAdrressConfirm(){ //把地图里面的值放入到表单地址里面,通过这种方式获得值 this.employee.address = document.getElementById("searchInput").value; //把对话框关闭 this.mapDialogVisibale = false; }, settledIn(){ //验证表单数据 this.$refs.tenantForm.validate((valid) => { //校验表单成功后才做一下操作 if (valid) { this.$confirm('确认关联吗?', '提示', {}).then(() => { //拷贝后面对象的值到新对象,防止后面代码改动引起模型变化 let url = window.location.href; let openid = url.split("=")[1]; //xxx let para = Object.assign({}, this.employee); //employee //tenant? para.openid=openid; //判断是否有id有就是修改,否则就是添加 this.$http.post("/binder",para).then((res) =>{ this.logining = false; //NProgress.done(); let { msg, success, resultObj } = res.data; if (!success) { this.$message({ message: msg, type: 'error' }); } else { sessionStorage.setItem('user', JSON.stringify(resultObj.user.username)); sessionStorage.setItem('token',resultObj.token); this.$router.push({ path: '/echarts' }); } }); }); } }) } }, } </script> <style lang="scss" scoped> .login-container { -webkit-border-radius: 5px; border-radius: 5px; -moz-border-radius: 5px; background-clip: padding-box; margin: 180px auto; 500px; padding: 35px 35px 15px 35px; background: #fff; border: 1px solid #eaeaea; box-shadow: 0 0 25px #cac6c6; .title { margin: 0px auto 40px auto; text-align: center; color: #505458; } .remember { margin: 0px 0px 35px 0px; } } .bmap{ 100%; height: 600px; } .searchinput{ 300px; box-sizing: border-box; padding: 9px; border: 1px solid #dddee1; line-height: 20px; font-size: 16px; height: 38px; color: #333; position: relative; border-radius: 4px; } </style>
binder方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/*绑定微信用户和我们的员工用户 * */ @RequestMapping(value = "/binder",method = RequestMethod.POST) @ResponseBody public AjaxResoult binder(@RequestBody Map<String,String> map){ String username = map.get("username"); System.out.println("username"+username); String password = map.get("password"); System.out.println("password"+password); String openid = map.get("openid"); System.out.println("openid"+openid); //在数据库中查找是否有该员工 Employee employee = employeeService.findEmployeeByUsername(username); if(employee==null){ return new AjaxResoult().setMsg("用户或者密码错误").setSuccess(false); }else { //判断密码是否正确 String encrypt = MD5Util.encrypt(password); if(encrypt.equals(employee.getPassword())){ WeChart weChart = weChartService.findByopenid(openid); //添加用户 Long employee_id = employee.getId(); weChart.setEmployee_id(employee_id); weChartService.update(weChart); AjaxResoult ajaxResoult = new AjaxResoult(); //登陆当前用户 Subject subject = SecurityUtils.getSubject(); //使用自己的token MyUsernamePasswordToken token = new MyUsernamePasswordToken(username); //登录用户 subject.login(token); Map<String,Object> result = new HashMap<>(); //除了返回登录成功与否,还要把登录的用户返回前端 result.put("user",employee); result.put("token",subject.getSession().getId()); ajaxResoult.setResultObj(result); return ajaxResoult; }else { return new AjaxResoult().setMsg("用户或者密码错误").setSuccess(false); } } }
这里就完成了
但是因为我们需要使用免密登陆
需要覆写身份认证过滤器
FormAuthenticationFilter
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jiedada.crm.web.shiro; import cn.jiedada.crm.web.wechart.LoginType; import cn.jiedada.crm.web.wechart.MyUsernamePasswordToken; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; /** * 自定义身份认证过滤器 */ public class MyAuthenticationFilter extends FormAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //如果是OPTIONS请求,直接放行 HttpServletRequest httpServletRequest = (HttpServletRequest) request; String method = httpServletRequest.getMethod(); //判断是否是OPTIONS请求 if("OPTIONS".equalsIgnoreCase(method)){ return true; } return super.isAccessAllowed(request, response, mappedValue); } //薪增方法 @Override protected AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response) { boolean rememberMe = isRememberMe(request); String host = getHost(request); String loginType = LoginType.PASSWORD;//需要密码 if(request.getParameter("loginType")!=null && !"".equals(request.getParameter("loginType").trim())){ loginType = request.getParameter("loginType"); } return new MyUsernamePasswordToken(username, password,loginType,rememberMe,host); } }
所以需要覆写他的加密验证方法
HashedCredentialsMatcher
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jiedada.crm.web.wechart; import com.sun.org.apache.xml.internal.resolver.readers.DOMCatalogReader; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; public class MyHashedCredentialsMatcher extends HashedCredentialsMatcher { //doCredentialsMatch @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //使用我们自定义的MyUsernamePasswordToken MyUsernamePasswordToken mupt = (MyUsernamePasswordToken) token; //是否是免密登陆,最后修改配置 if (mupt.getLoginType().equals(LoginType.NOPASSWD)) { //免密登录 return true; } return super.doCredentialsMatch(token, info); } }
而其中需要使用覆写
UsernamePasswordToken
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jiedada.crm.web.wechart; import com.sun.org.apache.xml.internal.resolver.readers.DOMCatalogReader; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; public class MyHashedCredentialsMatcher extends HashedCredentialsMatcher { //doCredentialsMatch @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //使用我们自定义的MyUsernamePasswordToken MyUsernamePasswordToken mupt = (MyUsernamePasswordToken) token; //是否是免密登陆,最后修改配置 if (mupt.getLoginType().equals(LoginType.NOPASSWD)) { //免密登录 return true; } return super.doCredentialsMatch(token, info); } }
而UsernamePasswordToken需要一个常量类
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package cn.jiedada.crm.web.wechart; public class LoginType { public static final String NOPASSWD = "NoPassword"; public static final String PASSWORD = "Password"; }
并且配置到shiro中
在application-shiro.xml中配置这里是所有的配置
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<?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:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> <!--session管理器通过继承DefaultWebSecurityManager来自定义我们的session--> <bean id="crmSessionManager" class="cn.jiedada.crm.web.shiro.CrmSessionManager"></bean> <!--shiro的核心对象--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--配置realm--> <property name="sessionManager" ref="crmSessionManager"/> <property name="realm" ref="myRealm"/> </bean> <!--Realms--> <bean id="myRealm" class="cn.jiedada.crm.web.shiro.MyRealm"> <property name="credentialsMatcher"> <bean class="cn.jiedada.crm.web.wechart.MyHashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5"/> <property name="hashIterations" value="10"/> </bean> </property> </bean> <!--自定义过滤器--> <bean id="myAuthenticationFilter" class="cn.jiedada.crm.web.shiro.MyAuthenticationFilter"></bean> <bean id="aisellPermissionsAuthorizationFilter" class="cn.jiedada.crm.web.shiro.AisellPermissionsAuthorizationFilter"></bean> <!--shiro的过滤器配置--> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="successUrl" value="/s/index"/> <property name="unauthorizedUrl" value="/s/unauthorized"/> <!--通过key在下面找到我们需要的东西,需要使用value-ref关联--> <property name="filters"> <map> <entry key="myFilter" value-ref="myAuthenticationFilter"></entry> <entry key="aisellPers" value-ref="aisellPermissionsAuthorizationFilter"></entry> </map> </property> <!--在这下面使用我们的myFilter--> <!--<property name="filterChainDefinitions"> <value> /* = anon /js/** = anon /** = myFilter </value> </property>--> </bean> <bean id="filterChainDefinitionMap" factory-bean="shiroFilterMapFactory" factory-method="createMap" /> <!--配置返回shiro权限拦截的bean--> <bean id="shiroFilterMapFactory" class="cn.jiedada.crm.web.shiro.ShiroFilterMapFactory"/> </beans>