我想大家做微信开发都涉及到微信授权这个问题。那么对于项目中需要授权的URL大家都是怎么设计开发的呢?我想大家一般都有2个方案。一种是用Servlet中的Filter,还有一种就是Spring MVC中的HandlerInterceptor。当需要获取微信中的用户信息时,我们可以在Cookie中添加相关的code然后使用拦截器机制对面URL进行授权。
我们先来分析一下这2种方案。
那么有没有更好的办法来解决这个问题呢?答案是有的。我们可以使用Spring MVC中的自定义方法参数解析,然后用于换取微信授权返回的用户信息给HandleMethod也就是定义@RequstMapping的Controller中的方法来使用。这样就可以针对特殊URL,特殊的参数来区分可以是否授权。可以标记,Method将授权结果作为参数传入到方法中。
public interface HandlerMethodArgumentResolver {
/**
* 方法参数是否被当前解析解析器支持
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 解析这个方法参数
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
该业务场景使用到的URL。(至少一次,最多三次)
流程1:COOKIE中有code,并且也拿到了正确的用户信息
流程2:COOKIE中有code,但是没有拿到了正确的用户信息(1),跳微信授权(2)授权回来(调用我们的URL回来重定向),CODE拿到了正确的用户信息(3) – 对应场景(以前没有授权,现在授权。或者之前是之前授权现在过期,之前是静默授权现在变成确认授权)。
这2种不同的流程可以思路cookie中的code,进行授权验证然后换取用户信息给HandleMethod使用。
1)定义Spring MVC中Controller中的方法注解。用于方法参数解析。
/**
* 微信身份信息
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WechatAuthInfo {
public enum AidFrom {
PATH_VARIABLE,
REQUEST_PARAM,
HOSTNAME
}
/**
* 是否强制要求为非空,若为true,则表示没有授权信息时抛出异常,可在异常中进行授权和重定向
*/
boolean required() default true;
/**
* 从哪里获取aid
*/
AidFrom aidFrom() default AidFrom.PATH_VARIABLE;
/**
* 获取aid所用的path,如PathVariable中的名字或者RequestParam中的名字
*/
String name() default "aid";
}
2)用户信息
定义用户信息可以在HandlerMethod中使用。
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown=true)
public class SilentAuthorizationResult {
@JsonProperty("AId")
private Long aid;
@JsonProperty("OpenId")
private String openId;
@JsonProperty("BizOpenId")
private String bizOpenId;
}
3)微信地址
@Builder
@Log
public class OAuthProvider {
private String silentAuthUrl;
private String silentResultUrl;
private String confirmAuthUrl;
private String confirmResultUrl;
public SilentAuthorizationResult silentAuth(String weimobSID) {
try {
String url = String.format(silentResultUrl, URLEncoder.encode(weimobSID, "utf-8"));
ObjectNode response = HTTPClientUtils.sendHTTPRequest(url, null, "GET");
SilentAuthorizationResult authInfo = JSON.parseObject(response.toString(), SilentAuthorizationResult.class);
if (authInfo == null || StringUtils.isEmpty(authInfo.getOpenId())) {
// 信息不完整
return null;
}
return authInfo;
} catch (Exception e) {
// 网络错误或者。。。。
log.info(e.getLocalizedMessage());
return null;
}
}
public ConfirmAuthorizationResult confirmAuth(String weimobSID) {
try {
String url = String.format(confirmResultUrl, URLEncoder.encode(weimobSID, "utf-8"));
ObjectNode response = HTTPClientUtils.sendHTTPRequest(url, null, "GET");
ConfirmAuthorizationResult authInfo = JSON.parseObject(response.toString(), ConfirmAuthorizationResult.class);
if (authInfo == null || StringUtils.isEmpty(authInfo.getOpenId()) || StringUtils.isEmpty(authInfo.getNickName())) {
// 信息不完整
return null;
}
return authInfo;
} catch (Exception e) {
// 网络错误或者。。。。
log.info(e.getLocalizedMessage());
return null;
}
}
public String silentUrl(Long aid, String url) {
try {
return String.format(silentAuthUrl, URLEncoder.encode(String.valueOf(aid), "utf-8"), URLEncoder.encode(String.valueOf(aid), "utf-8"), URLEncoder.encode(url, "utf-8"));
} catch (UnsupportedEncodingException ignored) {
//不会出现
}
return null;
}
public String confirmUrl(Long aid, String url) {
try {
return String.format(confirmAuthUrl, URLEncoder.encode(String.valueOf(aid), "utf-8"), URLEncoder.encode(String.valueOf(aid), "utf-8"), URLEncoder.encode(url, "utf-8"));
} catch (UnsupportedEncodingException ignored) {
//不会出现
}
return null;
}
}
4)自定义HandlerMethodArgumentResolver
实现Spring中的HandlerMethodArgumentResolver。进行微信验证与用户信息获取。
@ControllerAdvice
public class AuthInfoMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final Pattern HOST_PATTERN = Pattern.compile("^(\\d+)\\..*$");
private OAuthProvider provider;
private String baseUrl;
public String getBaseUrl() {
return baseUrl;
}
/**
* 如 计算地址时会在后面加上controller里的地址和contextPath
*
* @param baseUrl 基础url
*/
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setProvider(OAuthProvider provider) {
this.provider = provider;
}
public OAuthProvider getProvider() {
return provider;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class> paramType = parameter.getParameterType();
return parameter.hasParameterAnnotation(WechatAuthInfo.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
if (provider == null) {
provider = OAuthProvider.builder().silentAuthUrl(Constant.WECHAT_SILENT_OAUTH_URL).confirmAuthUrl(Constant.WECHAT_CONFIRM_OAUTH_URL).silentResultUrl(Constant.WECHAT_SILENT_OAUTH_RESULT_URL).confirmResultUrl(Constant.WECHAT_CONFIRM_OAUTH_RESULT_URL).build();
}
WechatAuthInfo annotation = parameter.getParameterAnnotation(WechatAuthInfo.class);
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
boolean found = false;
Long aid = null;
String name = annotation.name();
boolean isConfirm = ConfirmAuthorizationResult.class.isAssignableFrom(parameter.getParameterType());
// 查找aid
switch (annotation.aidFrom()) {
case PATH_VARIABLE:
Map variables = getUriTemplateVariables(webRequest);
String pathVariable = variables.get(name);
if (StringUtils.hasText(pathVariable)) {
aid = Long.valueOf(pathVariable);
}
break;
case REQUEST_PARAM:
String requestParam = request.getParameter(name);
if (StringUtils.hasText(requestParam)) {
aid = Long.valueOf(requestParam);
}
break;
case HOSTNAME:
String host = request.getHeader("Host");
if (StringUtils.hasText(host)) {
String hostValue = HOST_PATTERN.matcher(host).replaceAll("$1");
if (StringUtils.hasText(hostValue)) {
aid = Long.valueOf(hostValue);
}
}
break;
default:
break;
}
// aid没找到
if (aid == null) {
if (annotation.required()) {
throw new MissingServletRequestParameterException(annotation.name(), Long.class.getName());
}
// 非必须
return null;
}
// 重新计算url, 修正url
String url = String.format(getBaseUrl(), aid) +
(request.getPathInfo() == null ? request.getServletPath() : request.getPathInfo());
StringBuilder currentUrl = new StringBuilder(url);
if (StringUtils.hasText(request.getQueryString())) {
currentUrl.append("?");
currentUrl.append(request.getQueryString());
}
// 查找 cookie
String weimobSID = null;
String cookieName = String.format("SessionId_%s", aid);
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (cookieName.equals(cookie.getName())) {
weimobSID = cookie.getValue();
if (StringUtils.hasText(weimobSID)) {
break;
}
}
}
}
// cookie没找到
if (!StringUtils.hasText(weimobSID) && annotation.required()) {
throw new WechatAuthInfoMissingException(isConfirm ? provider.confirmUrl(aid, currentUrl.toString()) : provider.silentUrl(aid, currentUrl.toString()), "Auth into required. redirecting");
}
// cookie找到
SilentAuthorizationResult authInfo;
if (isConfirm) {
ConfirmAuthorizationResult confirmAuthInfo = provider.confirmAuth(weimobSID);
// 信息不完整
if ((confirmAuthInfo == null || StringUtils.isEmpty(confirmAuthInfo.getNickName())) && annotation.required()) {
throw new WechatAuthInfoMissingException(provider.confirmUrl(aid, currentUrl.toString()), "Auth not completed. redirecting");
}
authInfo = confirmAuthInfo;
} else {
authInfo = provider.silentAuth(weimobSID);
if ((authInfo == null || StringUtils.isEmpty(authInfo.getOpenId())) && annotation.required()) {
throw new WechatAuthInfoMissingException(provider.silentUrl(aid, currentUrl.toString()), "Not auth. redirecting");
}
}
return authInfo;
}
@SuppressWarnings("unchecked")
protected final Map getUriTemplateVariables(NativeWebRequest request) {
Map variables = (Map) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (variables != null ? variables : Collections.emptyMap());
}
public static class WechatAuthInfoMissingException extends ServletRequestBindingException {
private static final long serialVersionUID = 2756877094069648764L;
public WechatAuthInfoMissingException(String url, String msg) {
super(msg);
this.url = url;
}
public WechatAuthInfoMissingException(String msg, Throwable cause) {
super(msg);
if (cause != null) {
initCause(cause);
}
}
private String url;
public String getUrl() {
return url;
}
}
/**
* 如果BaseController没有处理,则此处为后备方案
* fallback
*
* @param ex 异常详情
* @return 处理结果
* @throws Throwable 不兼容的异常
*/
@ExceptionHandler(WechatAuthInfoMissingException.class)
public ResponseEntity onWechatAuthMissing(WechatAuthInfoMissingException ex) throws Throwable {
if (ex.getUrl() != null) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", ex.getUrl());
return new ResponseEntity("正在载入", headers, HttpStatus.FOUND);
}
throw ex.getCause();
}
}
5)纳入Spring的解析器管理
<mvc:annotation-driven validator="validator">
<mvc:argument-resolvers>
<bean class="com.weimob.common.web.param.AuthInfoMethodArgumentResolver">
<property name="baseUrl">
<util:constant static-field="com.weimob.o2o.common.Constant.O2OConstant.O2O_H5_ADDRESS"/>
property>
bean>
mvc:argument-resolvers>
mvc:annotation-driven>
6)项目应用
@RequestMapping("yoururl")
public ModelAndView get(@WechatAuthInfo(name = "merchantId") SilentAuthorizationResult auth ...) {
// do something
return null;
}
这样可以使用WechatAuthInfo注解进行URL的授权管理,以及获取用户信息。当然你可以只使用微授权,不使用这个方法参数。根据你的具体业务逻辑来考虑。
使用这个方式主要是有以下3个考虑点: