springBoot securiyt实现微信登录

 

从这里开始

4. maven依赖


      commons-httpclient
      commons-httpclient
      3.0.1
  


  
       org.springframework.boot
       spring-boot-devtools
  

  
        org.apache.commons
        commons-io
        1.3.2


 
        org.apache.commons
        commons-lang3
        3.4
 


 
      org.apache.httpcomponents
        httpclient
        4.3.2
   


   
        com.alibaba
        fastjson
        1.2.38
   


5.在application.yml文件中配置你的

#第三方微信登录(用你自己的)
#appID App的ID
#appSecret
weixinconfig:
weixinappID: wxf7865421a3c4d5f
weixinappSecret: 6cdbe6d4ce6sbcf0593c913d8a0ce12
创建配置类

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix="weixinconfig")
public class WeixinLoginProperties {
    
    private String weixinappID; // 商户appid
    
    private String weixinappSecret; // 私钥 pkcs8格式的

    public String getWeixinappID() {
        return weixinappID;
    }

    public void setWeixinappID(String weixinappID) {
        this.weixinappID = weixinappID;
    }

    public String getWeixinappSecret() {
        return weixinappSecret;
    }

    public void setWeixinappSecret(String weixinappSecret) {
        this.weixinappSecret = weixinappSecret;
    }

}


6.第一步:请求CODE

这一步客户端会把code传过来 ,不用你操心
7.第二步:通过code获取access_token

package io.renren.api.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import io.renren.api.dao.TpAccesstokenMapper;
import io.renren.api.dao.TpUsersMapper;
import io.renren.api.entity.TpAccesstoken;
import io.renren.api.entity.TpAccesstokenExample;
import io.renren.api.entity.TpAccumulativeAward;
import io.renren.api.entity.TpUsers;
import io.renren.api.entity.TpUsersExample;
import io.renren.api.properties.WeixinLoginProperties;
import io.renren.api.service.TpAccesstokenService;
import io.renren.api.service.TpAccumulativeAwardService;
import io.renren.api.service.TpUsersService;
import io.renren.common.utils.R;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import javax.annotation.Resource;
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.impl.client.DefaultHttpClient;

/**
 * 第三方微信登录
 * @author Administrator
 *
 */
@SuppressWarnings("deprecation")
@Controller
@RequestMapping("/api")
public class WeXinController {
    
    
    //微信公众平台申请
    //应用唯一标识,在微信开放平台提交应用审核通过后获得 appID
    //应用密钥AppSecret,在微信开放平台提交应用审核通过后获得 appSecret
    //TpAccesstoken 用来保存微信返回的用户信息oppid等
    
    
    @Resource
    private WeixinLoginProperties weixinLoginProperties;
    @Autowired
    TpUsersService tpUsersService;    
    @Autowired
    TpUsersMapper tpUsersMapper;
    @Autowired
    TpAccesstokenService tpAccesstokenService;    
    @Autowired
    TpAccesstokenMapper tpAccesstokenMapper;
    @Autowired
    TpAccumulativeAwardService tpAccumulativeAwardService;

