调用 Rest 身份验证控制器的 EOFException [英] EOFException calling a Rest Authentication Controller
问题描述
我有一个基本的 SpringBoot 2.0.5.RELEASE 应用程序.使用 Spring Initializer、JPA、嵌入式 Tomcat、Thymeleaf 模板引擎,并打包为具有 Restful 架构的可执行 JAR
我已经创建了这个 Rest 控制器用于身份验证:
@RestController公共类 AuthenticationRestController {private static final Logger LOG = LoggerFactory.getLogger (AuthenticationRestController.class);@Value("${jwt.header}")私人字符串令牌头;@自动连线私有 AuthenticationManager authenticationManager;@自动连线私有 JwtTokenUtil jwtTokenUtil;@自动连线私有 UserSecurityService userSecurityService;@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)公共响应实体>createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) 抛出 AuthenticationException {LOG.info("正在验证 [ " + authenticationRequest.getUsername() + " ]");身份验证(身份验证请求.getUsername(),身份验证请求.getPassword());//重新加载密码后安全性,以便我们可以生成令牌final UserDetails userDetails = userSecurityService.loadUserByUsername(authenticationRequest.getUsername());final String token = jwtTokenUtil.generateToken(userDetails);//返回令牌返回 ResponseEntity.ok(new JwtAuthenticationResponse(token));}@RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)公共响应实体>refreshAndGetAuthenticationToken(HttpServletRequest 请求){String authToken = request.getHeader(tokenHeader);最终字符串令牌 = authToken.substring(7);String username = jwtTokenUtil.getUsernameFromToken(token);JwtUser user = (JwtUser) userSecurityService.loadUserByUsername(username);if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())) {String refreshedToken = jwtTokenUtil.refreshToken(token);返回 ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));} 别的 {返回 ResponseEntity.badRequest().body(null);}}@ExceptionHandler({AuthenticationException.class})公共响应实体handleAuthenticationException(AuthenticationException e) {返回 ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());}/*** 验证用户.如果出现问题,将抛出 {@link AuthenticationException}*/私人无效认证(字符串用户名,字符串密码){Objects.requireNonNull(username);Objects.requireNonNull(密码);尝试 {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));} catch (DisabledException e) {throw new AuthenticationException("用户被禁用!", e);} catch (BadCredentialsException e) {throw new AuthenticationException("证书错误!", e);}}}
和这个配置文件来管理配置
@Configuration@启用网络安全@EnableGlobalMethodSecurity(prePostEnabled = true)公共类 ApiWebSecurityConfig 扩展了 WebSecurityConfigurerAdapter {//private static final Logger LOG = LoggerFactory.getLogger(ApiWebSecurityConfig.class);@自动连线私有 JwtAuthenticationEntryPoint 未授权处理程序;@自动连线私有 JwtTokenUtil jwtTokenUtil;@自动连线私有 UserSecurityService userSecurityService;@Value("${jwt.header}")私人字符串令牌头;@Value("${jwt.route.authentication.path}")私有字符串身份验证路径;@Value("${server.servlet.context-path}")私有字符串服务器上下文路径;/** 加密 SALT.*/private static final String SALT = "f13333";@自动连线public void configureGlobal(AuthenticationManagerBuilder auth) 抛出异常 {授权.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());}@豆角,扁豆公共 BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));}@豆角,扁豆@覆盖公共 AuthenticationManager authenticationManagerBean() 抛出异常 {返回 super.authenticationManagerBean();}@覆盖protected void configure(HttpSecurity httpSecurity) 抛出异常 {http安全//我们不需要 CSRF 因为我们的令牌是无懈可击的.csrf().disable().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()//不创建会话.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//不安全的 H2 数据库.antMatchers("/h2-console/**/**").permitAll().antMatchers("/auth/**").permitAll().anyRequest().authenticated();//基于 JWT 的自定义安全过滤器JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);http安全.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//禁用页面缓存http安全.headers().frameOptions().sameOrigin()//需要为 H2 设置,否则 H2 控制台将为空白..cacheControl();}@覆盖公共无效配置(WebSecurity web)抛出异常{//AuthenticationTokenFilter 将忽略以下路径网络.忽略().antMatchers(HttpMethod.POST,认证路径)}}
但是当我把这个 curl 放到我的计算机的控制台中时,我在 Ubuntu 服务器中托管的服务器控制台中出现了这个错误:
url -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{"username":"nunet@gmail.com","password":"qwerty"}' "http://139.262.221.117:1234/calssada/api/v1/auth"
服务器日志:
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.coyote.http11.Http11InputBuffer - 收到 [POST/calssada/api/v1/auth HTTP/1.1主持人:139.262.221.117:1234用户代理:curl/7.54.0接受: */*内容类型:应用程序/json缓存控制:无缓存内容长度:56{"用户名":"nunet@gmail.com","密码":"qwerty"}]2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.a.AuthenticatorBase - 安全检查请求 POST/calssada/api/v1/auth2018-10-15 19:14 [http-nio-1234-exec-1] 调试 org.apache.catalina.realm.RealmBase - 未定义适用的约束2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.a.AuthenticatorBase - 不受任何约束2018-10-15 19:14 [http-nio-1234-exec-1] 调试 o.a.tomcat.util.http.Parameters - 将编码设置为 UTF-82018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.c.C.[Tomcat].[localhost] - 处理 ErrorPage[errorCode=0, location=/error]2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.c.C.[.[.[.[dispatcherServlet] - 禁用进一步输出的响应2018-10-15 19:14 [http-nio-1234-exec-1] 调试 oatutil.net.SocketWrapperBase - 套接字:[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]],从缓冲区读取:[0]2018-10-15 19:14 [http-nio-1234-exec-1] 调试 oatomcat.util.net.NioEndpoint - 套接字:[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]],直接从socket读取:[0]2018-10-15 19:14 [http-nio-1234-exec-1] 调试 oacoyote.http11.Http11Processor - 套接字:[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]],状态在:[OPEN_READ],状态:[OPEN]]2018-10-15 19:14 [http-nio-1234-exec-1] 调试 o.a.coyote.http11.Http11NioProtocol - 推送处理器 [org.apache.coyote.http11.Http11Processor@28de9baa]2018-10-15 19:14 [http-nio-1234-exec-2] 调试 oacoyote.http11.Http11NioProtocol - 处理套接字 [org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]] 状态为 [OPEN_READ]2018-10-15 19:14 [http-nio-1234-exec-2] 调试 oacoyote.http11.Http11NioProtocol - 为套接字 [org.apache.tomcat.util.net.NioChannel@5c80e3a0:java 找到处理器 [null].nio.channels.SocketChannel[连接本地=/139.262.221.117:1234远程=/119.88.31.26:58071]]2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11NioProtocol - 从缓存中弹出处理器 [org.apache.coyote.http11.Http11Processor@28de9baa]2018-10-15 19:14 [http-nio-1234-exec-2] 调试 oatutil.net.SocketWrapperBase - 套接字:[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]],从缓冲区读取:[0]2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11Processor - 解析 HTTP 请求标头时出错java.io.EOFException: null在 org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1289)在 org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1223)在 org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:729)在 org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:368)在 org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:684)在 org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)在 org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)在 org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)在 org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)在 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)在 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)在 org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)在 java.lang.Thread.run(Thread.java:748)2018-10-15 19:14 [http-nio-1234-exec-2] 调试 oacoyote.http11.Http11Processor - 套接字:[org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]],状态在:[OPEN_READ],状态:[CLOSED]]
也许您必须将 -H "Connection: close"
添加到您的 curl
命令中?>
请注意,在您的错误日志中有两个请求.第一个由线程http-nio-1234-exec-1"处理.我想知道Processing ErrorPage[errorCode=0, location=/error]"是否意味着它导致失败.
HTTP/1.1 协议允许通过同一连接(又名 Keep-Alive)发送多个请求.由于没有Connection: close"标头,Tomcat 会继续从连接中读取数据.这是由线程http-nio-1234-exec-2"执行的,并且在尝试读取请求的第一行 (Http11InputBuffer.parseRequestLine()
) 时失败并显示 EOF.此处应有 EOF.
I have a basic SpringBoot 2.0.5.RELEASE app. Using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR with a restful architecture
I have created this Rest controller for authentication:
@RestController
public class AuthenticationRestController {
private static final Logger LOG = LoggerFactory.getLogger (AuthenticationRestController.class);
@Value("${jwt.header}")
private String tokenHeader;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserSecurityService userSecurityService;
@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
LOG.info("authenticating [ " + authenticationRequest.getUsername() + " ]");
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
// Reload password post-security so we can generate the token
final UserDetails userDetails = userSecurityService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
@RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) {
String authToken = request.getHeader(tokenHeader);
final String token = authToken.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(token);
JwtUser user = (JwtUser) userSecurityService.loadUserByUsername(username);
if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())) {
String refreshedToken = jwtTokenUtil.refreshToken(token);
return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
} else {
return ResponseEntity.badRequest().body(null);
}
}
@ExceptionHandler({AuthenticationException.class})
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
}
/**
* Authenticates the user. If something is wrong, an {@link AuthenticationException} will be thrown
*/
private void authenticate(String username, String password) {
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new AuthenticationException("User is disabled!", e);
} catch (BadCredentialsException e) {
throw new AuthenticationException("Bad credentials!", e);
}
}
}
and this configuration file to manage the configuration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter {
//private static final Logger LOG = LoggerFactory.getLogger(ApiWebSecurityConfig.class);
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserSecurityService userSecurityService;
@Value("${jwt.header}")
private String tokenHeader;
@Value("${jwt.route.authentication.path}")
private String authenticationPath;
@Value("${server.servlet.context-path}")
private String serverContextPath;
/** The encryption SALT. */
private static final String SALT = "f13333";
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// Un-secure H2 Database
.antMatchers("/h2-console/**/**").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity
.headers()
.frameOptions().sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
}
@Override
public void configure(WebSecurity web) throws Exception {
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
authenticationPath
)
}
}
but when I put this curl in the console of my computer, I have this error in the server console hosted in a Ubuntu server:
url -X POST -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{"username":"nunet@gmail.com","password":"qwerty"}' "http://139.262.221.117:1234/calssada/api/v1/auth"
the server log:
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.coyote.http11.Http11InputBuffer - Received [POST /calssada/api/v1/auth HTTP/1.1
Host: 139.262.221.117:1234
User-Agent: curl/7.54.0
Accept: */*
Content-Type: application/json
Cache-Control: no-cache
Content-Length: 56
{"username":"nunet@gmail.com","password":"qwerty"}]
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.a.AuthenticatorBase - Security checking request POST /calssada/api/v1/auth
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG org.apache.catalina.realm.RealmBase - No applicable constraints defined
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.a.AuthenticatorBase - Not subject to any constraint
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.tomcat.util.http.Parameters - Set encoding to UTF-8
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.c.C.[Tomcat].[localhost] - Processing ErrorPage[errorCode=0, location=/error]
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.c.c.C.[.[.[.[dispatcherServlet] - Disabling the response for further output
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.t.util.net.SocketWrapperBase - Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]], Read from buffer: [0]
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.tomcat.util.net.NioEndpoint - Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]], Read direct from socket: [0]
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.coyote.http11.Http11Processor - Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]], Status in: [OPEN_READ], State out: [OPEN]
2018-10-15 19:14 [http-nio-1234-exec-1] DEBUG o.a.coyote.http11.Http11NioProtocol - Pushed Processor [org.apache.coyote.http11.Http11Processor@28de9baa]
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11NioProtocol - Processing socket [org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]] with status [OPEN_READ]
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11NioProtocol - Found processor [null] for socket [org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]]
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11NioProtocol - Popped processor [org.apache.coyote.http11.Http11Processor@28de9baa] from cache
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.t.util.net.SocketWrapperBase - Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]], Read from buffer: [0]
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11Processor - Error parsing HTTP request header
java.io.EOFException: null
at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1289)
at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1223)
at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:729)
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:368)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:684)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
2018-10-15 19:14 [http-nio-1234-exec-2] DEBUG o.a.coyote.http11.Http11Processor - Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@2fca076b:org.apache.tomcat.util.net.NioChannel@5c80e3a0:java.nio.channels.SocketChannel[connected local=/139.262.221.117:1234 remote=/119.88.31.26:58071]], Status in: [OPEN_READ], State out: [CLOSED]
Maybe you have to add -H "Connection: close"
to your curl
command?
Note that in your error log there are two requests. The first one is processed by thread "http-nio-1234-exec-1". I wonder whether "Processing ErrorPage[errorCode=0, location=/error]" means that it resulted in a failure.
HTTP/1.1 protocol allows sending several requests over the same connection (aka Keep-Alive). As there is no "Connection: close" header, Tomcat continues to read from the connection. This is performed by thread "http-nio-1234-exec-2" and fails with an EOF when trying to read the first line of the request (Http11InputBuffer.parseRequestLine()
). The EOF is expected here.
这篇关于调用 Rest 身份验证控制器的 EOFException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!