Ssm+vue+ElementUI实现微信第三方登录

ssm+vue+ElementUI实现前后端分离第三方登录功能

这次的分享记录得非常的详细,希望对大家有所帮助!

1.继承ssm需要的jar包和配置文件在我另一篇文章已经写下来了

(第三方登录本地host文件配置:127.0.0.1 bugtracker.itsource.cn)

2.准备shiro模块

2.1 导入shiro需要的jar包(这里web模块也要依赖shiro模块)

 
            cn.itsource
            itsource_service
            1.0-SNAPSHOT
        
        
            org.apache.shiro
            shiro-all
            1.4.1
        

        
        
            javax.servlet
            javax.servlet-api
            3.0.1
            provided
        

2.2 在web.xml配置文件中加入过滤器


 
   shiroFilter
   org.springframework.web.filter.DelegatingFilterProxy
   
     targetFilterLifecycle
     true
   
 

 
   shiroFilter
   /*
 

2.3 准备shiro spring配置文件
(applicationContext-shiro.xml)

Itsource_auth_shiro中的applicationContext-shiro.xml



   
   
             
       
   

   
   
       
           
               
               
           
       
   

   
   
       
       
       
       
      
       
           
               /login = anon
               /** = authc
           
       
   

2.4 把shiro配置文件(applicationContext-shiro.xml)集成到Spring(这里注意集成到的是Spring而不是SpringMvc)


    contextConfigLocation
    

      classpath*:applicationContext.xml,
      classpath*:applicationContext-shiro.xml
    
  
  
  
    org.springframework.web.context.ContextLoaderListener
  

3.实现登录

3.1. ==密码加密保存工具 (这里用的是MD5加密方式) ==

package cn.fours.util;

import org.apache.shiro.crypto.hash.SimpleHash;

public class MD5Util {

    public static final String SALT = "itsource";

    /**
     * 加密
     *
     * @param source
     * @return
     */
    public static String encrypt(String source) {
        SimpleHash simpleHash = new SimpleHash("MD5", source, SALT, 10);
        return simpleHash.toString();
    }

}

3.2 在添加调用添加员工时添加加密方式密码

employee.setPassword(MD5Util.encrypt(employee.getPassword()));

3.3 前端vue登录实现(这里的页面是最终的页面,包括了最后第三方登录的,注意的是我这里的图片是用的绝对路径,引用时一定要注意删掉不然会报错)





3.4 LoginController(这里的登录模块包括了普通用户名密码登录+微信第三方登录+注册+绑定+免密登录,重定向的页面下面会给出详细代码)
还有要注意的就是因为cookie的管理机制导致前后端分离项目中,ajax请求是没有携带cookie的。所以后台是无法通过cookie获取ssionid的,从而无法获取到session对象,下面代码也是写了解决方法的(Token)

package cn.fours.web.controller;

import cn.fours.common.domain.Login;
import cn.fours.common.domain.WxUser;
import cn.fours.service.ILoginService;
import cn.fours.service.IWxUserService;
import cn.fours.util.*;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 微信登录
 */
@Controller
@CrossOrigin
public class WxLoginController {
    @Autowired
    private IWxUserService service;
    @Autowired
    private ILoginService loginService;
    
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ResponseBody
    public JsonResult login(@RequestBody Login login){
        Subject subject = SecurityUtils.getSubject();
      
        if(!subject.isAuthenticated()){
            try {
                MyUsernamePasswordToken token = new MyUsernamePasswordToken(login.getUsername(),login.getPassword());
              
                subject.login(token);
            } catch (UnknownAccountException e) {
                e.printStackTrace();
                return new JsonResult("用户名不存在!");
            } catch (IncorrectCredentialsException e){
                e.printStackTrace();
                return new JsonResult("密码错误!");
            } catch (AuthenticationException e){
                e.printStackTrace();
                return new JsonResult("你登个鸡巴!");
            }
        }
        JsonResult jsonResult = new JsonResult();
        Login login1 = (Login) subject.getPrincipal();
        
        login.setPassword(null);
        HashMap map = new HashMap<>();
        map.put("user",login1);
        map.put("token",subject.getSession().getId());
       
        
        jsonResult.setResultObj(map);
        return jsonResult;
    }
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult register(@RequestBody Login login) {
        
        JsonResult result = new JsonResult();
        //密码一致验证
/*		if (!Objects.equals(login.getPassword(), comfirmPassword)) {
			result.setMessage("密码不一致");
			result.setSuccess(false);
			return result;
		}*/
        login.setPassword(MD5Util.encrypt(login.getPassword()));

        String password = login.getPassword();

        String encrypt = MD5Util.encrypt(password);

        login.setPassword(encrypt);
        try {
            loginService.save(login);
            
        } catch (Exception e){
            result.setSuccess(false);
            result.setMsg("注册失败,请重试");
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 拉起二维码
     * 跳转到本地的login
     * @return
     */
    @RequestMapping(value = "/wxlogin",method =RequestMethod.GET )
    @ResponseBody
    public String login(Model model){
        System.out.println("242424");
        String url = WxConstants.CODEURL.replaceAll("APPID", WxConstants.APPID).
                replaceAll("CALLBACK", WxConstants.CALLBACK)
                .replaceAll("SCOPE", WxConstants.SCOPE);
        model.addAttribute("wxLoginUrl",url);
        return url;
    }
    @RequestMapping(value = "/callback",method =RequestMethod.GET )
    public String callBack(String code,String state) throws Exception {
        /**
         * 获取code
         *发送请求获取ak
         */
        String akUrl = WxConstants.ACCESSTOKEURL.replaceAll("APPID",
                WxConstants.APPID).replaceAll("SECRET", WxConstants.APPSECRET)
                .replaceAll("CODE", code);
        /**
         * 发送请求获取ak
         */
        String responseStr = HttpClientUtils.httpGet(akUrl, null);
        
        /**
         * 发送请求,获取用户信息''
         * json字符串转换为对象
         */
        JSONObject jsonobject = (JSONObject)JSON.parse(responseStr);
        String access_token = jsonobject.getString("access_token");
        
        String openid = jsonobject.getString("openid");
        System.out.println("openid:"+jsonobject.getString("openid"));


        /**
         * 拿到ak和openid再发送请求
         * 发送用户信息的请求地址
         */
        String userInfoUrl = WxConstants.USERINFOURL.replaceAll("ACCESS_TOKEN", access_token).replaceAll("OPENID", openid);
        String userInfoStr = HttpClientUtils.httpGet(userInfoUrl, null);
        

        /**
         * 判断用户是否绑定过,没有绑定就跳转到绑定页面(用户名和密码)
         * 根据openid进行查询用户
         */
        JSONObject userInfo = (JSONObject)JSON.parse(userInfoStr);
        String openid1 = userInfo.getString("openid");
        WxUser wxuser = service.findWxUserById(openid);
        if(wxuser==null){
            String nickname = userInfo.getString("nickname");
            System.out.println("nickname:"+nickname);
            String headimgurl = userInfo.getString("headimgurl");
            String unionid = userInfo.getString("unionid");
            wxuser = new WxUser();
            //保存到数据库
            wxuser.setOpenid(openid1);
            wxuser.setNickname(nickname);
            wxuser.setHeadimgurl(headimgurl);
            wxuser.setUnionid(unionid);
            service.save(wxuser);
            //跳转到绑定页面,绑定一个用户
            return "redirect:http://localhost:8080/#/bind?openid="+openid;
        }else {
            //说明有这个人,登录过得,如果没有绑定,进行绑定,如果绑定过就免密登录
            if(wxuser.getEmpid()!=null){
                //查询单个绑定数据
                Login login = loginService.queryOne(wxuser.getEmpid());
                //传name参数,免密只需要name
                MyUsernamePasswordToken token = new MyUsernamePasswordToken(login.getUsername());
                Subject subject = SecurityUtils.getSubject();
                subject.login(token);
                Login mmlogin = (Login) subject.getPrincipal();
                Serializable tokenid = subject.getSession().getId();
                //扫码免密登录成功之后直接跳转到登陆成功页面
                return "redirect:http://localhost:8080/#/mianmi?user="+mmlogin+"tokenid="+tokenid;
            }
        }
        return null;
    }

    //没有登录过就绑定用户(绑定方法)
    @RequestMapping(value = "/binder",method = RequestMethod.POST)
    @ResponseBody
    public JsonResult binder(@RequestBody Map map ){
        //拿到用户名,密码,openid
        String username = map.get("username");
        String password = map.get("password");
        String openid = map.get("openid");
        //根据openid去查询
        WxUser wxUser = service.findWxUserById(openid);
        if(wxUser!=null){
            //根据username查询用户id(还没有写这一个方法)
            /*WxLoginController login = loginService.getByUsername(username);*/
            Login byUsername = loginService.getByUsername(username);
            Long id = byUsername.getId();
            wxUser.setEmpid(id);
            //更新,绑定之后
            service.update(wxUser);
            //免密登录---认证和授权
            return new JsonResult("绑定成功");
        }
        return new JsonResult("绑定失败");
    }
}

3.5 退出登录

sessionStorage.removeItem('token');

3.6 Token放在请求体里面,所以在前端main.js中主动携带Token

axios.interceptors.request.use(config => {
   if (sessionStorage.getItem('token')) {
       // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
       config.headers['X-Token'] = sessionStorage.getItem('token')
   }
   console.debug('config',config)
   return config
}, error => {
   // Do something with request error
   Promise.reject(error)
})

3.7 applicationContext-shiro.xml

Shirospring配置文件
  
    

    
    
        
        
        
    

3.8 CrmSessionMannager

package cn.fours.util;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;

public class CrmSessionManager extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "X-TOKEN";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public CrmSessionManager() {
        super();
    }

    @Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
    //取到jessionid
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        HttpServletRequest request1 = (HttpServletRequest) request;
        //如果请求头中有 X-TOKEN 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            System.out.println(id+"jjjjjjjjj"+request1.getRequestURI()+request1.getMethod());
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

3.9 这里前后端分离是会出现跨域的,所以在预检请求放行OPTIONS (cors跨域处理时,每次都要跨域预检查,也就是发一个options请求,只要是这样的请求shiro都是要放行的)






    
    
        
        
        
        
        
            
                
            
        
        
            
                /login = anon
                /** = myAuthc
            
        
    

3.10 自定义身份认证过滤器

package cn.fours.util;

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;

/**
 * 登录认证Token的处理
 */
public class MyAuthenticationFilter extends FormAuthenticationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //如果是OPTIONS请求,直接放行
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String method = httpServletRequest.getMethod();
        System.out.println(method);
        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);
    }
}

4 微信登录

4.1 工具类的封装

package cn.fours.util;

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 params) throws Exception {
   	DefaultHttpClient defaultHttpClient = null;
   	try {
   		defaultHttpClient = new DefaultHttpClient();
   		if (params != null) {
   			// 参数的拼接
   			StringBuilder stringBuilder = new StringBuilder();
   			Iterator 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();
   	}
   }
}

4.2 常量封装

package cn.fours.util;

public class WxConstants {
   /**
    * 应用程序id
    */
   public  final static String APPID = "wxd853562a0548a7d0";

   //用户授权后微信的回调域名
   public final static String CALLBACK="http://bugtracker.itsource.cn/callback";
   /**
    * pc端常量
    */
   public final static String SCOPE = "snsapi_login";
   /**
    * 应用密码
    */
   public final static String APPSECRET = "4a5d5615f93f24bdba2ba8534642dbb6";
   //微信上获取code的地址---拉起二维码
   public final static String CODEURL = "https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=CALLBACK&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
   //微信上获取ak的地址
   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";

}

4.2.1 重写登录密码匹配器

package cn.fours.util;

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;

/**
 * 
 * 重写密码登录匹配器
 */
public class MyHashedCredentialsMatcher extends HashedCredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        MyUsernamePasswordToken mupt = (MyUsernamePasswordToken) token;
        if (mupt.getLoginType().equals(LoginType.NOPASSWD)) {
            //免密登录
            return true;
        }
        if (mupt.getLoginType().equals(LoginType.PASSWORD)) {
            //密码登录
            return super.doCredentialsMatch(token, info);
        } else {
            return false;
        }
    }
}

4.2.2 配置登录类型常量

package cn.fours.util;

//登录类型常量
public class LoginType {
   public static final String NOPASSWD = "NoPassword";
   public static final String PASSWORD = "Password";
}

4.2.3 在自定义身份认证过滤器(MyAuthenticationFilter )新增方法

//薪增方法
   @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);
   }
}

4.2.4 重写UsernamePasswordToken(MyUsernamePasswordToken )

package cn.fours.util;

import org.apache.shiro.authc.UsernamePasswordToken;

public class MyUsernamePasswordToken extends UsernamePasswordToken {

   private String loginType;

   public MyUsernamePasswordToken() {
       super();
   }
   /**账号密码登录*/
   public MyUsernamePasswordToken(String username, String password, String loginType, boolean rememberMe, String host) {
       super(username, password, rememberMe, host);
       this.loginType = loginType;
   }

   /**免密登录*/
   public MyUsernamePasswordToken(String username) {
       super(username, "", false, null);
       this.loginType = LoginType.NOPASSWD;
   }
  //需要用户名和密码登录
   public MyUsernamePasswordToken(String username, String password) {
       super(username, password, false, null);
       this.loginType = LoginType.PASSWORD;
   }

   public String getLoginType() {
       return loginType;
   }

   public void setLoginType(String loginType) {
       this.loginType = loginType;
   }
}

==4.3 bind前端页面 ==





4.4 register前端页面





4.5 mianmi前端页面(这个页面只是我免密登录时需要携带Token数据和用户名,所以相当于一个跳板,在页面加载之前执行方法跳转到主页),这里不建议这样做,很不安全,地址完全暴露,但是实在是没有想到其它好的办法





到这里基本的配置和代码详解已经完成了,希望对大家有所帮助!

你可能感兴趣的:(Ssm+vue+ElementUI实现微信第三方登录)