6

したがって、すべての websocket チュートリアル/例は非常に簡単に見えると言わざるを得ませんが、単純な例から除外されている本当に重要な情報を見つけるには、本当に掘り下げる必要があるようです。フロントエンドで SockJS を使用して Spring 4 Stomp メッセージブローカーを使用している webapp には、まだかなりの問題があります。

現在、SockJS() を有効にせずに StompEndpointRegistry にエンドポイントを追加し、dojo の dojox/socket を使用してフロントエンドでソケットを宣言すると、Firefox 28 は問題なく websocket を開きます。ただし、IE8 と IE9 でのサポートが必要なので、SockJS に切り替えました。AbstractAnnotationConfigDispatcherServletInitializer を使用して、すべてのフィルターとサーブレットが非同期を使用するように設定されていることを確認する方法を理解するのにかなりの時間がかかりました (これに関する Web 上のドキュメントは非常にまばらです)。これを解決したら、Firefox で動作するようになりましたが、xhr_streaming のみを使用しています。sessionCookieNeeded を true に設定すると、IE9 はデフォルトで接続に iframe を使用しようとしますが、失敗します。

LOG: Opening Web Socket... 
LOG: Opening transport: iframe-htmlfile  url:rest/hello/904/ft3apk1g  RTO:1008 
LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) 
LOG: Opening transport: iframe-xhr-polling  url:rest/hello/904/bf63eisu  RTO:1008 
LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) 
LOG: Whoops! Lost connection to undefined 

必要な Cookie を false に設定すると、IE は xdr-streaming を使用して正常に動作しますが、リクエストで jsessionid Cookie が失われ、コントローラーで重要なプリンシパルを取得できなくなります。Spring Security で同じオリジン x フレーム ヘッダーを有効にし、ヘッダーがリクエストに存在することを確認しましたが、役に立ちませんでした。したがって、A) Spring と SockJS が Firefox で WebSocket トランスポートを使用して適切にネゴシエートするようにする方法と、B) IE8 と 9 で iframe トランスポートを適切に使用して Cookie を保持できるようにする方法を理解したいと思います。

ここに私の設定/コードがあります:

ウェブアプリの構成:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        super.onStartup(servletContext);
        Map<String, ? extends FilterRegistration> registrations = servletContext.getFilterRegistrations();
    }

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        // this is needed for async support for websockets/sockjs
        registration.setInitParameter("dispatchOptionsRequest", "true");
        registration.setAsyncSupported(true);
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SecurityConfig.class, Log4jConfig.class, PersistenceConfig.class, ServiceConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // loading the Initializer class from the dispatcher servlet context ensures it only executes once,
        // as the ContextRefreshedEvent fires once from the root context and once from the dispatcher servlet context   
        return new Class[]{SpringMvcConfig.class, WebSocketConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{
            "/rest/*",
            "/index.html",
            "/login.html",
            "/admin.html",
            "/index/*",
            "/login/*",
            "/admin/*"
        };
    }

    @Override
    protected Filter[] getServletFilters() {
        OpenEntityManagerInViewFilter openEntityManagerInViewFilter = new OpenEntityManagerInViewFilter();
        openEntityManagerInViewFilter.setBeanName("openEntityManagerInViewFilter");
        openEntityManagerInViewFilter.setPersistenceUnitName("HSQL");

        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8");
        encodingFilter.setForceEncoding(true);

        return new javax.servlet.Filter[]{openEntityManagerInViewFilter, encodingFilter};
    }

}

Spring MVC 構成:

