SpringMVC Custom ArgumentResolver

23126 ワード

皆さんが微信の開発をするには、微信の授権という問題にかかわっていると思います.では、プロジェクトで許可が必要なURLについては、どのように設計開発されているのでしょうか.皆さんは普通2つの案があると思います.一つはサーブレットの中のFilterで、もう一つはSpring MVCの中のHandlerInterceptorです.微信のユーザ情報を取得する必要がある場合、Cookieに関連するcodeを追加し、ブロッキングメカニズムを使用して対面URLに権限を付与することができます.
1、プランを考える
まず、この2つの案を分析してみましょう.
  • Filter:Filterの後ろのFilterを検証するにはドメイン名の判断が必要です.また、Controllerのメソッドに値を設定することはできません.
  • HandlerInterceptor:パラメータを取得でき、メソッドに値を設定できません.

  • では、この問題を解決する方法はありますか?答えはある.Spring MVCのカスタムメソッドパラメータを使用して解析し、WeChatライセンスで返されたユーザー情報をHandleMethod、すなわち@RequstMappingを定義するControllerに交換する方法を使用できます.これにより、特殊なURLに対して、特殊なパラメータで権限を付与できるかどうかを区別することができます.メソッドに認証結果がパラメータとして渡されることをマークできます.
    2、HandlerMethodArgumentResolver
    public interface HandlerMethodArgumentResolver {
    
        /**
         *                 
         */
        boolean supportsParameter(MethodParameter parameter);
    
        /**
         *         
         */
        Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
    
    }

    3、コード構想
    このビジネスシーンで使用されるURL.(少なくとも1回、最大3回)フロー1:COOKIEにはcodeがあり、正しいユーザ情報フロー2:COOKIEにもcodeがあるが、正しいユーザ情報が得られなかった(1)、ホップ微信授権(2)授権が戻ってきた(私たちのURLを呼び出して戻ってリダイレクト)、CODEは正しいユーザー情報を取得しました(3)-対応するシーン(以前は許可されていませんでしたが、現在は許可されています.または、以前は許可が期限切れで、以前はサイレント許可だったが、現在は確認許可になりました).
    この2つの異なるプロセスは、クッキー内のcodeを考え、認証を行い、HandleMethodにユーザー情報を交換して使用することができる.
    4、コード実装
    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の認証管理やユーザ情報の取得が可能になります.もちろん、このメソッドパラメータを使用しないで、マイクロライセンスのみを使用することができます.あなたの具体的なビジネスロジックに基づいて考えます.
    5、まとめ
    この方式を使うには主に以下の3つの考慮点があります.
  • out of the box:開梱即用で、特定のURLに対して、選択的に使用できます.
  • open:ControllerはFilterとHandlerInterceptorに比べてメソッド上位層(開発者)に近づくことができる.
  • easy test.シミュレーションの許可、Controllerはデバッグすることができて、Controllerは1つの注釈のマークだけあって、高度にデバッグすることができて便利で高いです.コードの変更とコードの変更は同じ場所ではありません.権限が必要なときにこの注釈を追加し、必要でないときに注釈することができます.FilterやHandlerInterceptorより便利です.