    /**
     * 获取accessToken,该步骤返回的accessToken期限为一个月
     *
     * @param code
     * @return
     * @throws Exception
     */
    @SuppressWarnings("all")
    @RequestMapping("weixincallback")
    @ResponseBody
    public R getAccessToken(String code) throws Exception {
        
        String appID = weixinLoginProperties.getWeixinappID();
        String appSecret = weixinLoginProperties.getWeixinappSecret();
        String accesstoken;
        String openid = null;
        String refreshtoken;
        int expiresIn;
        String unionid;//可通过获取用户基本信息中的unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,
        //用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。
        if (code != null) {
            System.out.println(code);
        }
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appID+"&secret="+appSecret+"&code="+code+"&grant_type=authorization_code";
        URI uri = URI.create(url);
        org.apache.http.client.HttpClient client = new DefaultHttpClient();
        HttpGet get = new HttpGet(uri);
        HttpResponse response;
        try {
            response = client.execute(get);
            if (response.getStatusLine().getStatusCode() == 200) {
                HttpEntity entity = response.getEntity();

                BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
                StringBuilder sb = new StringBuilder();

                for (String temp = reader.readLine(); temp != null; temp = reader.readLine()) {
                    sb.append(temp);
                }
                JSONObject object = new JSONObject(sb.toString().trim());
                System.out.println("object:"+object);
                accesstoken = object.getString("access_token");
                System.out.println("accesstoken:"+accesstoken);
                openid = object.getString("openid");
                System.out.println("openid:"+openid);
                refreshtoken = object.getString("refresh_token");
                System.out.println("refreshtoken:"+refreshtoken);
                expiresIn = (int) object.getLong("expires_in");
                unionid = object.getString("unionid");
                // 将用户信息保存到数据库
                //1.先查询用户是否是第一次第三方登录如果是第一次那么是将用户信息添加到数据库 如果不是那么是更新到数据库
                TpUsers userInfo = getUserInfo(accesstoken,openid);
                Integer userId = userInfo.getUserId();
                
                TpAccesstokenExample example = new TpAccesstokenExample();
                example.createCriteria().andOpenidEqualTo(openid);
                List list = tpAccesstokenMapper.selectByExample(example);
                if(list!=null&&list.size()>0) {
                    //那么该用户不是第一次 执行更新操作
                    TpAccesstoken tpAccesstoken = list.get(0);
                    tpAccesstoken.setAccesstoken(accesstoken);
                    tpAccesstoken.setUserId(userId);
                    tpAccesstoken.setExpiresIn(expiresIn);
                    tpAccesstoken.setOpenid(openid);
                    tpAccesstoken.setRefreshtoken(refreshtoken);
                    tpAccesstokenService.save(tpAccesstoken);
                }else {
                    TpAccesstoken tpAccesstoken=new TpAccesstoken();
                    tpAccesstoken.setUserId(userId);
                    tpAccesstoken.setAccesstoken(accesstoken);
                    tpAccesstoken.setExpiresIn(expiresIn);
                    tpAccesstoken.setOpenid(openid);
                    tpAccesstoken.setRefreshtoken(refreshtoken);
                    tpAccesstokenService.save(tpAccesstoken);
                    //tpAccesstokenService.insertAccesstoken(userId,openid, accesstoken, expiresIn, refreshtoken);
                }
                //refreshAccessToken(openid);
                System.out.println("Openid"+userInfo.getOpenid());
                return R.ok().put("userInfo", userInfo).put("openid", openid);
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }

        return R.ok().put("openid", openid);
    }
        /*
         *
         * 1 { 2 "access_token":"ACCESS_TOKEN", 3 "expires_in":7200, 4
         * "refresh_token":"REFRESH_TOKEN", 5 "openid":"OPENID", 6 "scope":"SCOPE", 7
         * "unionid":"o6_bmasdasdsad6_2sgVt7hMZOPfL" 8 } 复制代码 复制代码 参数 说明 access_token
         * 接口调用凭证 expires_in access_token 接口调用凭证超时时间,单位(秒) refresh_token
         * 用户刷新access_token openid 授权用户唯一标识 scope 用户授权的作用域,使用逗号(,)分隔 unionid
         * 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
         *
         */

    
    /**
     * 刷新token
     *
     * @param openID
     * @return
     */
    @SuppressWarnings({ "unused", "resource" })
    private void refreshAccessToken(String openid) {
        String refreshtoken=null;
        TpAccesstoken tpAccesstoken=new TpAccesstoken();
        String appID = weixinLoginProperties.getWeixinappID();
        String appSecret = weixinLoginProperties.getWeixinappSecret();
        TpAccesstokenExample example = new TpAccesstokenExample();
        example.createCriteria().andOpenidEqualTo(openid);
        List list = tpAccesstokenMapper.selectByExample(example);
        if(list!=null&&list.size()>0) {
             tpAccesstoken = list.get(0);
             refreshtoken = tpAccesstoken.getRefreshtoken();
        }
        
String uri = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid="+appID+"&grant_type=refresh_token&refresh_token="+refreshtoken;
        org.apache.http.client.HttpClient client = new DefaultHttpClient();
        HttpGet get = new HttpGet(URI.create(uri));
        try {
            HttpResponse response = client.execute(get);
            if (response.getStatusLine().getStatusCode() == 200) {
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
                StringBuilder builder = new StringBuilder();
                for (String temp = reader.readLine(); temp != null; temp = reader.readLine()) {
                    builder.append(temp);
                }
                JSONObject object = new JSONObject(builder.toString().trim());
                String    accessToken = object.getString("access_token");
                String    refreshToken = object.getString("refresh_token");
                openid = object.getString("openid");
                int   expires_in = (int) object.getLong("expires_in");
                tpAccesstoken.setAccesstoken(accessToken);
                tpAccesstoken.setExpiresIn(expires_in);
                tpAccesstoken.setOpenid(openid);
                tpAccesstoken.setRefreshtoken(refreshToken);
                tpAccesstokenService.save(tpAccesstoken);
            }
        } catch (ClientProtocolException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
    }

    /**
     * 根据accessToken获取用户信息
     *
     * @param accessToken
     * @param openID
     * @return
     * @throws Exception
     */
    @SuppressWarnings({ "unused", "resource" })
    public TpUsers getUserInfo(String accessToken, String openID) throws Exception {
        String appID = weixinLoginProperties.getWeixinappID();
        String appSecret = weixinLoginProperties.getWeixinappSecret();
        String uri = "https://api.weixin.qq.com/sns/userinfo?access_token=" + accessToken + "&openid=" + openID;
        org.apache.http.client.HttpClient client = new DefaultHttpClient();
        HttpGet get = new HttpGet(URI.create(uri));
        try {
            HttpResponse response = client.execute(get);
            if (response.getStatusLine().getStatusCode() == 200) {
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
                StringBuilder builder = new StringBuilder();
                for (String temp = reader.readLine(); temp != null; temp = reader.readLine()) {
                    builder.append(temp);
                }
                JSONObject object = new JSONObject(builder.toString().trim());
                
                String country = object.getString("country");
                String nikeName = object.getString("nickname");
                String unionid = object.getString("unionid");
                String province = object.getString("province");
                String city = object.getString("city");
                String openid = object.getString("openid");
                String sex = object.getString("sex");
                String headimgurl = object.getString("headimgurl");
                String language = object.getString("language");
                BigDecimal bigDecimal=new BigDecimal(0.0);
                TpUsersExample example=new TpUsersExample();
                example.createCriteria().andOpenidEqualTo(openid);
                List list = tpUsersMapper.selectByExample(example);
                if(list!=null&&list.size()>0) {
                    TpUsers tpUsers = list.get(0);
                    System.out.println("---------");
    
                    return tpUsers;
                    
                }else {
                    TpUsers tpUsers=new TpUsers();
                    tpUsers.setOauth("wx");
                    tpUsers.setOpenid(openid);
                    tpUsers.setUnionid(unionid);
                    tpUsers.setUserName(nikeName);
                    tpUsers.setUserMoney(bigDecimal);
                    tpUsersService.save(tpUsers);
                    System.out.println("+++++++");
                    return tpUsers;
                }
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }

        return null;
    }
    
    @RequestMapping("/isaccesstoken")
    @SuppressWarnings({ "resource" })
    private boolean isAccessTokenIsInvalid(String accessToken,String openID) {
        String url = "https://api.weixin.qq.com/sns/auth?access_token=" + accessToken + "&openid=" + openID;
        URI uri = URI.create(url);
        org.apache.http.client.HttpClient client = new DefaultHttpClient();
        HttpGet get = new HttpGet(uri);
        HttpResponse response;
        try {
            response = client.execute(get);
            if (response.getStatusLine().getStatusCode() == 200) {
                HttpEntity entity = response.getEntity();

                BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), "UTF-8"));
                StringBuilder sb = new StringBuilder();

                for (String temp = reader.readLine(); temp != null; temp = reader.readLine()) {
                    sb.append(temp);
                }
                JSONObject object = new JSONObject(sb.toString().trim());
                  /* {
                    "errcode":0,"errmsg":"ok"
                    }
                    错误的Json返回示例:
                    {
                    "errcode":40003,"errmsg":"invalid openid"
                    }*/
                int errorCode = object.getInt("errcode");
                if (errorCode == 0) {
                    return true;
                }
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return false;
        
    }


}

获取第一步的code后,请求以下链接进行refresh_token:

https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN


  参数说明
    
    参数    是否必须    说明
    appid    是    应用唯一标识
    grant_type    是    填refresh_token
    refresh_token    是    填写通过access_token获取到的refresh_token参数
    返回说明
    
    正确的返回:
    
    {
    "access_token":"ACCESS_TOKEN",
    "expires_in":7200,
    "refresh_token":"REFRESH_TOKEN",
    "openid":"OPENID",
    "scope":"SCOPE"
    }
    参数    说明
    access_token    接口调用凭证
    expires_in    access_token接口调用凭证超时时间,单位(秒)
    refresh_token    用户刷新access_token
    openid    授权用户唯一标识
    scope    用户授权的作用域,使用逗号(,)分隔
    错误返回样例:
    
    {"errcode":40030,"errmsg":"invalid refresh_token"}

刷新或续期access_token使用

接口说明

access_token是调用授权关系接口的调用凭证,由于access_token有效期(目前为2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新,access_token刷新结果有两种:

1.若access_token已超时,那么进行refresh_token会获取一个新的access_token,新的超时时间;

2.若access_token未超时,那么进行refresh_token不会改变access_token,但超时时间会刷新,相当于续期access_token。

refresh_token拥有较长的有效期(30天)且无法续期,当refresh_token失效的后,需要用户重新授权后才可以继续获取用户头像昵称。

请求方法

使用/sns/oauth2/access_token接口获取到的refresh_token进行以下接口调用:

http请求方式: GET
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
参数说明

参数    是否必须    说明
appid    是    应用唯一标识
grant_type    是    填refresh_token
refresh_token    是    填写通过access_token获取到的refresh_token参数
返回说明

正确的返回:

{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE"
}
参数    说明
access_token    接口调用凭证
expires_in    access_token接口调用凭证超时时间,单位(秒)
refresh_token    用户刷新access_token
openid    授权用户唯一标识
scope    用户授权的作用域,使用逗号(,)分隔
错误返回样例:

{
"errcode":40030,"errmsg":"invalid refresh_token"
}

获取用户个人信息(UnionID机制)

接口说明

此接口用于获取用户个人信息。开发者可通过OpenID来获取用户基本信息。特别需要注意的是,如果开发者拥有多个移动应用、网站应用和公众帐号,可通过获取用户基本信息中的unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号,用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。请注意,在用户修改微信头像后,旧的微信头像URL将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像URL失效后的异常情况。

请求说明

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
参数说明

参数    是否必须    说明
access_token    是    调用凭证
openid    是    普通用户的标识,对当前开发者帐号唯一
lang    否    国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为zh-CN
返回说明

正确的Json返回结果:

{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
参数    说明
openid    普通用户的标识,对当前开发者帐号唯一
nickname    普通用户昵称
sex    普通用户性别,1为男性,2为女性
province    普通用户个人资料填写的省份
city    普通用户个人资料填写的城市
country    国家,如中国为CN
headimgurl    用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege    用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid    用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
建议:

开发者最好保存unionID信息,以便以后在不同应用之间进行用户信息互通。

错误的Json返回示例:

{
"errcode":40003,"errmsg":"invalid openid"
}

工具类

package io.renren.common.utils;

import java.util.HashMap;
import java.util.Map;

/**
 * 返回数据
 *
 * @author chenshun
 * @email [email protected]
 * @date 2016年10月27日 下午9:59:27
 */
public class R extends HashMap {
    private static final long serialVersionUID = 1L;
    
    public R() {
        put("code", 0);
        put("msg", "success");
    }
    
    public static R error() {
        return error(500, "未知异常,请联系管理员");
    }
    
    public static R error(String msg) {
        return error(500, msg);
    }
    
    public static R error(int code, String msg) {
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put("msg", msg);
        return r;
    }
    
    public static R ok(Map map) {
        R r = new R();
        r.putAll(map);
        return r;
    }
    
    public static R ok() {
        return new R();
    }

    @Override
    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}
————————————————

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

前面的步骤主要是为了拿到微信给的授权id和密钥
后端开发步骤:
1.配置文件application.properties

# 微信开放平台 appid
wx.open.app_id=你的appid
# 微信开放平台 appsecret
wx.open.app_secret=你的appsecret
# 微信开放平台 重定向url
wx.open.redirect_url=http://你的服务器名称/api/ucenter/wx/callback
1
2
3
4
5
6
2.创建一个工具类读取文件

package com.qiu.educenter.utils;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ConstantWxUtils implements InitializingBean {
    @Value("${wx.open.app_id}")
    private String appId;

    @Value("${wx.open.app_secret}")
    private String appSecret;

    @Value("${wx.open.redirect_url}")
    private String redirectUrl;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;
    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3.生成一个微信扫描的二维码
这里微信给了一个固定的地址

官方文档
i.请求code
第三方使用网站应用授权登录前请注意已获取相应网页授权作用域 (scope=snsapi_login),则可以通过在PC端打开以下链接: https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 若提示“该链接无法访问”,请检查参数是否填写错误,如redirect_uri的域名与审核时填写的授权域名不一致或scope不为snsapi_login。`

参数说明

参数    是否必须    说明
appid    是    应用唯一标识
redirect_uri    是    请使用urlEncode对链接进行处理
response_type    是    填code
scope    是    应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login
state    否    用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验
做法
直接请求微信提供固定的地址,向地址后面拼接参数
项目中新建一个controller

package com.qiu.educenter.controller;

import com.qiu.educenter.utils.ConstantWxUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;

@Controller
@CrossOrigin
@RequestMapping("/api/ucenter/wx")
public class WxApiController {
    @GetMapping("callback")
    //这种规则只是为了测试,在实际开发中并不需要这么做
    public String callback(String code,String state){
        System.out.println(code);
        return "redirect:http://localhost:3000";
    }
    //1.生成微信扫描二维码
    @GetMapping("login") //直接生成二维码,不返回数据
    public String getWxCode(){
        /*第一种方式:固定地址后面拼参数,参数太多,容易拼错
        String url = "https://open.weixin.qq.com/connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID
            +"&response_type=code";
         */
        //第二种方式:%s:相当于一个占位符
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";

        //对redirect_url进行URLEnccode编码
        String redirect_url =ConstantWxUtils.WX_OPEN_REDIRECT_URL;
        try {
            redirect_url = URLEncoder.encode(redirect_url, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        String url =String.format(
                baseUrl,
                ConstantWxUtils.WX_OPEN_APP_ID,
                ConstantWxUtils.WX_OPEN_APP_SECRET,
                redirect_url,
                "qiuzhikang"
        );
        //请求微信地址
        return "redirect:"+url;
    }
}
扫描之后,执行了本地的callback方法.在calllback方法中获取到了两个值,在跳转的时候传来的.
第一步:

1.code:类似于手机验证码1,随机唯一的值
2.state:原样传递
1
2
第二步:拿着第一步获取到的code值请求微信提供固定的地址.可以获取到两个值,一个是accessToken另一个是openid.

1.accessToken:访问凭证
2.openid:区分不同微信的唯一标识
1
2
第三步:拿着第二步获取到的两个值access_token,openid再去请求微信给出的一个固定地址.最终才可以获取到微信扫码人的信息,比如说微信扫码人的昵称,头像等等.

多步骤也是为了从安全考虑.
我们再来看看这张时序图:

这个时候就能看明白很多了

这里用到了几个技术点:
httpclient:这个能做到不需要浏览器也能做到浏览器的效果
json转换工具:gson,jackjson等等

完整的controller:

@Controller
@CrossOrigin
@RequestMapping("/api/ucenter/wx")
public class WxApiController {

    @Autowired
    private UcenterMemberService memberService;
    @GetMapping("callback")
    //这种规则只是为了测试,在实际开发中并不需要这么做
    public String callback(String code,String state){
        try{
//1.获取code值,临时票据,类似于验证码
            System.out.println(code);

            //2.拿着code请求微信固定的地址,得到两个值access_token和openid
            String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                    "?appid=%s" +
                    "&secret=%s" +
                    "&code=%s" +
                    "&grant_type=authorization_code";

            String accessTokenUrl = String.format(baseAccessTokenUrl,
                    ConstantWxUtils.WX_OPEN_APP_ID,
                    ConstantWxUtils.WX_OPEN_APP_SECRET,
                    code);
            //请求这个拼接好的地址得到返回的值,现在请求地址,不用浏览器了,用httpclient发送一个请求得到一个返回的地址
            String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);//得到的是一个json的字符串,并不是前端需要的json数据
            //所以我们需要对字符串进行分割,先将字符串转换成map集合,map是key-value结构,再根据map中的key得到结果,这里我们用gson
            Gson gson = new Gson();
            HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);//将字符串转换成Hashmao
            String access_token =(String) mapAccessToken.get("access_token");
            String openid = (String)mapAccessToken.get("openid");

            //扫码人信息加到数据库中去
            //判断数据库中是否存在相同的数据库信息,我们可以根据openid来做判断
            UcenterMember member = memberService.getOpenIdMember(openid);
            if (member ==null){
                //member为空,表示数据库中没有相同的信息
                //3.拿到access_token和openid,再去请求微信提供固定的地址,获取到扫码人的信息
                String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                        "?access_token=%s" +
                        "&openid=%s";
                String userInfoUrl = String.format(
                        baseUserInfoUrl,
                        access_token,
                        openid
                );
                //发送请求
                String userInfo = HttpClientUtils.get(userInfoUrl);
                //获取返回的userInfo字符串的扫描人信息
                HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
                String nickname =(String) userInfoMap.get("nickname");//昵称
                String headimgurl =(String) userInfoMap.get("headimgurl");//头像

                member = new UcenterMember();
                member.setOpenid(openid);
                member.setNickname(nickname);
                member.setAvatar(headimgurl);
                memberService.save(member);
            }
            //最后返回我们的首页面
            //由于我们在前端中需要显示用户名和头像,所以我们需要使用jwt在地址中传递token
            //不能使用cookie,是因为cookie不能跨域访问,会导致值传递失败
            //使用jwt根据member对象生成token字符串
            String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
            
            return "redirect:http://localhost:3000?token="+jwtToken;
        }catch(Exception e){
           throw new QiuException(20001,"登录失败");
        }
    }
    //1.生成微信扫描二维码
    @GetMapping("login") //直接生成二维码,不返回数据
    public String getWxCode(){
        /*第一种方式:固定地址后面拼参数,参数太多,容易拼错
        String url = "https://open.weixin.qq.com/connect/qrconnect?appid="+ ConstantWxUtils.WX_OPEN_APP_ID
            +"&response_type=code";
         */
        //第二种方式:%s:相当于一个占位符
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";

        //对redirect_url进行URLEnccode编码
        String redirect_url =ConstantWxUtils.WX_OPEN_REDIRECT_URL;
        try {
            redirect_url = URLEncoder.encode(redirect_url, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        String url =String.format(
                baseUrl,
                ConstantWxUtils.WX_OPEN_APP_ID,
                ConstantWxUtils.WX_OPEN_APP_SECRET,
                redirect_url,
                "atguigu"
        );
        //请求微信地址
        return "redirect:"+url;
    }
}

service层:

/**
 *


 * 会员表 服务类
 *


 *
 * @author qiuzhikang
 * @since 2020-08-15
 */
public interface UcenterMemberService extends IService {

    String login(UcenterMember member);

    void register(RegisterVo registerVo);


    UcenterMember getOpenIdMember(String openid);
}


impl:

/**
 *


 * 会员表 服务实现类
 *


 *
 * @author qiuzhikang
 * @since 2020-08-15
 */
@Service
public class UcenterMemberServiceImpl extends ServiceImpl implements UcenterMemberService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public String login(UcenterMember member) {

        //原始方法就是用UcenterMember里面的用户名密码在数据库中做比对
        //现在用一种新的方式

        //获取手机号和密码
        String mobile = member.getMobile();
        String password = member.getPassword();
        //手机号和密码飞空判断
        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)){
            throw new QiuException(20001,"用户名密码不为空,登录失败");
        }
        //判断手机号是否正确
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        UcenterMember mobileMember = baseMapper.selectOne(wrapper);
        //判断查出来对象是否为空
        if (mobileMember == null){
            //没有这个手机号
            throw new QiuException(20001,"登录失败");
        }
        //判断密码是否正确
        if (!MD5.encrypt(password).equals(mobileMember.getPassword())){
            //密码不一样
            throw new QiuException(20001,"登录失败");
        }
        if (mobileMember.getIsDisabled()){
            //账号是否被禁用
            throw new QiuException(20001,"登录失败");
        }

        //表示登录成功了
        //生成token的字符串,使用jwt工具类
        String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(),mobileMember.getNickname());
        System.out.println(member.getId()+member.getNickname());
        return jwtToken;
    }
//注册的方法
    @Override
    public void register(RegisterVo registerVo) {
        //获取注册的数据
        String code = registerVo.getCode();

        String mobile = registerVo.getMobile();

        String nickname = registerVo.getNickname();

        String password = registerVo.getPassword();
        //非空判断
        if (StringUtils.isEmpty(code) ||StringUtils.isEmpty(mobile)
                ||StringUtils.isEmpty(nickname) ||StringUtils.isEmpty(password) ){
            throw new QiuException(20001,"注册失败,数据不能为空");
        }

        //判断验证码,与redis中存的验证码是否一样
        String redisCode = redisTemplate.opsForValue().get(mobile);
        System.out.println(redisCode);
        if (!code.equals(redisCode)){
            throw new QiuException(20001,"验证码输入不一致");
        }
        //判断手机号是否重复
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("mobile",mobile);
        Integer count = baseMapper.selectCount(wrapper);
        if (count>0){
            throw new QiuException(20001,"手机号已被使用");
        }
        //数据添加到数据库中
        UcenterMember ucenterMember = new UcenterMember();
        ucenterMember.setMobile(mobile);
        ucenterMember.setPassword(MD5.encrypt(password));
        ucenterMember.setNickname(nickname);
        ucenterMember.setIsDisabled(false);
        ucenterMember.setAvatar("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597470759261&di=92492bc30e5e16276723561870a3ed60&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201505%2F04%2F20150504013225_XZNCu.thumb.700_0.jpeg");
        baseMapper.insert(ucenterMember);

    }

    @Override
    public UcenterMember getOpenIdMember(String openid) {
        QueryWrapper wrapper = new QueryWrapper<>();
        wrapper.eq("openid",openid);
        UcenterMember member = baseMapper.selectOne(wrapper);


        return member;
    }
}


这里面需要使用到的两个工具类:
jwtUtils:

public class JwtUtils {

    public static final long EXPIRE = 1000 * 60 * 60 * 24;//设置token过期时间的值
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";//密钥,为了后面做加密的编码,这个密码是随便写的

    public static String getJwtToken(String id, String nickname){
        //构建JWT字符串,设置头部的信息,也就是JWT三部分中的第一部分
        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                //设置过期时间
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                //设置token的主题部分
                .claim("id", id)
                .claim("nickname", nickname)
                //签名哈希,
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();//得到字符串的主题部分
        return (String)claims.get("id");
    }
}


httpclient:

/**
 *  依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
 * @author zhaoyb
 *
 */
public class HttpClientUtils {

    public static final int connTimeout=10000;
    public static final int readTimeout=10000;
    public static final String charset="UTF-8";
    private static HttpClient client = null;

    static {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(128);
        cm.setDefaultMaxPerRoute(128);
        client = HttpClients.custom().setConnectionManager(cm).build();
    }

    public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
        return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
    }

    public static String postParameters(String url, Map params) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String postParameters(String url, Map params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {
        return postForm(url, params, null, connTimeout, readTimeout);
    }

    public static String get(String url) throws Exception {
        return get(url, charset, null, null);
    }

    public static String get(String url, String charset) throws Exception {
        return get(url, charset, connTimeout, readTimeout);
    }

    /**
     * 发送一个 Post 请求, 使用指定的字符集编码.
     *
     * @param url
     * @param body RequestBody
     * @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
     * @param charset 编码
     * @param connTimeout 建立链接超时时间,毫秒.
     * @param readTimeout 响应超时时间,毫秒.
     * @return ResponseBody, 使用指定的字符集编码.
     * @throws ConnectTimeoutException 建立链接超时异常
     * @throws SocketTimeoutException  响应超时
     * @throws Exception
     */
    public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
            throws ConnectTimeoutException, SocketTimeoutException, Exception {
        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        String result = "";
        try {
            if (StringUtils.isNotBlank(body)) {
                HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
                post.setEntity(entity);
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());

            HttpResponse res;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }


    /**
     * 提交form表单
     *
     * @param url
     * @param params
     * @param connTimeout
     * @param readTimeout
     * @return
     * @throws ConnectTimeoutException
     * @throws SocketTimeoutException
     * @throws Exception
     */
    public static String postForm(String url, Map params, Map headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
            SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpPost post = new HttpPost(url);
        try {
            if (params != null && !params.isEmpty()) {
                List formParams = new ArrayList();
                Set> entrySet = params.entrySet();
                for (Entry entry : entrySet) {
                    formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                }
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
                post.setEntity(entity);
            }

            if (headers != null && !headers.isEmpty()) {
                for (Entry entry : headers.entrySet()) {
                    post.addHeader(entry.getKey(), entry.getValue());
                }
            }
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            post.setConfig(customReqConf.build());
            HttpResponse res = null;
            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(post);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(post);
            }
            return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
        } finally {
            post.releaseConnection();
            if (url.startsWith("https") && client != null
                    && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
    }


    /**
     * 发送一个 GET 请求
     *
     * @param url
     * @param charset
     * @param connTimeout  建立链接超时时间,毫秒.
     * @param readTimeout  响应超时时间,毫秒.
     * @return
     * @throws ConnectTimeoutException   建立链接超时
     * @throws SocketTimeoutException   响应超时
     * @throws Exception
     */
    public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
            throws ConnectTimeoutException,SocketTimeoutException, Exception {

        HttpClient client = null;
        HttpGet get = new HttpGet(url);
        String result = "";
        try {
            // 设置参数
            Builder customReqConf = RequestConfig.custom();
            if (connTimeout != null) {
                customReqConf.setConnectTimeout(connTimeout);
            }
            if (readTimeout != null) {
                customReqConf.setSocketTimeout(readTimeout);
            }
            get.setConfig(customReqConf.build());

            HttpResponse res = null;

            if (url.startsWith("https")) {
                // 执行 Https 请求.
                client = createSSLInsecureClient();
                res = client.execute(get);
            } else {
                // 执行 Http 请求.
                client = HttpClientUtils.client;
                res = client.execute(get);
            }

            result = IOUtils.toString(res.getEntity().getContent(), charset);
        } finally {
            get.releaseConnection();
            if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
                ((CloseableHttpClient) client).close();
            }
        }
        return result;
    }


    /**
     * 从 response 里获取 charset
     *
     * @param ressponse
     * @return
     */
    @SuppressWarnings("unused")
    private static String getCharsetFromResponse(HttpResponse ressponse) {
        // Content-Type:text/html; charset=GBK
        if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
            String contentType = ressponse.getEntity().getContentType().getValue();
            if (contentType.contains("charset=")) {
                return contentType.substring(contentType.indexOf("charset=") + 8);
            }
        }
        return null;
    }

    /**
     * 创建 SSL连接
     * @return
     * @throws GeneralSecurityException
     */
    private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
                    return true;
                }
            }).build();

            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }

                @Override
                public void verify(String host, SSLSocket ssl)
                        throws IOException {
                }

                @Override
                public void verify(String host, X509Certificate cert)
                        throws SSLException {
                }

                @Override
                public void verify(String host, String[] cns,
                                   String[] subjectAlts) throws SSLException {
                }

            });

            return HttpClients.custom().setSSLSocketFactory(sslsf).build();

        } catch (GeneralSecurityException e) {
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
            //String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
            /*Map map = new HashMap();
            map.put("name", "111");
            map.put("page", "222");
            String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
            System.out.println(str);
        } catch (ConnectTimeoutException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SocketTimeoutException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

spring security 实现微信登录

第一部分

首先,感谢https://github.com/liyiorg/weixin-popular,使用了一些关于微信的代码,其次,参考了网上一些springboot springsecurity oauth2 登录的文章。

自己的代码,自己测试过,微信登录木有问题。配置一下自己的appid和secret就可以使用,如果有问题,可以给我留言。

github:https://github.com/luotuo/springboot-security-wechat

MyOAuth2RestTemplate这个类也改了很多,因为微信的登录基本不怎么按照套路出牌,各种在改参数,所以代码里面也只能改改改。。。
欢迎大家fork、star、pr
也欢迎留言
这个文件很重要

@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomUserDetailsService customUserDetailsService;
 
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    @Autowired
    private OAuth2ClientContext oauth2ClientContext;
 
    @Resource
    private UserService userService;
    @Resource
    private UserWechatService userWechatService;
    @Resource
    private UserPrivilegeService userPrivilegeService;
    @Resource
    private PrivilegeConfigService privilegeConfigService;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //任何访问都必须授权
                .anyRequest().fullyAuthenticated()
                //配置那些路径可以不用权限访问
                .mvcMatchers("/login", "/login/wechat").permitAll()
                .and()
                .formLogin()
                //登陆成功后的处理,因为是API的形式所以不用跳转页面
                .successHandler(new MyAuthenticationSuccessHandler())
                //登陆失败后的处理
                .failureHandler(new MySimpleUrlAuthenticationFailureHandler())
                .and()
                //登出后的处理
                .logout().logoutSuccessHandler(new RestLogoutSuccessHandler())
                .and()
                //认证不通过后的处理
                .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint());
        http.addFilterAt(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
        http.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
        //http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        http.csrf().disable();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }
 
 
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder(){
        //密码加密
        //return new BCryptPasswordEncoder(16);
        return new MyPasswordEncoder("2");
    }
 
    /**
     * 登出成功后的处理
     */
    public static class RestLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
 
        @Override
        public void onLogoutSuccess(HttpServletRequest request,
                                    HttpServletResponse response, Authentication authentication)
                throws IOException, ServletException {
            //Do nothing!
        }
    }
 
    /**
     * 权限不通过的处理
     */
    public static class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
                    "Authentication Failed: " + authException.getMessage());
        }
    }
 
    @Bean
    public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }
 
    @Bean
    public Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List filters = new ArrayList<>();
        filters.add(ssoFilter(wechat(), "/login/wechat"));
        filter.setFilters(filters);
        return filter;
    }
 
    @Bean
    public Filter ssoFilter(ClientResources client, String path) {
        MappingJackson2HttpMessageConverter customJsonMessageConverter = new
                MappingJackson2HttpMessageConverter();
        customJsonMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN));
        MyOAuth2ClientAuthenticationProcessingFilter filter = new MyOAuth2ClientAuthenticationProcessingFilter(path);
        filter.setAllowSessionCreation(true);
        MyOAuth2RestTemplate template = new MyOAuth2RestTemplate(client.getClient(), oauth2ClientContext);
        template.setMessageConverters(Arrays.asList(customJsonMessageConverter));
        filter.setRestTemplate(template);
        MyUserInfoTokenServices tokenServices = new MyUserInfoTokenServices(client.getResource().getUserInfoUri(),
                client.getClient().getClientId(),
                userService,
                userWechatService,
                userPrivilegeService,
                privilegeConfigService);
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(tokenServices);
        return filter;
    }
 
    @Bean
    @ConfigurationProperties(prefix = "wechat")
    public ClientResources wechat() {
        return new ClientResources();
    }
}
 
class ClientResources {
 
    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
 
    @NestedConfigurationProperty
    private ResourceServerProperties resource = new ResourceServerProperties();
 
    public AuthorizationCodeResourceDetails getClient() {
        return client;
    }
 
    public ResourceServerProperties getResource() {
        return resource;
    }
}

第二部分


上一章讲解了如何用QQ实现登录,这一回讲解的是用微信实现登录。

实现功能
实现微信登录。

开发步骤
引入jar包
jar包的引入和QQ引入的jar包一致。

微信实体信息
/**
 * @author lvhaibao
 * @description
 * @date 2019/1/4 0004 9:46
 */
@Data
public class WeixinUserInfo {

    /**
     * 普通用户的标识,对当前开发者帐号唯一
     */
    private String openid;
    /**
     * 普通用户昵称
     */
    private String nickname;
    /**
     * 语言
     */
    private String language;
    /**
     * 普通用户性别,1为男性,2为女性
     */
    private String sex;
    /**
     * 普通用户个人资料填写的省份
     */
    private String province;
    /**
     * 普通用户个人资料填写的城市
     */
    private String city;
    /**
     * 国家,如中国为CN
     */
    private String country;
    /**
     * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
     */
    private String headimgurl;
    /**
     * 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
     */
    private String[] privilege;
    /**
     * 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
     */
    private String unionid;

}
获取微信用户的api接口和实现

public interface Weixin {

    WeixinUserInfo getUserInfo(String openId);
}



public class WeixinImpl  extends AbstractOAuth2ApiBinding implements Weixin  {


    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 获取用户信息的url
     */
    private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";


    /**
     * @param accessToken
     */
    public WeixinImpl(String accessToken) {
        super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
    }

    /**
     * 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
     */
    @Override
    protected List> getMessageConverters() {
        List> messageConverters = super.getMessageConverters();
        messageConverters.remove(0);
        messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return messageConverters;
    }

    /**
     * 获取微信用户信息。
     */
    @Override
    public WeixinUserInfo getUserInfo(String openId) {
        String url = URL_GET_USER_INFO + openId;
        String response = getRestTemplate().getForObject(url, String.class);
        if(StringUtils.contains(response, "errcode")) {
            return null;
        }
        WeixinUserInfo profile = null;
        try {
            profile = objectMapper.readValue(response, WeixinUserInfo.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return profile;
    }
}
重写AccessGrant
因为微信和QQ不同,在获取token的同时,微信也返回了openId。因此要重写AccessGrant。

/**
 * 微信的access_token信息。与标准OAuth2协议不同,微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务
 * 所以在这里继承了标准AccessGrant,添加了openId字段,作为对微信access_token信息的封装。
 *

 */
@Data
public class WeixinAccessGrant extends AccessGrant {

    private static final long serialVersionUID = -7243374526633186782L;

    private String openId;

    public WeixinAccessGrant() {
        super("");
    }

    public WeixinAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
        super(accessToken, scope, refreshToken, expiresIn);
    }
}
重写OAuth2Template

@Slf4j
public class WeixinOAuth2Template extends OAuth2Template {

    private String clientId;

    private String clientSecret;

    private String accessTokenUrl;

    private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

    public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
        setUseParametersForClientAuthentication(true);
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.accessTokenUrl = accessTokenUrl;
    }

    /**
     * 获取access_token
     *
     * @param authorizationCode
     * @param redirectUri
     * @param parameters
     * @return
     */
    @Override
    public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,
                                         MultiValueMap parameters) {

        StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);

        accessTokenRequestUrl.append("?appid=" + clientId);
        accessTokenRequestUrl.append("&secret=" + clientSecret);
        accessTokenRequestUrl.append("&code=" + authorizationCode);
        accessTokenRequestUrl.append("&grant_type=authorization_code");
        accessTokenRequestUrl.append("&redirect_uri=" + redirectUri);

        return getAccessToken(accessTokenRequestUrl);
    }

    @Override
    public AccessGrant refreshAccess(String refreshToken, MultiValueMap additionalParameters) {

        StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);

        refreshTokenUrl.append("?appid=" + clientId);
        refreshTokenUrl.append("&grant_type=refresh_token");
        refreshTokenUrl.append("&refresh_token=" + refreshToken);

        return getAccessToken(refreshTokenUrl);
    }

    @SuppressWarnings("unchecked")
    private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {

        log.info("获取access_token, 请求URL: " + accessTokenRequestUrl.toString());

        //发送获取token
        String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);

        log.info("获取access_token, 响应内容: " + response);

        Map result = null;
        try {
            result = new ObjectMapper().readValue(response, Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //返回错误码时直接返回空
        if (StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))) {
            String errcode = MapUtils.getString(result, "errcode");
            String errmsg = MapUtils.getString(result, "errmsg");
            throw new RuntimeException("获取access token失败, errcode:" + errcode + ", errmsg:" + errmsg);
        }

        //获取token的时候,会返回openid,保存
        WeixinAccessGrant accessToken = new WeixinAccessGrant(
                MapUtils.getString(result, "access_token"),
                MapUtils.getString(result, "scope"),
                MapUtils.getString(result, "refresh_token"),
                MapUtils.getLong(result, "expires_in"));

        accessToken.setOpenId(MapUtils.getString(result, "openid"));

        return accessToken;
    }

    /**
     * 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
     */
    @Override
    public String buildAuthenticateUrl(OAuth2Parameters parameters) {
        String url = super.buildAuthenticateUrl(parameters);
        url = url + "&appid=" + clientId + "&scope=snsapi_login";
        return url;
    }

    @Override
    public String buildAuthorizeUrl(OAuth2Parameters parameters) {
        return buildAuthenticateUrl(parameters);
    }

    /**
     * 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
     */
    @Override
    protected RestTemplate createRestTemplate() {
        RestTemplate restTemplate = super.createRestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}
编写自己的WeixinServiceProvider
/**
 * 微信的OAuth2流程处理器的提供器,供spring social的connect体系调用
 *
 * @author lvhaibao
 * @description
 * @date 2019/1/4 0004 10:02
 */
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider {

    /**
     * 微信获取授权码的url
     */
    private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
    /**
     * 微信获取accessToken的url
     */
    private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * @param appId
     * @param appSecret
     */
    public WeixinServiceProvider(String appId, String appSecret) {
        super(new WeixinOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
    }


    @Override
    public Weixin getApi(String accessToken) {
        return new WeixinImpl(accessToken);
    }
}
编写自己的WeixinAdapter
/**
 * 微信 api适配器,将微信 api的数据模型转为spring social的标准模型。
 *
 * @author lvhaibao
 * @description
 * @date 2019/1/4 0004 9:56
 */
public class WeixinAdapter implements ApiAdapter {

    private String openId;

    public WeixinAdapter() {}

    public WeixinAdapter(String openId){
        this.openId = openId;
    }
    /**
     * @param api
     * @return
     */
    @Override
    public boolean test(Weixin api) {
        return true;
    }

    /**
     * @param api
     * @param values
     */
    @Override
    public void setConnectionValues(Weixin api, ConnectionValues values) {
        WeixinUserInfo profile = api.getUserInfo(openId);
        values.setProviderUserId(profile.getOpenid());
        values.setDisplayName(profile.getNickname());
        values.setImageUrl(profile.getHeadimgurl());
    }

    /**
     * @param api
     * @return
     */
    @Override
    public UserProfile fetchUserProfile(Weixin api) {
        return null;
    }

    /**
     * @param api
     * @param message
     */
    @Override
    public void updateStatus(Weixin api, String message) {
        //do nothing
    }
}
1
2
3
4
5
6
微信连接工厂ConnectionFactory
/**
 * @author lvhaibao
 * @description 创建连接工厂
 * @date 2019/1/4 0004 9:59
 */
public class WeixinConnectionFactory extends OAuth2ConnectionFactory {

    /**
     * @param appId
     * @param appSecret
     */
    public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
        super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
    }

    /**
     * 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
     */
    @Override
    protected String extractProviderUserId(AccessGrant accessGrant) {
        if (accessGrant instanceof WeixinAccessGrant) {
            return ((WeixinAccessGrant) accessGrant).getOpenId();
        }
        return null;
    }

    @Override
    public Connection createConnection(AccessGrant accessGrant) {
        return new OAuth2Connection(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
                accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
    }

    @Override
    public Connection createConnection(ConnectionData data) {
        return new OAuth2Connection(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
    }

    private ApiAdapter getApiAdapter(String providerUserId) {
        return new WeixinAdapter(providerUserId);
    }

    private OAuth2ServiceProvider getOAuth2ServiceProvider() {
        return (OAuth2ServiceProvider) getServiceProvider();
    }
}
自定义微信的服务提供商Id
/**
 * @author lvhaibao
 * @description 自定义微信的服务提供商ID
 * @date 2019/1/4 0004 9:47
 */
@Data
public class WeixinProperties extends SocialProperties {

    private String providerId = "weixin";

}
编写配置applicaion.yml
system:
#客户端配置
  social:
    filterProcessesUrl: /qqLogin
    weixin:
      app-id: wx8a47a66e22296c62
      app-secret: deb57af7ec1753a2668889e74b34b789

页面
页面中添加这个链接就好。

微信登录
1
还有其他的配置在上一章中已经写好。读者可自行查阅或者查看项目源码。

测试
和QQ登录一样。这里就不再进行叙述。

项目源码
https://gitee.com/lvhaibao/spring-lhbauth/tree/42327d841a8d606bf5b5167c7ecabe72040ec735/

你可能感兴趣的:(学习笔记)