@Configuration
@EnableWebMvc
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@ComponentScan(basePackages = "x.controllers")  // Only scan for controllers.  Other classes are scanned in the parent's root context
public class SpringMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
        registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
        registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(mappingJacksonHttpMessageConverter());
        converters.add(marshallingMessageConverter());
        super.configureMessageConverters(converters);
    }

    @Bean
    public InternalResourceViewResolver setupViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Bean
    public JacksonAnnotationIntrospector jacksonAnnotationIntrospector() {
        return new JacksonAnnotationIntrospector();
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setAnnotationIntrospector(jacksonAnnotationIntrospector());
        mapper.registerModule(new JodaModule());
        mapper.registerModule(new Hibernate4Module());
        return mapper;
    }

    @Bean
    public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setObjectMapper(objectMapper());
        return messageConverter;
    }

    @Bean(name = "marshaller")
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.x);
        return marshaller;
    }

    @Bean
    public MarshallingHttpMessageConverter marshallingMessageConverter() {
        return new MarshallingHttpMessageConverter(
                jaxb2Marshaller(),
                jaxb2Marshaller()
        );
    }
}

Spring ルート コンテキスト構成:

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.x.services"},   // scan for all annotated classes for the root context OTHER than controllers -- those are in the child web context. also don't rescan these config files
        excludeFilters = {
            @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class),
            @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class)
        }
)
public class ServiceConfig {

    @Bean
    public DefaultAnnotationHandlerMapping defaultAnnotationHandlerMapping() {
        DefaultAnnotationHandlerMapping handlerMapping = new DefaultAnnotationHandlerMapping();
        handlerMapping.setAlwaysUseFullPath(true);
        handlerMapping.setDetectHandlersInAncestorContexts(true);
        return handlerMapping;
    }

    @Bean
    public DefaultConversionService defaultConversionService() {
        return new DefaultConversionService();
    }

    @Bean(name = "kmlContext")
    public JAXBContext kmlContext() throws JAXBException {
        return JAXBContext.newInstance("net.opengis.kml");
    }

    @Bean(name = "ogcContext")
    public JAXBContext ogcContext() throws JAXBException {
        return JAXBContext.newInstance("net.x");
    }
}

