一.框架、工具
spring+springMvc+idea
二.具体实现
1.首先需要了解一下微信提供的API开发文档
2.目的:授权后可获取用户基本信息,进而实现业务逻辑
3.需要的配置(参看API):
A.需要一个测试账号(用自己微信个人账号可申请注册),测试账号具备普通账号所没有的很多的功能,包括网页授权、分享等,便于测试。
或者是公司服务器用的是高级的服务号。
B.服务号登陆:在开发——》基本配置 配置URL,token,EncodingAESKey(测试号登陆:直接配置URL,token提交通过即可)
URL:项目中用来接收微信消息和事件的接口URL,即当填写完成提交之后,会向这个url发送请求,附带参数,校验成功后表示接入成功,成为开发者,否则成为开发者失败 (eg: http://本机域名/项目名称/接收消息的servlet)
token:可任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性),所以代码中的token要于此相同
EncodingAESKey(服务号有,测试号没有):随机生成
上述填写好之后就可以提交并启用
C.服务号登陆:在设置——》公众号设置 中设置js接口安全域名,设置后缀即可(如hengxin.com)(若测试号登陆:js接口安全域名处增加服务号下配置的并指向本机的域名(eg:wx.test.hengxin.com))
D.授权需要的配置:在接口权限的网页账号的网页授权获取用户基本信息处修改授权回调页面域名(填写本机的域名即可eg:wx.test.hengxin.com)
3.具体代码:
用于接收微信发送请求的Servlet
public class CoreServlet extends HttpServlet {
private static Logger LOGGER = LoggerFactory.getLogger(CoreServlet.class);
private static final long serialVersionUID = 4440739483644821986L;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
/**
* 确认请求来自微信服务器
*/
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 微信加密签名
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echostr = request.getParameter("echostr");
PrintWriter out = response.getWriter();
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
out.print(echostr);
}
out.close();
out = null;
}
util类 SignUtil
public class SignUtil {
// 与接口配置信息中的Token要一致
private static String token="xxxx";
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[] { token, timestamp, nonce };
// 将token、timestamp、nonce三个参数进行字典序排序
//数组排序
Arrays.sort(arr);
//String 字符串常量
//StringBuffer 字符串变量(线程安全)
//StringBuilder 字符串变量(非线程安全)
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
//java.security.MessageDigest类用于为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。简单点说就是用于生成散列码。
MessageDigest md = null;
String tmpStr = null;
try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
当然这个servlet要在web.xml中配置。
上面的代码主要是保证微信接口可以成功接入,接下来便是授权的步骤。
首先是需要授权的业务部分的请求
//APPID REDIREC_URI 定义
@RequestMapping("/home")
public String home(HttpServletRequest request,HttpServletResponse response,Model model,
@RequestParam(value = "authopenid", required = false, defaultValue = "")String authopenid){
//是否需要授权并带回openid
try {
if(StringUtils.isBlank(authopenid)){
//这里主要是对微信提供的接口的参数进行替换,redirect_uri需要进行编码,指向下面将要提到的授权的地址,重定向后会带有code
response.sendRedirect(WeixinOAuth.getActualCodeUrl(APPID,WeixinOAuth.SCOPE_BASE, WeixinOAuth.STATE ,REDIRECT_URI));//
return null;
}
} catch (IOException e) {
e.printStackTrace();
}
。。。。//业务代码
WeixinOAuth工具类中的方法
public static String getActualCodeUrl(String appid, String scope, String state, String redirect_uri){
//CODE_URL="https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect";
String str = CODE_URL.replace("APPID", appid).replace("SCOPE", scope).replace("STATE", state).replace("REDIRECT_URI", urlEncode(redirect_uri,"UTF-8"));
return str;
}
private static String urlEncode(String source,String charset) {
String result = source;
try {
result = URLEncoder.encode(source, charset);
} catch (UnsupportedEncodingException e) {
log.error("UnsupportedEncodingException:",e.fillInStackTrace());
}
return result;
}
redirect_uri指向的地址,用于授权获取access_token
@Controller
@RequestMapping("/signOAuth")
public class SignOAuthController {
private static final Logger LOGGER = LoggerFactory.getLogger(SignOAuthController.class);
/**服务号的id和secret**/
private static final String APPID="wx628381d0xxxxxx";
private static final String APP_SECRET="70617dae7de3bb5fb6xxxxxxxxxx";
//重定向到业务的链接
private static final String URL="http://xxxxxxxxxx";
@RequestMapping("/view")
public void view(HttpServletRequest request,HttpServletResponse response,
@RequestParam(value="code",required=false)String code, @RequestParam(value="state",required=false)String state,
@RequestParam(value="openid",required=false)String openid) throws IOException{
LOGGER.info(" view:\n++++++++++++++++++++++++++:"+openid);
//判断是否带有code,有则进行授权
if(StringUtils.isNotBlank(code)){
AccessTokenByCode token = WeixinUtil.getAuthAccessTokenByCode(APPID, APP_SECRET, code);
if(null!=token && StringUtils.isBlank(token.getAccess_token()) && StringUtils.isNotBlank(token.getRefresh_token())){
//获取刷新后的token
token = WeixinUtil.getRefreshOauthAccessToken(APPID, token.getRefresh_token());
}
if(null!=token && StringUtils.isNotBlank(token.getAccess_token()) && StringUtils.isNotBlank(token.getOpenid())){
response.sendRedirect(URL+"?authopenid="+token.getOpenid());//重定向到之前的业务
}
}else{
LOGGER.info("没有授权。。。。");
}
}
}
WeixinUtil中的方法
public static AccessTokenByCode getAuthAccessTokenByCode(String appid, String appsecret, String code){
AccessTokenByCode accessToken = null;
//AUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
String requestUrl = AUTH_ACCESS_TOKEN_URL.replace("APPID", appid).replace("SECRET", appsecret).replace("CODE", code);
JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
// 如果请求成功
if (null != jsonObject) {
try {
accessToken = new AccessTokenByCode();
accessToken.setAccess_token(jsonObject.getString("access_token"));
accessToken.setExpires_in(jsonObject.getInt("expires_in"));
accessToken.setRefresh_token(jsonObject.getString("refresh_token"));
accessToken.setOpenid(jsonObject.getString("openid"));
accessToken.setScope(jsonObject.getString("scope"));
} catch (Exception e) {
log.info("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}
}
return accessToken;
}
/**
* 刷新网页授权凭证
* @param appId 公众账号的唯一标识
* @param refreshToken
* @return AccessTokenByCode
*/
public static AccessTokenByCode getRefreshOauthAccessToken(String appId, String refreshToken) {
AccessTokenByCode token = null;
// 拼接请求地址
String requestUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN";
requestUrl = requestUrl.replace("APPID", appId);
requestUrl = requestUrl.replace("REFRESH_TOKEN", refreshToken);
// 刷新网页授权凭证
JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null);
if (null != jsonObject) {
try {
token = new AccessTokenByCode();
token.setAccess_token(jsonObject.getString("access_token"));
token.setExpires_in(jsonObject.getInt("expires_in"));
token.setRefresh_token(jsonObject.getString("refresh_token"));
token.setOpenid(jsonObject.getString("openid"));
token.setScope(jsonObject.getString("scope"));
} catch (Exception e) {
token = null;
int errorCode = jsonObject.getInt("errcode");
String errorMsg = jsonObject.getString("errmsg");
log.error("刷新网页授权凭证失败 errcode:{} errmsg:{}", errorCode, errorMsg);
}
}
return token;
}
CommonUtils的请求util类
/**
* 发送https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error("连接超时:{}", ce);
} catch (Exception e) {
log.error("https请求异常:{}", e);
}
return jsonObject;
}
MyX509TrustManager类
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
/**
* 证书信任管理器(用于https请求)
* <br>
* X509证书信任管理器类
* <br>
* 证书信任管理器类就是实现了接口X509TrustManager的类
*/
public class MyX509TrustManager implements X509TrustManager {
/**
* 该方法检查客户端的证书,若不信任该证书则抛出异常。<br>
* 由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。<br>
* JSSE中,默认的信任管理器类为TrustManager。
* *
*/
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
/**
* 该方法检查服务器的证书,若不信任该证书同样抛出异常。<br>
* 通过自己实现该方法,可以使之信任我们指定的任何证书。<br>
* 在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
* */
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
/**
* 返回受信任的X509证书数组。
* */
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
public class AccessTokenByCode {
private int id;
// 获取到的凭证
private String access_token;
// 凭证有效时间,单位:秒
private int expires_in;
private String refresh_token;
private String openid;
private String scope;
private String unionid;
//get/set方法省略
@Override
public String toString() {
return "AccessTokenByCode [id=" + id + ", access_token=" + access_token
+ ", expires_in=" + expires_in + ", refresh_token="
+ refresh_token + ", openid=" + openid + ", scope=" + scope
+ ", unionid=" + unionid + "]";
}
}