SegmentFault 我的java进阶之旅最新的文章
2017-05-12T10:22:25+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
API编写规范
https://segmentfault.com/a/1190000009389162
2017-05-12T10:22:25+08:00
2017-05-12T10:22:25+08:00
刘祖明
https://segmentfault.com/u/liuzuming
1
<h2>为什么设计api详情规范</h2>
<p>主要是为了保证公司的小伙伴写出的api尽量少出bug,加快研发速度。</p>
<h2>api 设计规范</h2>
<ol>
<li><p>绝大部分api都请务必加入用户id限定,主要为了防止某个用户可以请求所有的数据库数据;</p></li>
<li><p>管理员api,必须加入 <code>@Secured(AuthorityConstants.ADMIN)</code> 保证该api只能使用管理员账号才能访问;</p></li>
<li><p>所有的api都必须加入严格的参数验证机制,保证参数不合法的时候请求快速报错,减少数据库以及微服务的压力;</p></li>
<li><p>所有的api都必须要保证api可以向前兼容,api以增加为主、修改为辅;</p></li>
<li><p>api控制层,只做参数验证,不做业务逻辑,所有的业务逻辑都应该放到业务层;</p></li>
</ol>
Spring Cloud Netflix Eureka: 多网卡环境下Eureka服务注册IP选择问题
https://segmentfault.com/a/1190000009232983
2017-04-28T14:33:04+08:00
2017-04-28T14:33:04+08:00
刘祖明
https://segmentfault.com/u/liuzuming
0
<p>今天遇到 Spring Cloud Netflix Eureka 的多网卡环境下Eureka服务注册IP选择问题,在研究了半天源码之后完全没有解决思路,就差点想着重新编译源码了。google到这篇文章完美解决我的问题。</p>
<p><a href="https://link.segmentfault.com/?enc=fMVC4GogXNekyrAtEF%2BCmA%3D%3D.txmrwUOlb%2BfPmDeGacYdoV%2ByWWHdFIkBS1PtO3CH5M9mnkv0Iu3tuY4h9MUj4H%2Bq2LcHGnQzaLUwIHbx%2F1I3sA%3D%3D" rel="nofollow">http://blog.csdn.net/neosmith...</a></p>
<p>看到别人能想到网卡忽略的配置文件,为啥我想不到呢。</p>
记录cors跨域错误
https://segmentfault.com/a/1190000009220751
2017-04-27T15:40:55+08:00
2017-04-27T15:40:55+08:00
刘祖明
https://segmentfault.com/u/liuzuming
1
<p>由于公司使用的是spring boot + spring cloud 将开发全部微服务化了,在微服务的过程中将前后端完全分离了。我们公司前端使用一个域名、后端api使用一个域名,这样前后端之间就产生了跨域问题。</p>
<h4>常见跨域问题</h4>
<p>我们最常见的跨域问题就是浏览器提示在A域名下不可以访问B域名的api,这样的错误很好解决,我们的解决方式是基于springboot框架的。</p>
<p>这样的问题我们只需要在后端加入cors配置,就可以解决这类跨域问题,我们在springmvc的java配置中加入以下代码即可</p>
<pre><code> @Bean
@ConditionalOnProperty(name = jhipster.cors.allowed-origins")
public CorsFilter corsFilter() {
log.debug("Registering CORS filter");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = jhipsterProperties.getCors();
source.registerCorsConfiguration("/api/**", config);
source.registerCorsConfiguration("/v2/api-docs", config);
source.registerCorsConfiguration("/oauth/**", config);
source.registerCorsConfiguration("/*/api/**", config);
source.registerCorsConfiguration("/*/oauth/**", config);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}</code></pre>
<h4>多值跨域问题</h4>
<p>错误信息:</p>
<pre><code>The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:7080,http://localhost:7080', but only one is allowed. Origin 'http://localhost:7080' is therefore not allowed access.</code></pre>
<p>错误信息很好理解,就是说<code>Access-Control-Allow-Origin</code>有两个值,但是浏览器只准许有一个值,所以报错。</p>
<p>我们查看浏览器中的网络请求:<br><img src="/img/bVMQP3?w=1552&h=738" alt="图片描述" title="图片描述"></p>
<p>我们发现在 <code>response headers</code> 中 <code>Access-Control-Allow-Origin</code> 出现了两次。由于是在 <code>response</code> 中这说明我们是在后端出现的问题。</p>
<p>由于我们的后端属于标准的微服务架构,也就是:<br>front -> api gateway -> other Micro service<br>由于我已经在 api gateway 项目中已经加入了 cors,那么在 api 网关(gateway)后面的微服务不在需要加入 cors 了。由于小伙伴在后端的微服务项目中也同样加入了cors,这样在返回的response中就加入了两个相同的跨域header,这样浏览器发现多个<code>Access-Control-Allow-Origin</code>于是就报错了。</p>
<p>找到原因错误就很好解决了,我们只需要在小伙伴的后端将cors跨域配置删除即可,只保留API gateway 项目的cors配置。</p>
spring boot 全局错误处理
https://segmentfault.com/a/1190000008443705
2017-02-22T17:14:17+08:00
2017-02-22T17:14:17+08:00
刘祖明
https://segmentfault.com/u/liuzuming
3
<p>这两天在做 spring cloud 的 API gateway 的时候,遇到了一个全局错误处理的坑,我在在 spring security 中加入了一个 filter,该 filter 用来验证 token 是否合法,如果该 token 不合法,就抛出自定义错误 InvalidTokenException ,并且要返回的状态码为403,告诉前端该用户未认证。</p>
<p>我开始以为使用 <code>@ControllerAdvice</code> 来定义全局错误处理即可,最后发现过滤器中抛出了错误 <code>@ControllerAdvice</code> 却无法捕获到,并抛出我需要的错误信息。最后阅读 spring 的官方文档发现,spring 的全局错误处理不是只有 <code>@ControllerAdvice</code>。</p>
<p><code>@ControllerAdvice</code> 主要处理的就是 <code>controller</code> 层的错误信息,而没有进入 <code>controller</code> 层的错误 <code>@ControllerAdvice</code> 是无法处理的,那么我需要另外的一个全局错误处理。</p>
<pre><code class="java">
@ControllerAdvice
public class ExceptionTranslator {
@ExceptionHandler(ConcurrencyFailureException.class)
@ResponseStatus(HttpStatus.CONFLICT)
@ResponseBody
public ErrorVM processConcurencyError(ConcurrencyFailureException ex) {
return new ErrorVM(ErrorConstants.ERR_CONCURRENCY_FAILURE);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorVM processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
@ExceptionHandler(CustomParameterizedException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ParameterizedErrorVM processParameterizedValidationError(CustomParameterizedException ex) {
return ex.getErrorVM();
}
@ExceptionHandler(InvalidTokenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ErrorVM processInvalidTokenException(InvalidTokenException ex) {
return new ErrorVM(ErrorConstants.INVALID_TOKEN, ex.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
@ResponseBody
public ErrorVM processAccessDeniedException(AccessDeniedException e) {
return new ErrorVM(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage());
}
private ErrorVM processFieldErrors(List<FieldError> fieldErrors) {
ErrorVM dto = Objects.nonNull(fieldErrors.get(0)) ? new ErrorVM(ErrorConstants.ERR_VALIDATION, fieldErrors.get(0).getDefaultMessage()) : new ErrorVM(ErrorConstants.ERR_VALIDATION);
for (FieldError fieldError : fieldErrors) {
dto.add(fieldError.getObjectName(), fieldError.getField(), fieldError.getCode());
}
return dto;
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, exception.getMessage());
}
@ExceptionHandler(CustomException.class)
@ResponseBody
@ResponseStatus(HttpStatus.IM_USED)
public ErrorVM processCustomException(CustomException ex) {
return new ErrorVM(ErrorConstants.ERR_CUSTOM, ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorVM> processRuntimeException(Exception ex) {
BodyBuilder builder;
ErrorVM errorVM;
ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
builder = ResponseEntity.status(responseStatus.value());
errorVM = new ErrorVM("error." + responseStatus.value().value(), responseStatus.reason());
} else {
builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
errorVM = new ErrorVM(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, "Internal server error");
}
return builder.body(errorVM);
}
}
</code></pre>
<p><code>BasicErrorController</code> 这个类就是用来捕获 <code>/error</code> 的所有错误,而过滤器中的错误会被重定向到 <code>/error</code>。我编写一个新的控制层类 <code>TokenErrorController</code> 并继承 <code>BasicErrorController</code> 类,这样错误 json 类型的错误都会被重定向到这个控制层里。</p>
<pre><code class="java">
@RestController
public class TokenErrorController extends BasicErrorController {
public TokenErrorController(){
super(new DefaultErrorAttributes(), new ErrorProperties());
}
private static final String PATH = "/error";
@RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
if (!Strings.isNullOrEmpty((String)body.get("exception")) && body.get("exception").equals(InvalidTokenException.class.getName())){
body.put("status", HttpStatus.FORBIDDEN.value());
status = HttpStatus.FORBIDDEN;
}
return new ResponseEntity<Map<String, Object>>(body, status);
}
@Override
public String getErrorPath() {
return PATH;
}
}
</code></pre>
websocket配合spring-security使用token认证
https://segmentfault.com/a/1190000007853460
2016-12-19T18:08:52+08:00
2016-12-19T18:08:52+08:00
刘祖明
https://segmentfault.com/u/liuzuming
6
<h2>使用框架介绍</h2>
<ul>
<li><p>spring boot 1.4.3.RELEASE</p></li>
<li><p>spring websocket 4.3.5.RELEASE</p></li>
<li><p>spring security 4.1.3.RELEASE</p></li>
<li><p>sockjs-client 1.0.2</p></li>
<li><p>stompjs 2.3.3</p></li>
</ul>
<h2>项目介绍</h2>
<p>由于公司需要使用websocket主动给前端用户推送消息,公司的项目是使用jhipster自动生成的微服务项目,而spring boot本身就集成了websocket,这样我们不用自己处理所有的网络细节代码。我们的项目主要为:<br>前端 - nodeJS代理 - 后端 - 计算系统(由于我们公司是做云计算的,计算系统是一个底层系统)<br>项目的主要流程是:<img src="/img/bVHHD8?w=2202&h=356" alt="请求以及数据流向" title="请求以及数据流向"></p>
<h2>遇到的问题</h2>
<p>由于我们使用的是spring security oauth2 来进行认证,而且我们需要吧websocket消息推送给指定用户,这样为了保证websocket和http协议使用的同一套认证系统,我们就必须要把websocket认证集成到spring security中。</p>
<h3>第一个问题认证403错误</h3>
<p>首先贴出websocket的配置代码</p>
<pre><code class="java">
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final static Logger LOG = LoggerFactory.getLogger(WebSocketConfig.class);
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/api/v1/socket/send"); // 推送消息前缀
registry.setApplicationDestinationPrefixes("/api/v1/socket/req"); // 应用请求前缀
registry.setUserDestinationPrefix("/user");//推送用户前缀
}
/**
* 建立连接的端点
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/v1/socket/fallback").setAllowedOrigins("*").withSockJS().setInterceptors(httpSessionHandshakeInterceptor());
}
}</code></pre>
<p>第一次在打开websocket的时候发现三次握手都没有出现就直接报403,最后仔细看错误信息发现错误信息的链接为:<code>/api/v1/socket/fallback/info</code>,而且该请求使用的是http请求不是websocket请求。最后发现每一个websocket在连接端点之前都会发送一个http GET请求用于保证该服务是存在的。而该请求是程序自动发送的自能自动携带cookie数据,无法发送自定义header。</p>
<p>spring boot自带的认证是:如果<code>/api/v1/socket/fallback/info</code>该请求通过认证,那么websocket的所有请求以及发送全部自动绑定该认证用户。如果我们想办法让<code>/api/v1/socket/fallback/info</code>请求通过认证,那么接下来所有的问题都将解决。问题是我们的token是使用自定义header实现的认证。所以该方法不成立,所以只能让websocket自己认证。</p>
<p>为了解决<code>/api/v1/socket/fallback/info</code>请求的403问题我在安全配置中加入<code>.authorizeRequests().antMatchers("/api/v1/socket/fallback/**").permitAll()</code>这样第一步判断服务是否存在就解决了,这里离解决websocket的认证问题只是第一步。</p>
<h3>第二个问题:如果发送token给后端</h3>
<p>stomp 客户端可以直接在websocket请求中加入自定义header,如下:</p>
<pre><code class="javascript">let socket = new SockJS('/api/v1/socket/fallback')
let stompClient = Stomp.over(socket)
let token = localStorage.getItem('Auth-Token') // eslint-disable-line
stompClient.connect({'Auth-Token': token}, frame => {
stompClient.subscribe('/user/api/v1/socket/send/greetings', data => {
// TODO
})
})</code></pre>
<h3>第三个问题:后端如何认证</h3>
<p>我们在创建连接的时候前端需要将token发送到后端,现在我们已经将token发送到后端了,但是后端如何接受并处理token得到认证数据呢?带着这个问题开始google吧!<code>http://stackoverflow.com/questions/39422053/spring-4-x-token-based-websocket-sockjs-fallback-authentication</code>这个链接正好解决了我的问题,</p>
<pre><code>UPDATE 2016-12-13 : the issue referenced below is now marked fixed, so the hack below is no longer necessary which Spring 4.3.5 or above. See https://github.com/spring-projects/spring-framework/blob/master/src/asciidoc/web-websocket.adoc#token-based-authentication.</code></pre>
<p>原来这个问题在4.3.5版本中已经被继承进去了,查看自己的版本是4.3.4,不解释直接升级版本到4.3.5,然后加如代码</p>
<pre><code>@EnableWebSocketMessageBroker
public class MyConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
if (StringUtils.isNotEmpty(jwtToken)) {
UserAuthenticationToken authToken = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authToken);
accessor.setUser(authToken);
}
}
return message;
}
});
}
}</code></pre>
<p>开始测试,发现还是报错<code>MissingCsrfTokenException</code>,然后开始debug代码,发现代码错误的代码为:</p>
<pre><code class="java">public final class CsrfChannelInterceptor extends ChannelInterceptorAdapter {
private final MessageMatcher<Object> matcher;
public CsrfChannelInterceptor() {
this.matcher = new SimpMessageTypeMatcher(SimpMessageType.CONNECT);
}
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if(!this.matcher.matches(message)) {
return message;
} else {
Map sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
CsrfToken expectedToken = sessionAttributes == null?null:(CsrfToken)sessionAttributes.get(CsrfToken.class.getName());
if(expectedToken == null) { // 在这里为null
throw new MissingCsrfTokenException((String)null); //报错
} else {
String actualTokenValue = SimpMessageHeaderAccessor.wrap(message).getFirstNativeHeader(expectedToken.getHeaderName());
boolean csrfCheckPassed = expectedToken.getToken().equals(actualTokenValue);
if(csrfCheckPassed) {
return message;
} else {
throw new InvalidCsrfTokenException(expectedToken, actualTokenValue);
}
}
}
}
}</code></pre>
<p>仔细查看里面的数据,原来这里是需要在header中存放一些数据,于是乎将configureClientInboundChannel方法修正为:</p>
<pre><code class="java">
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
LOG.debug("webSocket token is {}", jwtToken);
if (StringUtils.isNotEmpty(jwtToken)) {
Map sessionAttributes = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
sessionAttributes.put(CsrfToken.class.getName(), new DefaultCsrfToken("Auth-Token", "Auth-Token", jwtToken));
UserAuthenticationToken authToken = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authToken);
accessor.setUser(authToken);
}
}
return message;
}
});
}</code></pre>
<p>然后修改websocket安全配置为:</p>
<pre><code class="java">@Configuration
public class WebsocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.anyMessage().permitAll();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}</code></pre>
<p>这样websocket 集成spring boot token的认证就搞定了。</p>