春のセキュリティ:

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;
    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationProvider rememberMeAuthenticationProvider = rememberMeAuthenticationProvider();
        TokenBasedRememberMeServices tokenBasedRememberMeServices = tokenBasedRememberMeServices();

        List<AuthenticationProvider> authenticationProviders = new ArrayList<AuthenticationProvider>(2);
        authenticationProviders.add(rememberMeAuthenticationProvider);
        authenticationProviders.add(customAuthenticationProvider);
        AuthenticationManager authenticationManager = authenticationManager(authenticationProviders);

        http
                .csrf().disable()
                //.headers().disable()
                .headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
                .and()
                .authenticationProvider(customAuthenticationProvider)
                .addFilter(new RememberMeAuthenticationFilter(authenticationManager, tokenBasedRememberMeServices))
                .rememberMe().rememberMeServices(tokenBasedRememberMeServices)
                .and()
                .authorizeRequests()
                .antMatchers("/js/**", "/css/**", "/img/**", "/login", "/processLogin").permitAll()
                .antMatchers("/index.jsp", "/index.html", "/index").hasRole("USER")
                .antMatchers("/admin", "/admin.html", "/admin.jsp", "/js/saic/jswe/admin/**").hasRole("ADMIN")
                .and()
                .formLogin().loginProcessingUrl("/processLogin").loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/login")
                .and()
                .logout().permitAll();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**", "/css/**", "/img/**");
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(List<AuthenticationProvider> authenticationProviders) {
        return new ProviderManager(authenticationProviders);
    }

    @Bean
    public TokenBasedRememberMeServices tokenBasedRememberMeServices() {
        return new TokenBasedRememberMeServices("testKey", userDetailsService);
    }

    @Bean
    public AuthenticationProvider rememberMeAuthenticationProvider() {
        return new org.springframework.security.authentication.RememberMeAuthenticationProvider("testKey");
    }

    protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

WebSocket メッセージ ブローカーの構成:

@Configuration
@EnableWebSocketMessageBroker
@EnableScheduling
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
            SockJsServiceRegistration registration = registry.addEndpoint("/hello").withSockJS().setClientLibraryUrl("http://localhost:8084/swtc/js/sockjs-0.3.4.min.js");
            registration.setWebSocketEnabled(true);
            //registration.setSessionCookieNeeded(false);

    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.taskExecutor().corePoolSize(4).maxPoolSize(8);
    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration registration) {
        registration.taskExecutor().corePoolSize(4).maxPoolSize(8);
    }

}

WebSocket コントローラー:

@Controller
public class WebSocketController {
    @MessageMapping({"/hello", "/hello/**"})
    @SendTo("/topic/greetings")
    // in order to get principal, you must set cookiesNeeded in WebSocketConfig, which forces IE to use iframes, which doesn't seem to work
    public AjaxResponse<String> greeting(@Payload PointRadiusRequest prr, Principal principal) throws Exception {
        Thread.sleep(3000); // simulated delay
        AjaxResponse<String> ajaxResponse = new AjaxResponse<String>();
        ajaxResponse.setValue(principal.getName());
        ajaxResponse.setSuccess(true);
        return ajaxResponse;
    } 
}

最後に、テストに使用している html の JavaScript は次のとおりです。

<script>
            // test/prototype websocket code
            stompClient = null;

            window.connect = function() {

                var options = {protocols_whitelist: ["websocket", "xhr-streaming", "xdr-streaming", "xhr-polling", "xdr-polling", "iframe-htmlfile", "iframe-eventsource", "iframe-xhr-polling"], debug: true}; 
                wsSocket = new SockJS('rest/hello', undefined, options);

                stompClient = Stomp.over(wsSocket);
                stompClient.connect({}, function(frame) {
                    console.log('Connected: ' + frame);
                    stompClient.subscribe('/topic/greetings', function(message) {
                        console.info("response: ", JSON.parse(message.body));
                    });
                });
            };

            window.disconnect = function() {
                stompClient.disconnect();
                console.log("Disconnected");
            };

            window.sendName = function() {
                stompClient.send("/app/hello", {}, JSON.stringify({'latitude': 12, 'longitude': 123.2, radius: 3.14}));
            };
        </script>

Firefox に接続すると、コンソールに次のように表示されます。

>>> connect()
connecting
/swtc/ (line 109)
Opening Web Socket...
stomp.js (line 130)
undefined
GET http://localhost:8084/swtc/rest/hello/info

200 OK
        202ms   
sockjs....min.js (line 27)
Opening transport: websocket url:rest/hello/007/xkc17fkt RTO:912
sockjs....min.js (line 27)
SyntaxError: An invalid or illegal string was specified


...3,reason:"All transports failed",wasClean:!1,last_event:g})}f.readyState=y.CLOSE...

sockjs....min.js (line 27)
Closed transport: websocket SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false)
sockjs....min.js (line 27)
Opening transport: xhr-streaming url:rest/hello/007/8xz79yip RTO:912
sockjs....min.js (line 27)
POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_streaming

200 OK
        353ms   
sockjs....min.js (line 27)
Web Socket Opened...

>>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000

�

stomp.js (line 130)
POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_send

204 No Content
        63ms    

<<< CONNECTED
user-name:first.mi.last
heart-beat:0,0
version:1.1

�

stomp.js (line 130)
connected to server undefined
stomp.js (line 130)

Connected: CONNECTED
version:1.1
heart-beat:0,0
user-name:xxx

>>> SUBSCRIBE
id:sub-0
destination:/topic/greetings

�

stomp.js (line 130)
POST http://localhost:8084/swtc/rest/hello/007/8xz79yip/xhr_send

204 No Content
        57ms

/info 応答は次のとおりです。

{"entropy":441118013,"origins":["*:*"],"cookie_needed":true,"websocket":true}

Websocket 接続を確立しようとすると、奇妙な文字列エラーが発生することに注意してください。それが私の問題の原因だと思いますが、私は何も面白いことをしていませんし、何が原因なのかわかりません.

IE では、ネットワーク トラフィックは次のようになります。iframe.html ファイルは適切にビルドされているように見えますが、バックエンドに接続できません。

URL Method  Result  Type    Received    Taken   Initiator   Wait‎‎  Start‎‎ Request‎‎   Response‎‎  Cache read‎‎    Gap‎‎
/swtc/rest/hello/info?t=1399328502157   GET 200 application/json    411 B   328 ms      0   47  281 0   0   2199
/swtc/rest/hello/iframe.html    GET 200 text/html   0.97 KB 156 ms  frame navigate  328 0   156 0   0   2043
/swtc/js/sockjs-0.3.4.min.js    GET 304 application/javascript  157 B   < 1 ms  <script>    484 0   0   0   0   2043
/swtc/rest/hello/iframe.html    GET 304 text/html   191 B   < 1 ms  frame navigate  2527    0   0   0   0   0
/swtc/js/sockjs-0.3.4.min.js    GET 304 application/javascript  157 B   < 1 ms  <script>    2527    0   0   0   0   0

情報応答は次のようになります。

{"entropy":-475136625,"origins":["*:*"],"cookie_needed":true,"websocket":true}

リクエストまたはレスポンス ヘッダーを確認したい場合は、お知らせください。

更新 1:

ロッセンさん、返信ありがとうございます。Spring 4について私が知っていることはすべてあなたから学びました:)

Firefox が実際には (完全に) 動作していません。websocket セッションを取得できません。xhr-streaming にダウングレードされます。xhr-streaming では問題はありませんが、本当の websocket セッションが必要です。

IE では、ヘッダーを削除すると何が確認されるのかわかりません。x フレーム ヘッダーは iframe セッションにのみ影響を与えていると思いましたが、まったく機能しません。必須の Cookie を無効にすると、IE は xdr-streaming を使用します (プリンシパルをフェッチする機能はありませんが、機能します)。Cookie を有効にすると、IE は適切に iframe を使用しようとします。ただし、ヘッダーが配置されていても、すべての試みは失敗します。

    http://localhost:8084/swtc/rest/hello/info?t=1399328502157

        Key Value
        Response    HTTP/1.1 200 OK
        Server  Apache-Coyote/1.1
        X-Frame-Options SAMEORIGIN
        Access-Control-Allow-Origin http://localhost:8084
        Access-Control-Allow-Credentials    true
        Cache-Control   no-store, no-cache, must-revalidate, max-age=0
        Content-Type    application/json;charset=UTF-8
        Content-Length  78
        Date    Mon, 05 May 2014 22:21:42 GMT

LOG: Opening Web Socket... 
LOG: Opening transport: iframe-htmlfile  url:rest/hello/904/ft3apk1g  RTO:1008 
LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) 
LOG: Opening transport: iframe-xhr-polling  url:rest/hello/904/bf63eisu  RTO:1008 
LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=1006, reason=Unable to load an iframe (onload timeout), wasClean=false) 
LOG: Whoops! Lost connection to undefined 

iframe-htmlfile と iframe-xhr-polling の両方が失敗します。私はIEでリフレッシュするたびにキャッシュをクリアし、SockJSでデバッグモードを有効にしています。IE で xdr-streaming を使用しても問題ありませんが、本当に jsessionid Cookie が必要です。

何かご意見は?

余談ですが、クライアント ライブラリ コードが相対パスをサポートしていれば非常に便利です (実際には相対パスを使用して html ファイルを作成し、動作するはずですが、ログにエラーが生成されます)。つまり、次のようになります。

SockJsServiceRegistration registration = registry.addEndpoint("/hello").withSockJS().setClientLibraryUrl("js/sockjs-0.3.4.min.js");

これにより、本番環境へのデプロイの負担が軽減されます。

更新 2:

簡単な要約: 変化はありませんでした。

これは、セキュリティ設定で .headers().and() を使用して IE9 で接続しようとする試みです。

LOG: Opening Web Socket... 
LOG: Opening transport: iframe-htmlfile  url:rest/hello/924/1ztfjm7z  RTO:330 
LOG: Closed transport: iframe-htmlfile SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) 
LOG: Opening transport: iframe-xhr-polling  url:rest/hello/924/cgq8_s5j  RTO:330 
LOG: Closed transport: iframe-xhr-polling SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false) 
LOG: Whoops! Lost connection to undefined 

/info の要求ヘッダー:

Key Value
Request GET /swtc/rest/hello/info?t=1399404419358 HTTP/1.1
Accept  */*
Origin  http://localhost:8084
Accept-Language en-US
UA-CPU  AMD64
Accept-Encoding gzip, deflate
User-Agent  Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Host    localhost:8084
Connection  Keep-Alive
Cache-Control   no-cache

および応答ヘッダー:

Key Value
Response    HTTP/1.1 200 OK
Server  Apache-Coyote/1.1
X-Content-Type-Options  nosniff
X-XSS-Protection    1; mode=block
Cache-Control   no-cache, no-store, max-age=0, must-revalidate
Pragma  no-cache
Expires 0
X-Frame-Options DENY
Access-Control-Allow-Origin http://localhost:8084
Access-Control-Allow-Credentials    true
Cache-Control   no-store, no-cache, must-revalidate, max-age=0
Content-Type    application/json;charset=UTF-8
Content-Length  78
Date    Tue, 06 May 2014 19:26:59 GMT

Firefox では違いはありませんでした。Websocket を開こうとすると、同じ奇妙な文字列エラーが発生し、xhr-streaming にフォールバックします。

Opening transport: websocket url:rest/hello/849/fy_06t1v RTO:342
SyntaxError: An invalid or illegal string was specified
Closed transport: websocket SimpleEvent(type=close, code=2007, reason=Transport timeouted, wasClean=false)
Opening transport: xhr-streaming url:rest/hello/849/2r0raiz8 RTO:342
http://localhost:8084/swtc/rest/hello/849/2r0raiz8/xhr_streaming
Web Socket Opened...
>>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
4

3 に答える 3

4

WebSocket 接続を試みると SockJS が奇妙な文字列エラーを生成し、その後 xhr_streaming にフォールバックしたため、縮小されていないバージョンの .js ファイルをロードし、Firebug でデバッグして何が起こっているかを確認することにしました。結局のところ、SockJS は相対 URL を好みません。

ほとんどの REST/AJAX サービスでは、/rest/* をディスパッチャー サーブレットにマップし、通常は各コントローラーに @RequestMapping を配置し、各コントローラー メソッドに別の @RequestMapping を配置します。Dojo を使用して、url を指定して AJAX 呼び出しを行います"rest/<controller>/<method>"

私はSockJSで同じことを試みていました。私はちょうど「休息/こんにちは」を指していました。これを完全修飾 URL " http://localhost:8084/swtc/rest/hello" に変更したところ、突然、Firefox が WebSocket トランスポート層を問題なく構築できるようになりました。簡単なテストのために IE に飛び乗ったところ、確かに iframe セッションが構築され、問題なく動作しました。

そんなばかげた小さな問題。このコード ベースは複数の開発者間で共有され、すべての開発者がテスト用に異なるサーバーにデプロイし、運用環境にデプロイするため、非相対 URL をどこかに指定する必要はありません。フロントエンドでは、window.doc.URL を使用して URL を動的に構築できると思いますが、setClientLibraryUrl を指定するときに、デプロイメント全体で AbstractWebSocketMessageBrokerConfigurer を自動的に機能させるのは少し難しいでしょう。

いずれにせよ、SockJS で相対パスを使用しないでください。

于 2014-05-07T13:32:01.077 に答える
3

sessionCookieNeeded=false を使用して FF および IE で機能することを考えると、問題は X-Frame-Options ヘッダーに関係していると思います。

あなたの構成は正しいようです。具体的には、Spring Security の場合:

        .headers().addHeaderWriter(
            new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)).and()

また、これはSockJSの場合:

            setClientLibraryUrl("http://localhost:8084/swtc/js/sockjs-0.3.4.min.js");

問題があるかどうかを確認するためだけに、ヘッダーを無効にすることをお勧めします。

        .headers().and()

また、ブラウザのキャッシュに問題がなく、同じ応答が返されていないことも確認してください。したがって、X-Frame-Options の値については、実際の応答ヘッダーを確認してください。

そのために、SockJS コンストラクターの options パラメーターを使用して SockJS クライアント デバッグ モードを有効にすることを強くお勧めします。

于 2014-05-06T14:05:32.713 に答える