最近看到一个有趣的开源项目pig,主要的技术点在认证授权中心,spring security oauth,zuul网关实现,Elastic-Job定时任务,趁着刚刚入门微服务,赶快写个博客分析一下。此篇文章主要用于个人备忘。如果有不对,请批评。?

由于每个模块篇幅较长,且部分内容和前文有重叠,干货和图片较少,阅读时使用旁边的导航功能体验较佳。?

想要解锁更多新姿势?请访问https://blog.tengshe789.tech/

说明

本篇文章是对基于spring boot1.5的pig 1版本做的分析,不是收费的pigx 2版本。

开源项目地址

https://gitee.com/log4j/pig

配置中心:https://gitee.com/cqzqxq_lxh/pig-config

冷冷官方地址

https://pig4cloud.com/zh-cn/index.html

体验地址

http://pigx.pig4cloud.com/#/wel/index

项目启动顺序

请确保启动顺序(要先启动认证中心,再启动网关

  1. eureka
  2. config
  3. auth
  4. gateway
  5. upms

认证中心

老规矩,自上到下看代码,先从接口层看起

请求rest接口

@RestController
@RequestMapping("/authentication")
public class AuthenticationController {@Autowired@Qualifier("consumerTokenServices")private ConsumerTokenServices consumerTokenServices;/*** 认证页面* @return ModelAndView*/@GetMapping("/require")public ModelAndView require() {return new ModelAndView("ftl/login");}/*** 用户信息校验* @param authentication 信息* @return 用户信息*/@RequestMapping("/user")public Object user(Authentication authentication) {return authentication.getPrincipal();}/*** 清除Redis中 accesstoken refreshtoken** @param accesstoken  accesstoken* @return true/false*/@PostMapping("/removeToken")@CacheEvict(value = SecurityConstants.TOKEN_USER_DETAIL, key = "#accesstoken")public R<Boolean> removeToken(String accesstoken) {return new R<>( consumerTokenServices.revokeToken(accesstoken));}
}

接口层有三个接口路径,第一个应该没用,剩下两个是校验用户信息的/user和清除Redis中 accesstoken 与refreshtoken的/removeToken

框架配置

框架配置

下面这段代码时配置各种spring security配置,包括登陆界面url是"/authentication/require"啦。如果不使用默认的弹出框而使用自己的页面,表单的action是"/authentication/form"啦。使用自己定义的过滤规则啦。禁用csrf啦(自行搜索csrf,jwt验证不需要防跨域,但是需要使用xss过滤)。使用手机登陆配置啦。

@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER - 1)
@Configuration
@EnableWebSecurity
public class PigSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {@Autowiredprivate FilterIgnorePropertiesConfig filterIgnorePropertiesConfig;@Autowiredprivate MobileSecurityConfigurer mobileSecurityConfigurer;@Overridepublic void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry =http.formLogin().loginPage("/authentication/require").loginProcessingUrl("/authentication/form").and().authorizeRequests();filterIgnorePropertiesConfig.getUrls().forEach(url -> registry.antMatchers(url).permitAll());registry.anyRequest().authenticated().and().csrf().disable();http.apply(mobileSecurityConfigurer);}
}

校验用户信息

读配置类和接口层,我们知道了,总的逻辑大概就是用户登陆了以后,使用spring security框架的认证来获取权限。

我们一步一步看,边猜想边来。接口处有"ftl/login",这大概就是使用freemarker模板,login信息携带的token会传到用户信息校验url"/user"上,可作者直接使用Authentication返回一个getPrincipal(),就没了,根本没看见自定义的代码,这是怎么回事呢?

原来,作者使用spring security框架,使用框架来实现校验信息。

打卡config包下的PigAuthorizationConfig,我们来一探究竟。

使用spring security 实现 授权服务器

注明,阅读此处模块需要OAUTH基础,https://blog.tengshe789.tech/2018/12/02/感性认识jwt/#more

这里简单提一下,spring security oauth里有两个概念,授权服务器和资源服务器。

授权服务器是根据授权许可给访问的客户端发放access token令牌的,提供认证、授权服务;

资源服务器需要验证这个access token,客户端才能访问对应服务。

客户详细信息服务配置

ClientDetailsServiceConfigurer(AuthorizationServerConfigurer 的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),Spring Security OAuth2的配置方法是编写@Configuration类继承AuthorizationServerConfigurerAdapter,然后重写void configure(ClientDetailsServiceConfigurer clients)方法

下面代码主要逻辑是,使用spring security框架封装的简单sql连接器,查询客户端的详细信息?

 @Overridepublic void configure(` clients) throws Exception {JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);clientDetailsService.setSelectClientDetailsSql(SecurityConstants.DEFAULT_SELECT_STATEMENT);clientDetailsService.setFindClientDetailsSql(SecurityConstants.DEFAULT_FIND_STATEMENT);clients.withClientDetails(clientDetailsService);}

相关的sql语句如下,由于耦合度较大,我将sql声明语句改了一改,方面阅读:

 /*** 默认的查询语句*/String DEFAULT_FIND_STATEMENT = "select " + "client_id, client_secret, resource_ids, scope, "+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "+ "refresh_token_validity, additional_information, autoapprove"+ " from sys_oauth_client_details" + " order by client_id";/*** 按条件client_id 查询*/String DEFAULT_SELECT_STATEMENT = "select " +"client_id, client_secret, resource_ids, scope, "+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "+ "refresh_token_validity, additional_information, autoapprove"+ " from sys_oauth_client_details" + " where client_id = ?";

相关数据库信息如下:

授权服务器端点配置器

endpoints参数是什么?所有获取令牌的请求都将会在Spring MVC controller endpoints中进行处理

@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {//token增强配置TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));endpoints.tokenStore(redisTokenStore()).tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager).reuseRefreshTokens(false).userDetailsService(userDetailsService);}
token增强器(自定义token信息中携带的信息)

有时候需要额外的信息加到token返回中,这部分也可以自定义,此时我们可以自定义一个TokenEnhancer,来自定义生成token携带的信息。TokenEnhancer接口提供一个 enhance(OAuth2AccessToken var1, OAuth2Authentication var2) 方法,用于对token信息的添加,信息来源于OAuth2Authentication

作者将生成的accessToken中,加上了自己的名字,加上了userId

@Beanpublic TokenEnhancer tokenEnhancer() {return (accessToken, authentication) -> {final Map<String, Object> additionalInfo = new HashMap<>(2);additionalInfo.put("license", SecurityConstants.PIG_LICENSE);UserDetailsImpl user = (UserDetailsImpl) authentication.getUserAuthentication().getPrincipal();if (user != null) {additionalInfo.put("userId", user.getUserId());}((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);return accessToken;};}
JWT转换器(自定义token信息中添加的信息)

JWT中,需要在token中携带额外的信息,这样可以在服务之间共享部分用户信息,spring security默认在JWT的token中加入了user_name,如果我们需要额外的信息,需要自定义这部分内容。

JwtAccessTokenConverter是使用JWT替换默认的Token的转换器,而token令牌默认是有签名的,且资源服务器需要验证这个签名。此处的加密及验签包括两种方式:

  • 对称加密

  • 非对称加密(公钥密钥)

对称加密需要授权服务器和资源服务器存储同一key值,而非对称加密可使用密钥加密,暴露公钥给资源服务器验签

public class PigJwtAccessTokenConverter extends JwtAccessTokenConverter {@Overridepublic Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {Map<String, Object> representation = (Map<String, Object>) super.convertAccessToken(token, authentication);representation.put("license", SecurityConstants.PIG_LICENSE);return representation;}@Overridepublic OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {return super.extractAccessToken(value, map);}@Overridepublic OAuth2Authentication extractAuthentication(Map<String, ?> map) {return super.extractAuthentication(map);}
}
redis与token

使用鉴权的endpoint将加上自己名字的token放入redis,redis连接器用的srping data redis框架

 /*** tokenstore 定制化处理** @return TokenStore* 1. 如果使用的 redis-cluster 模式请使用 PigRedisTokenStore* PigRedisTokenStore tokenStore = new PigRedisTokenStore();* tokenStore.setRedisTemplate(redisTemplate);*/@Beanpublic TokenStore redisTokenStore() {RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);tokenStore.setPrefix(SecurityConstants.PIG_PREFIX);return tokenStore;}

授权服务器安全配置器

@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()").checkTokenAccess("permitAll()");}

自定义实现的手机号 认证服务

接口层

先看接口层,这里和pig-upms-service联动,给了三个路径,用户使用手机号码登陆可通过三个路径发送请求

@FeignClient(name = "pig-upms-service", fallback = UserServiceFallbackImpl.class)
public interface UserService {/*** 通过用户名查询用户、角色信息** @param username 用户名* @return UserVo*/@GetMapping("/user/findUserByUsername/{username}")UserVO findUserByUsername(@PathVariable("username") String username);/*** 通过手机号查询用户、角色信息** @param mobile 手机号* @return UserVo*/@GetMapping("/user/findUserByMobile/{mobile}")UserVO findUserByMobile(@PathVariable("mobile") String mobile);/*** 根据OpenId查询用户信息* @param openId openId* @return UserVo*/@GetMapping("/user/findUserByOpenId/{openId}")UserVO findUserByOpenId(@PathVariable("openId") String openId);
}

配置类

重写SecurityConfigurerAdapter的方法,通过http请求,找出有关手机号的token,用token找出相关用户的信息,已Authentication方式保存。拿到信息后,使用过滤器验证

@Component
public class MobileSecurityConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate AuthenticationSuccessHandler mobileLoginSuccessHandler;@Autowiredprivate UserService userService;@Overridepublic void configure(HttpSecurity http) throws Exception {MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileLoginSuccessHandler);MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();mobileAuthenticationProvider.setUserService(userService);http.authenticationProvider(mobileAuthenticationProvider).addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}
}

手机号登录校验逻辑MobileAuthenticationProvider

spring security 中,AuthenticationManage管理一系列的AuthenticationProvider
而每一个Provider都会通UserDetailsServiceUserDetail来返回一个
MobileAuthenticationToken实现的带用户以及权限的Authentication

此处逻辑是,通过UserService查找已有用户的手机号码,生成对应的UserDetails,使用UserDetails生成手机验证Authentication

@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;UserVO userVo = userService.findUserByMobile((String) mobileAuthenticationToken.getPrincipal());if (userVo == null) {throw new UsernameNotFoundException("手机号不存在:" + mobileAuthenticationToken.getPrincipal());}UserDetailsImpl userDetails = buildUserDeatils(userVo);MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities());authenticationToken.setDetails(mobileAuthenticationToken.getDetails());return authenticationToken;}private UserDetailsImpl buildUserDeatils(UserVO userVo) {return new UserDetailsImpl(userVo);}@Overridepublic boolean supports(Class<?> authentication) {return MobileAuthenticationToken.class.isAssignableFrom(authentication);}
手机号登录令牌类MobileAuthenticationToken

MobileAuthenticationToken继承AbstractAuthenticationToken实现Authentication
所以当在页面中输入手机之后首先会进入到MobileAuthenticationToken验证(Authentication),
然后生成的Authentication会被交由我上面说的AuthenticationManager来进行管理

public class MobileAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;private final Object principal;public MobileAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}public MobileAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic Object getCredentials() {return null;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}

手机号登录验证filter

判断http请求是否是post,不是则返回错误。

根据request请求拿到moblie信息,使用moblie信息返回手机号码登陆成功的oauth token。

@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String mobile = obtainMobile(request);if (mobile == null) {mobile = "";}mobile = mobile.trim();MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile);setDetails(request, mobileAuthenticationToken);return this.getAuthenticationManager().authenticate(mobileAuthenticationToken);}

手机登陆成功的处理器MobileLoginSuccessHandler

这个处理器可以返回手机号登录成功的oauth token,但是要将oauth token传输出去必须配合上面的手机号登录验证filter

逻辑都在注释中

@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {String header = request.getHeader("Authorization");if (header == null || !header.startsWith(BASIC_)) {throw new UnapprovedClientAuthenticationException("请求头中client信息为空");}try {String[] tokens = AuthUtils.extractAndDecodeHeader(header);assert tokens.length == 2;String clientId = tokens[0];ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);//校验secretif (!clientDetails.getClientSecret().equals(tokens[1])) {throw new InvalidClientException("Given client ID does not match authenticated client");}TokenRequest tokenRequest = new TokenRequest(MapUtil.newHashMap(), clientId, clientDetails.getScope(), "mobile");//校验scopenew DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);log.info("获取token 成功:{}", oAuth2AccessToken.getValue());response.setCharacterEncoding(CommonConstant.UTF8);response.setContentType(CommonConstant.CONTENT_TYPE);PrintWriter printWriter = response.getWriter();printWriter.append(objectMapper.writeValueAsString(oAuth2AccessToken));} catch (IOException e) {throw new BadCredentialsException("Failed to decode basic authentication token");}}/*** 从header 请求中的clientId/clientsecect** @param header header中的参数* @throws CheckedException if the Basic header is not present or is not valid*                          Base64*/public static String[] extractAndDecodeHeader(String header)throws IOException {byte[] base64Token = header.substring(6).getBytes("UTF-8");byte[] decoded;try {decoded = Base64.decode(base64Token);} catch (IllegalArgumentException e) {throw new CheckedException("Failed to decode basic authentication token");}String token = new String(decoded, CommonConstant.UTF8);int delim = token.indexOf(":");if (delim == -1) {throw new CheckedException("Invalid basic authentication token");}return new String[]{token.substring(0, delim), token.substring(delim + 1)};}

其他配置

redis集群

挺好的模板,收藏一下

public class PigRedisTokenStore implements TokenStore {private static final String ACCESS = "access:";private static final String AUTH_TO_ACCESS = "auth_to_access:";private static final String AUTH = "auth:";private static final String REFRESH_AUTH = "refresh_auth:";private static final String ACCESS_TO_REFRESH = "access_to_refresh:";private static final String REFRESH = "refresh:";private static final String REFRESH_TO_ACCESS = "refresh_to_access:";private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";private static final String UNAME_TO_ACCESS = "uname_to_access:";private RedisTemplate<String, Object> redisTemplate;public RedisTemplate<String, Object> getRedisTemplate() {return redisTemplate;}public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {this.authenticationKeyGenerator = authenticationKeyGenerator;}@Overridepublic OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {String key = authenticationKeyGenerator.extractKey(authentication);OAuth2AccessToken accessToken = (OAuth2AccessToken) redisTemplate.opsForValue().get(AUTH_TO_ACCESS + key);if (accessToken != null&& !key.equals(authenticationKeyGenerator.extractKey(readAuthentication(accessToken.getValue())))) {storeAccessToken(accessToken, authentication);}return accessToken;}@Overridepublic OAuth2Authentication readAuthentication(OAuth2AccessToken token) {return readAuthentication(token.getValue());}@Overridepublic OAuth2Authentication readAuthentication(String token) {return (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + token);}@Overridepublic OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {return readAuthenticationForRefreshToken(token.getValue());}public OAuth2Authentication readAuthenticationForRefreshToken(String token) {return (OAuth2Authentication) this.redisTemplate.opsForValue().get(REFRESH_AUTH + token);}@Overridepublic void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {this.redisTemplate.opsForValue().set(ACCESS + token.getValue(), token);this.redisTemplate.opsForValue().set(AUTH + token.getValue(), authentication);this.redisTemplate.opsForValue().set(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication), token);if (!authentication.isClientOnly()) {redisTemplate.opsForList().rightPush(UNAME_TO_ACCESS + getApprovalKey(authentication), token);}redisTemplate.opsForList().rightPush(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(), token);if (token.getExpiration() != null) {int seconds = token.getExpiresIn();redisTemplate.expire(ACCESS + token.getValue(), seconds, TimeUnit.SECONDS);redisTemplate.expire(AUTH + token.getValue(), seconds, TimeUnit.SECONDS);redisTemplate.expire(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication), seconds, TimeUnit.SECONDS);redisTemplate.expire(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId(), seconds, TimeUnit.SECONDS);redisTemplate.expire(UNAME_TO_ACCESS + getApprovalKey(authentication), seconds, TimeUnit.SECONDS);}if (token.getRefreshToken() != null && token.getRefreshToken().getValue() != null) {this.redisTemplate.opsForValue().set(REFRESH_TO_ACCESS + token.getRefreshToken().getValue(), token.getValue());this.redisTemplate.opsForValue().set(ACCESS_TO_REFRESH + token.getValue(), token.getRefreshToken().getValue());}}private String getApprovalKey(OAuth2Authentication authentication) {String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication().getName();return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);}private String getApprovalKey(String clientId, String userName) {return clientId + (userName == null ? "" : ":" + userName);}@Overridepublic void removeAccessToken(OAuth2AccessToken accessToken) {removeAccessToken(accessToken.getValue());}@Overridepublic OAuth2AccessToken readAccessToken(String tokenValue) {return (OAuth2AccessToken) this.redisTemplate.opsForValue().get(ACCESS + tokenValue);}public void removeAccessToken(String tokenValue) {OAuth2AccessToken removed = (OAuth2AccessToken) redisTemplate.opsForValue().get(ACCESS + tokenValue);// caller to do thatOAuth2Authentication authentication = (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + tokenValue);this.redisTemplate.delete(AUTH + tokenValue);redisTemplate.delete(ACCESS + tokenValue);this.redisTemplate.delete(ACCESS_TO_REFRESH + tokenValue);if (authentication != null) {this.redisTemplate.delete(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));String clientId = authentication.getOAuth2Request().getClientId();redisTemplate.opsForList().leftPop(UNAME_TO_ACCESS + getApprovalKey(clientId, authentication.getName()));redisTemplate.opsForList().leftPop(CLIENT_ID_TO_ACCESS + clientId);this.redisTemplate.delete(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));}}@Overridepublic void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {this.redisTemplate.opsForValue().set(REFRESH + refreshToken.getValue(), refreshToken);this.redisTemplate.opsForValue().set(REFRESH_AUTH + refreshToken.getValue(), authentication);}@Overridepublic OAuth2RefreshToken readRefreshToken(String tokenValue) {return (OAuth2RefreshToken) this.redisTemplate.opsForValue().get(REFRESH + tokenValue);}@Overridepublic void removeRefreshToken(OAuth2RefreshToken refreshToken) {removeRefreshToken(refreshToken.getValue());}public void removeRefreshToken(String tokenValue) {this.redisTemplate.delete(REFRESH + tokenValue);this.redisTemplate.delete(REFRESH_AUTH + tokenValue);this.redisTemplate.delete(REFRESH_TO_ACCESS + tokenValue);}@Overridepublic void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {removeAccessTokenUsingRefreshToken(refreshToken.getValue());}private void removeAccessTokenUsingRefreshToken(String refreshToken) {String token = (String) this.redisTemplate.opsForValue().get(REFRESH_TO_ACCESS + refreshToken);if (token != null) {redisTemplate.delete(ACCESS + token);}}@Overridepublic Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {List<Object> result = redisTemplate.opsForList().range(UNAME_TO_ACCESS + getApprovalKey(clientId, userName), 0, -1);if (result == null || result.size() == 0) {return Collections.emptySet();}List<OAuth2AccessToken> accessTokens = new ArrayList<>(result.size());for (Iterator<Object> it = result.iterator(); it.hasNext(); ) {OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next();accessTokens.add(accessToken);}return Collections.unmodifiableCollection(accessTokens);}@Overridepublic Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {List<Object> result = redisTemplate.opsForList().range((CLIENT_ID_TO_ACCESS + clientId), 0, -1);if (result == null || result.size() == 0) {return Collections.emptySet();}List<OAuth2AccessToken> accessTokens = new ArrayList<>(result.size());for (Iterator<Object> it = result.iterator(); it.hasNext(); ) {OAuth2AccessToken accessToken = (OAuth2AccessToken) it.next();accessTokens.add(accessToken);}return Collections.unmodifiableCollection(accessTokens);}
}

服务网关模块

网关主体在包pig\pig-gateway\src\main\java\com\github\pig\gateway

作者使用了Zuul做为网关,它Netflix开源的微服务网关,可以和Eureka,Ribbon,Hystrix等组件配合使用。

Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:

  • 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求

  • 审查与监控:

  • 动态路由:动态将请求路由到不同后端集群

  • 压力测试:逐渐增加指向集群的流量,以了解性能

  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求

  • 静态响应处理:边缘位置进行响应,避免转发到内部集群

  • 多区域弹性:跨域AWS Region进行请求路由,旨在实现ELB(ElasticLoad Balancing)使用多样化

多种功能的过滤器过滤器

Zuul组件的核心是一系列的过滤器,我们先从过滤器下手。

网关统一异常过滤器

@Component
public class ErrorHandlerFilter extends ZuulFilter {@Autowiredprivate LogSendService logSendService;@Overridepublic String filterType() {return ERROR_TYPE;}@Overridepublic int filterOrder() {return SEND_RESPONSE_FILTER_ORDER + 1;}@Overridepublic boolean shouldFilter() {RequestContext requestContext = RequestContext.getCurrentContext();return requestContext.getThrowable() != null;}@Overridepublic Object run() {RequestContext requestContext = RequestContext.getCurrentContext();logSendService.send(requestContext);return null;}
}

作者以原生zuul过滤器为基础加了日志配置,优先级为+1,数字越大优先级越低。

XSS过滤器

public class XssSecurityFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper(request);filterChain.doFilter(xssRequest, response);}

重写springMVC里面的的确保在一次请求只通过一次filter的类OncePerRequestFilter,添加一条https://gitee.com/renrenio/renren-fast的工具类XssHttpServletRequestWrapper为过滤链条。

@Overridepublic ServletInputStream getInputStream() throws IOException {····略//xss过滤json = xssEncode(json);final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8"));return new ServletInputStream() {···略}};}

密码过滤器DecodePasswordFilter

此过滤器优先级为+2.每当一个请求不是请求/oauth/token或者/mobile/token这个地址时,都会解析使用aes解码器password

@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();Map<String, List<String>> params = ctx.getRequestQueryParams();if (params == null) {return null;}List<String> passList = params.get(PASSWORD);if (CollUtil.isEmpty(passList)) {return null;}String password = passList.get(0);if (StrUtil.isNotBlank(password)) {try {password = decryptAES(password, key);} catch (Exception e) {log.error("密码解密失败:{}", password);}params.put(PASSWORD, CollUtil.newArrayList(password.trim()));}ctx.setRequestQueryParams(params);return null;}

校验码过滤器ValidateCodeFilter

逻辑作者都写在注释中了,此处使用了redis做为服务端验证码的缓存

*** 是否校验验证码* 1. 判断验证码开关是否开启* 2. 判断请求是否登录请求* 2.1 判断是不是刷新请求(不用单独在建立刷新客户端)* 3. 判断终端是否支持** @return true/false*/@Overridepublic boolean shouldFilter() {HttpServletRequest request = RequestContext.getCurrentContext().getRequest();if (!StrUtil.containsAnyIgnoreCase(request.getRequestURI(),SecurityConstants.OAUTH_TOKEN_URL, SecurityConstants.MOBILE_TOKEN_URL)) {return false;}if (SecurityConstants.REFRESH_TOKEN.equals(request.getParameter(GRANT_TYPE))) {return false;}try {String[] clientInfos = AuthUtils.extractAndDecodeHeader(request);if (CollUtil.containsAny(filterIgnorePropertiesConfig.getClients(), Arrays.asList(clientInfos))) {return false;}} catch (IOException e) {log.error("解析终端信息失败", e);}return true;}@Overridepublic Object run() {try {checkCode(RequestContext.getCurrentContext().getRequest());} catch (ValidateCodeException e) {RequestContext ctx = RequestContext.getCurrentContext();R<String> result = new R<>(e);result.setCode(478);ctx.setResponseStatusCode(478);ctx.setSendZuulResponse(false);ctx.getResponse().setContentType("application/json;charset=UTF-8");ctx.setResponseBody(JSONObject.toJSONString(result));}return null;}/*** 检查code** @param httpServletRequest request* @throws ValidateCodeException 验证码校验异常*/private void checkCode(HttpServletRequest httpServletRequest) throws ValidateCodeException {String code = httpServletRequest.getParameter("code");if (StrUtil.isBlank(code)) {throw new ValidateCodeException("请输入验证码");}String randomStr = httpServletRequest.getParameter("randomStr");if (StrUtil.isBlank(randomStr)) {randomStr = httpServletRequest.getParameter("mobile");}String key = SecurityConstants.DEFAULT_CODE_KEY + randomStr;if (!redisTemplate.hasKey(key)) {throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR);}Object codeObj = redisTemplate.opsForValue().get(key);if (codeObj == null) {throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR);}String saveCode = codeObj.toString();if (StrUtil.isBlank(saveCode)) {redisTemplate.delete(key);throw new ValidateCodeException(EXPIRED_CAPTCHA_ERROR);}if (!StrUtil.equals(saveCode, code)) {redisTemplate.delete(key);throw new ValidateCodeException("验证码错误,请重新输入");}redisTemplate.delete(key);}

灰度发布

灰度发布,已经不是一个很新的概念了.一个产品,如果需要快速迭代开发上线,又要保证质量,保证刚上线的系统,一旦出现问题那么可以很快的控制影响面,就需要设计一套灰度发布系统.

灰度发布系统的作用在于,可以根据自己的配置,来将用户的流量导到新上线的系统上,来快速验证新的功能修改,而一旦出问题,也可以马上的恢复,简单的说,就是一套A/BTest系统.

初始化

下面是灰度路由初始化类:

@Configuration
@ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)
@AutoConfigureBefore(RibbonClientConfiguration.class)
@ConditionalOnProperty(value = "zuul.ribbon.metadata.enabled")
public class RibbonMetaFilterAutoConfiguration {@Bean@ConditionalOnMissingBean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public ZoneAvoidanceRule metadataAwareRule() {return new MetadataCanaryRuleHandler();}
}

灰度发布有关过滤器AccessFilter

首先重写filterOrder()方法,使这个过滤器在在RateLimitPreFilter之前运行,不会出现空指针问题。此处优先级FORM_BODY_WRAPPER_FILTER_ORDER-1.

@Component
public class AccessFilter extends ZuulFilter {@Value("${zuul.ribbon.metadata.enabled:false}")private boolean canary;@Overridepublic String filterType() {return FilterConstants.PRE_TYPE;}@Overridepublic int filterOrder() {return FORM_BODY_WRAPPER_FILTER_ORDER - 1;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {RequestContext requestContext = RequestContext.getCurrentContext();String version = requestContext.getRequest().getHeader(SecurityConstants.VERSION);if (canary && StrUtil.isNotBlank(version)) {RibbonVersionHolder.setContext(version);}requestContext.set("startTime", System.currentTimeMillis());Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null) {requestContext.addZuulRequestHeader(SecurityConstants.USER_HEADER, authentication.getName());requestContext.addZuulRequestHeader(SecurityConstants.ROLE_HEADER, CollectionUtil.join(authentication.getAuthorities(), ","));}return null;}
}

核心方法在run()上,首先受到request请求,拿到他的版本约束信息,然后根据选择添加token

路由微服务断言处理器MetadataCanaryRuleHandler

自定义ribbon路由规则匹配多版本请求,实现灰度发布。复合判断server所在区域的性能和server的可用性选择server,即,使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。

此处逻辑是

  1. eureka metadata (主机名,IP地址,端口号,状态页健康检查等信息,或者通过配置文件自定义元数据)存在版本定义时候进行判断
  2. 不存在 metadata 直接返回true
@Overridepublic AbstractServerPredicate getPredicate() {return new AbstractServerPredicate() {@Overridepublic boolean apply(PredicateKey predicateKey) {String targetVersion = RibbonVersionHolder.getContext();RibbonVersionHolder.clearContext();if (StrUtil.isBlank(targetVersion)) {log.debug("客户端未配置目标版本直接路由");return true;}DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer();final Map<String, String> metadata = server.getInstanceInfo().getMetadata();if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) {log.debug("当前微服务{} 未配置版本直接路由");return true;}if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) {return true;} else {log.debug("当前微服务{} 版本为{},目标版本{} 匹配失败", server.getInstanceInfo().getAppName(), metadata.get(SecurityConstants.VERSION), targetVersion);return false;}}};}

动态路由

配置

public class DynamicRouteLocator extends DiscoveryClientRouteLocator {private ZuulProperties properties;private RedisTemplate redisTemplate;public DynamicRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties,ServiceInstance localServiceInstance, RedisTemplate redisTemplate) {super(servletPath, discovery, properties, localServiceInstance);this.properties = properties;this.redisTemplate = redisTemplate;}/*** 重写路由配置* <p>* 1. properties 配置。* 2. eureka 默认配置。* 3. DB数据库配置。** @return 路由表*/@Overrideprotected LinkedHashMap<String, ZuulProperties.ZuulRoute> locateRoutes() {LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();//读取properties配置、eureka默认配置routesMap.putAll(super.locateRoutes());log.debug("初始默认的路由配置完成");routesMap.putAll(locateRoutesFromDb());LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {String path = entry.getKey();if (!path.startsWith("/")) {path = "/" + path;}if (StrUtil.isNotBlank(this.properties.getPrefix())) {path = this.properties.getPrefix() + path;if (!path.startsWith("/")) {path = "/" + path;}}values.put(path, entry.getValue());}return values;}/*** Redis中保存的,没有从upms拉去,避免启动链路依赖问题(取舍),网关依赖业务模块的问题** @return*/private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDb() {Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();Object obj = redisTemplate.opsForValue().get(CommonConstant.ROUTE_KEY);if (obj == null) {return routes;}List<SysZuulRoute> results = (List<SysZuulRoute>) obj;for (SysZuulRoute result : results) {if (StrUtil.isBlank(result.getPath()) && StrUtil.isBlank(result.getUrl())) {continue;}ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();try {zuulRoute.setId(result.getServiceId());zuulRoute.setPath(result.getPath());zuulRoute.setServiceId(result.getServiceId());zuulRoute.setRetryable(StrUtil.equals(result.getRetryable(), "0") ? Boolean.FALSE : Boolean.TRUE);zuulRoute.setStripPrefix(StrUtil.equals(result.getStripPrefix(), "0") ? Boolean.FALSE : Boolean.TRUE);zuulRoute.setUrl(result.getUrl());List<String> sensitiveHeadersList = StrUtil.splitTrim(result.getSensitiveheadersList(), ",");if (sensitiveHeadersList != null) {Set<String> sensitiveHeaderSet = CollUtil.newHashSet();sensitiveHeadersList.forEach(sensitiveHeader -> sensitiveHeaderSet.add(sensitiveHeader));zuulRoute.setSensitiveHeaders(sensitiveHeaderSet);zuulRoute.setCustomSensitiveHeaders(true);}} catch (Exception e) {log.error("从数据库加载路由配置异常", e);}log.debug("添加数据库自定义的路由配置,path:{},serviceId:{}", zuulRoute.getPath(), zuulRoute.getServiceId());routes.put(zuulRoute.getPath(), zuulRoute);}return routes;}
}

网关日志处理

代码注释已经将逻辑写的很清楚了

@Slf4j
@Component
public class LogSendServiceImpl implements LogSendService {private static final String SERVICE_ID = "serviceId";@Autowiredprivate AmqpTemplate rabbitTemplate;/*** 1. 获取 requestContext 中的请求信息* 2. 如果返回状态不是OK,则获取返回信息中的错误信息* 3. 发送到MQ** @param requestContext 上下文对象*/@Overridepublic void send(RequestContext requestContext) {HttpServletRequest request = requestContext.getRequest();String requestUri = request.getRequestURI();String method = request.getMethod();SysLog sysLog = new SysLog();sysLog.setType(CommonConstant.STATUS_NORMAL);sysLog.setRemoteAddr(HttpUtil.getClientIP(request));sysLog.setRequestUri(URLUtil.getPath(requestUri));sysLog.setMethod(method);sysLog.setUserAgent(request.getHeader("user-agent"));sysLog.setParams(HttpUtil.toParams(request.getParameterMap()));Long startTime = (Long) requestContext.get("startTime");sysLog.setTime(System.currentTimeMillis() - startTime);if (requestContext.get(SERVICE_ID) != null) {sysLog.setServiceId(requestContext.get(SERVICE_ID).toString());}//正常发送服务异常解析if (requestContext.getResponseStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR&& requestContext.getResponseDataStream() != null) {InputStream inputStream = requestContext.getResponseDataStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();InputStream stream1 = null;InputStream stream2;byte[] buffer = IoUtil.readBytes(inputStream);try {baos.write(buffer);baos.flush();stream1 = new ByteArrayInputStream(baos.toByteArray());stream2 = new ByteArrayInputStream(baos.toByteArray());String resp = IoUtil.read(stream1, CommonConstant.UTF8);sysLog.setType(CommonConstant.STATUS_LOCK);sysLog.setException(resp);requestContext.setResponseDataStream(stream2);} catch (IOException e) {log.error("响应流解析异常:", e);throw new RuntimeException(e);} finally {IoUtil.close(stream1);IoUtil.close(baos);IoUtil.close(inputStream);}}//网关内部异常Throwable throwable = requestContext.getThrowable();if (throwable != null) {log.error("网关异常", throwable);sysLog.setException(throwable.getMessage());}//保存发往MQ(只保存授权)Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && StrUtil.isNotBlank(authentication.getName())) {LogVO logVo = new LogVO();sysLog.setCreateBy(authentication.getName());logVo.setSysLog(sysLog);logVo.setUsername(authentication.getName());rabbitTemplate.convertAndSend(MqQueueConstant.LOG_QUEUE, logVo);}}
}

多维度限流

限流降级处理器ZuulRateLimiterErrorHandler

重写zuul中默认的限流处理器DefaultRateLimiterErrorHandler,使之记录日志内容

@Beanpublic RateLimiterErrorHandler rateLimitErrorHandler() {return new DefaultRateLimiterErrorHandler() {@Overridepublic void handleSaveError(String key, Exception e) {log.error("保存key:[{}]异常", key, e);}@Overridepublic void handleFetchError(String key, Exception e) {log.error("路由失败:[{}]异常", key);}@Overridepublic void handleError(String msg, Exception e) {log.error("限流异常:[{}]", msg, e);}};}

与spring security oAuth方法整合单点登陆

授权拒绝处理器 PigAccessDeniedHandler

重写Srping security oAuth 提供单点登录验证拒绝OAuth2AccessDeniedHandler接口,使用R包装失败信息到PigDeniedException

@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {log.info("授权失败,禁止访问 {}", request.getRequestURI());response.setCharacterEncoding(CommonConstant.UTF8);response.setContentType(CommonConstant.CONTENT_TYPE);R<String> result = new R<>(new PigDeniedException("授权失败,禁止访问"));response.setStatus(HttpStatus.SC_FORBIDDEN);PrintWriter printWriter = response.getWriter();printWriter.append(objectMapper.writeValueAsString(result));}

菜单管理

MenuService

@FeignClient(name = "pig-upms-service", fallback = MenuServiceFallbackImpl.class)
public interface MenuService {/*** 通过角色名查询菜单** @param role 角色名称* @return 菜单列表*/@GetMapping(value = "/menu/findMenuByRole/{role}")Set<MenuVO> findMenuByRole(@PathVariable("role") String role);
}

使用feign连接pig系统的菜单微服务

菜单权限

@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {@Autowiredprivate MenuService menuService;private AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic boolean hasPermission(HttpServletRequest request, Authentication authentication) {//ele-admin options 跨域配置,现在处理是通过前端配置代理,不使用这种方式,存在风险
//        if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {//            return true;
//        }Object principal = authentication.getPrincipal();List<SimpleGrantedAuthority> authorityList = (List<SimpleGrantedAuthority>) authentication.getAuthorities();AtomicBoolean hasPermission = new AtomicBoolean(false);if (principal != null) {if (CollUtil.isEmpty(authorityList)) {log.warn("角色列表为空:{}", authentication.getPrincipal());return false;}Set<MenuVO> urls = new HashSet<>();authorityList.stream().filter(authority ->!StrUtil.equals(authority.getAuthority(), "ROLE_USER")).forEach(authority -> {Set<MenuVO> menuVOSet = menuService.findMenuByRole(authority.getAuthority());CollUtil.addAll(urls, menuVOSet);});urls.stream().filter(menu -> StrUtil.isNotEmpty(menu.getUrl())&& antPathMatcher.match(menu.getUrl(), request.getRequestURI())&& request.getMethod().equalsIgnoreCase(menu.getMethod())).findFirst().ifPresent(menuVO -> hasPermission.set(true));}return hasPermission.get();}
}

网关总结

pig这个系统是个很好的框架,本次体验的是pig的zuul网关模块,此模块与feign,ribbon,spring security,Eurasia进行整合,完成或部分完成了动态路由灰度发布,菜单权限管理服务限流网关日志处理,非常值得学习!

UPMs权限管理系统模块

百度了一下,UPMS是User Permissions Management System,通用用户权限管理系统

数据库设计

部门表

部门关系表

字典表

/*** 编号*/@TableId(value="id", type= IdType.AUTO)private Integer id;/*** 数据值*/private String value;/*** 标签名*/private String label;/*** 类型*/private String type;/*** 描述*/private String description;/*** 排序(升序)*/private BigDecimal sort;/*** 创建时间*/@TableField("create_time")private Date createTime;/*** 更新时间*/@TableField("update_time")private Date updateTime;/*** 备注信息*/private String remarks;/*** 删除标记*/@TableField("del_flag")private String delFlag;

日志表

@Data
public class SysLog implements Serializable {private static final long serialVersionUID = 1L;/*** 编号*/@TableId(type = IdType.ID_WORKER)@JsonSerialize(using = ToStringSerializer.class)private Long id;/*** 日志类型*/private String type;/*** 日志标题*/private String title;/*** 创建者*/private String createBy;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;/*** 操作IP地址*/private String remoteAddr;/*** 用户代理*/private String userAgent;/*** 请求URI*/private String requestUri;/*** 操作方式*/private String method;/*** 操作提交的数据*/private String params;/*** 执行时间*/private Long time;/*** 删除标记*/private String delFlag;/*** 异常信息*/private String exception;/*** 服务ID*/private String serviceId; }}

菜单权限表

角色表

角色与部门对应关系

角色与菜单权限对应关系

用户表

/*** 主键ID*/
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
/*** 用户名*/
private String username;private String password;
/*** 随机盐*/
@JsonIgnore
private String salt;
/*** 创建时间*/
@TableField("create_time")
private Date createTime;
/*** 修改时间*/
@TableField("update_time")
private Date updateTime;
/*** 0-正常,1-删除*/
@TableField("del_flag")
private String delFlag;/*** 简介*/
private String phone;
/*** 头像*/
private String avatar;/*** 部门ID*/
@TableField("dept_id")
private Integer deptId;

动态路由配置表

业务逻辑

全是基于mybatis plus的CRUD,有点多。大部分干这行的都懂,我就不详细展开了。

验证码

创建

ValidateCodeController可以找到创建验证码相关代码

/*** 创建验证码** @param request request* @throws Exception*/@GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/{randomStr}")public void createCode(@PathVariable String randomStr, HttpServletRequest request, HttpServletResponse response)throws Exception {Assert.isBlank(randomStr, "机器码不能为空");response.setHeader("Cache-Control", "no-store, no-cache");response.setContentType("image/jpeg");//生成文字验证码String text = producer.createText();//生成图片验证码BufferedImage image = producer.createImage(text);userService.saveImageCode(randomStr, text);ServletOutputStream out = response.getOutputStream();ImageIO.write(image, "JPEG", out);IOUtils.closeQuietly(out);}

其中的 producer是使用Kaptcha,下面是配置类

@Configuration
public class KaptchaConfig {private static final String KAPTCHA_BORDER = "kaptcha.border";private static final String KAPTCHA_TEXTPRODUCER_FONT_COLOR = "kaptcha.textproducer.font.color";private static final String KAPTCHA_TEXTPRODUCER_CHAR_SPACE = "kaptcha.textproducer.char.space";private static final String KAPTCHA_IMAGE_WIDTH = "kaptcha.image.width";private static final String KAPTCHA_IMAGE_HEIGHT = "kaptcha.image.height";private static final String KAPTCHA_TEXTPRODUCER_CHAR_LENGTH = "kaptcha.textproducer.char.length";private static final Object KAPTCHA_IMAGE_FONT_SIZE = "kaptcha.textproducer.font.size";@Beanpublic DefaultKaptcha producer() {Properties properties = new Properties();properties.put(KAPTCHA_BORDER, SecurityConstants.DEFAULT_IMAGE_BORDER);properties.put(KAPTCHA_TEXTPRODUCER_FONT_COLOR, SecurityConstants.DEFAULT_COLOR_FONT);properties.put(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, SecurityConstants.DEFAULT_CHAR_SPACE);properties.put(KAPTCHA_IMAGE_WIDTH, SecurityConstants.DEFAULT_IMAGE_WIDTH);properties.put(KAPTCHA_IMAGE_HEIGHT, SecurityConstants.DEFAULT_IMAGE_HEIGHT);properties.put(KAPTCHA_IMAGE_FONT_SIZE, SecurityConstants.DEFAULT_IMAGE_FONT_SIZE);properties.put(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, SecurityConstants.DEFAULT_IMAGE_LENGTH);Config config = new Config(properties);DefaultKaptcha defaultKaptcha = new DefaultKaptcha();defaultKaptcha.setConfig(config);return defaultKaptcha;}
}

发送手机验证码

大体逻辑为,先查询验证码redis缓存,没有缓存则说明验证码缓存没有失效,返回错误。

查到没有验证码,则根据手机号码从数据库获得用户信息,生成一个4位的验证码,使用rabbbitmq队列把短信验证码保存到队列,同时加上手机验证码的redis缓存

/*** 发送验证码* <p>* 1. 先去redis 查询是否 60S内已经发送* 2. 未发送: 判断手机号是否存 ? false :产生4位数字  手机号-验证码* 3. 发往消息中心-》发送信息* 4. 保存redis** @param mobile 手机号* @return true、false*/@Overridepublic R<Boolean> sendSmsCode(String mobile) {Object tempCode = redisTemplate.opsForValue().get(SecurityConstants.DEFAULT_CODE_KEY + mobile);if (tempCode != null) {log.error("用户:{}验证码未失效{}", mobile, tempCode);return new R<>(false, "验证码未失效,请失效后再次申请");}SysUser params = new SysUser();params.setPhone(mobile);List<SysUser> userList = this.selectList(new EntityWrapper<>(params));if (CollectionUtil.isEmpty(userList)) {log.error("根据用户手机号{}查询用户为空", mobile);return new R<>(false, "手机号不存在");}String code = RandomUtil.randomNumbers(4);JSONObject contextJson = new JSONObject();contextJson.put("code", code);contextJson.put("product", "Pig4Cloud");log.info("短信发送请求消息中心 -> 手机号:{} -> 验证码:{}", mobile, code);rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_CODE_QUEUE,new MobileMsgTemplate(mobile,contextJson.toJSONString(),CommonConstant.ALIYUN_SMS,EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getSignName(),EnumSmsChannelTemplate.LOGIN_NAME_LOGIN.getTemplate()));redisTemplate.opsForValue().set(SecurityConstants.DEFAULT_CODE_KEY + mobile, code, SecurityConstants.DEFAULT_IMAGE_EXPIRE, TimeUnit.SECONDS);return new R<>(true);}

树形节点工具栏

public class TreeUtil {/*** 两层循环实现建树** @param treeNodes 传入的树节点列表* @return*/public static <T extends TreeNode> List<T> bulid(List<T> treeNodes, Object root) {List<T> trees = new ArrayList<T>();for (T treeNode : treeNodes) {if (root.equals(treeNode.getParentId())) {trees.add(treeNode);}for (T it : treeNodes) {if (it.getParentId() == treeNode.getId()) {if (treeNode.getChildren() == null) {treeNode.setChildren(new ArrayList<TreeNode>());}treeNode.add(it);}}}return trees;}/*** 使用递归方法建树** @param treeNodes* @return*/public static <T extends TreeNode> List<T> buildByRecursive(List<T> treeNodes, Object root) {List<T> trees = new ArrayList<T>();for (T treeNode : treeNodes) {if (root.equals(treeNode.getParentId())) {trees.add(findChildren(treeNode, treeNodes));}}return trees;}/*** 递归查找子节点** @param treeNodes* @return*/public static <T extends TreeNode> T findChildren(T treeNode, List<T> treeNodes) {for (T it : treeNodes) {if (treeNode.getId() == it.getParentId()) {if (treeNode.getChildren() == null) {treeNode.setChildren(new ArrayList<TreeNode>());}treeNode.add(findChildren(it, treeNodes));}}return treeNode;}/*** 通过sysMenu创建树形节点** @param menus* @param root* @return*/public static List<MenuTree> bulidTree(List<SysMenu> menus, int root) {List<MenuTree> trees = new ArrayList<MenuTree>();MenuTree node;for (SysMenu menu : menus) {node = new MenuTree();node.setId(menu.getMenuId());node.setParentId(menu.getParentId());node.setName(menu.getName());node.setUrl(menu.getUrl());node.setPath(menu.getPath());node.setCode(menu.getPermission());node.setLabel(menu.getName());node.setComponent(menu.getComponent());node.setIcon(menu.getIcon());trees.add(node);}return TreeUtil.bulid(trees, root);}
}

生成avue模板类

public class PigResourcesGenerator {public static void main(String[] args) {String outputDir = "/Users/lengleng/work/temp";final String viewOutputDir = outputDir + "/view/";AutoGenerator mpg = new AutoGenerator();// 全局配置GlobalConfig gc = new GlobalConfig();gc.setOutputDir(outputDir);gc.setFileOverride(true);gc.setActiveRecord(true);// XML 二级缓存gc.setEnableCache(false);// XML ResultMapgc.setBaseResultMap(true);// XML columListgc.setBaseColumnList(true);gc.setAuthor("lengleng");mpg.setGlobalConfig(gc);// 数据源配置DataSourceConfig dsc = new DataSourceConfig();dsc.setDbType(DbType.MYSQL);dsc.setDriverName("com.mysql.jdbc.Driver");dsc.setUsername("root");dsc.setPassword("lengleng");dsc.setUrl("jdbc:mysql://139.224.200.249:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false");mpg.setDataSource(dsc);// 策略配置StrategyConfig strategy = new StrategyConfig();// strategy.setCapitalMode(true);// 全局大写命名 ORACLE 注意strategy.setSuperControllerClass("com.github.pig.common.web.BaseController");// 表名生成策略strategy.setNaming(NamingStrategy.underline_to_camel);mpg.setStrategy(strategy);// 包配置PackageConfig pc = new PackageConfig();pc.setParent("com.github.pig.admin");pc.setController("controller");mpg.setPackageInfo(pc);// 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值InjectionConfig cfg = new InjectionConfig() {@Overridepublic void initMap() {}};// 生成的模版路径,不存在时需要先新建File viewDir = new File(viewOutputDir);if (!viewDir.exists()) {viewDir.mkdirs();}List<FileOutConfig> focList = new ArrayList<FileOutConfig>();focList.add(new FileOutConfig("/templates/listvue.vue.vm") {@Overridepublic String outputFile(TableInfo tableInfo) {return getGeneratorViewPath(viewOutputDir, tableInfo, ".vue");}});cfg.setFileOutConfigList(focList);mpg.setCfg(cfg);//生成controller相关mpg.execute();}/*** 获取配置文件** @return 配置Props*/private static Properties getProperties() {// 读取配置文件Resource resource = new ClassPathResource("/config/application.properties");Properties props = new Properties();try {props = PropertiesLoaderUtils.loadProperties(resource);} catch (IOException e) {e.printStackTrace();}return props;}/*** 页面生成的文件名*/private static String getGeneratorViewPath(String viewOutputDir, TableInfo tableInfo, String suffixPath) {String name = StringUtils.firstToLowerCase(tableInfo.getEntityName());String path = viewOutputDir + "/" + name + "/index"  + suffixPath;File viewDir = new File(path).getParentFile();if (!viewDir.exists()) {viewDir.mkdirs();}return path;}
}

velocity模板

package $!{package.Controller};
import java.util.Map;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.github.pig.common.constant.CommonConstant;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.github.pig.common.util.Query;
import com.github.pig.common.util.R;
import $!{package.Entity}.$!{entity};
import $!{package.Service}.$!{entity}Service;
#if($!{superControllerClassPackage})
import $!{superControllerClassPackage};
#end/*** <p>* $!{table.comment} 前端控制器* </p>** @author $!{author}* @since $!{date}*/
@RestController
@RequestMapping("/$!{table.entityPath}")
public class $!{table.controllerName} extends $!{superControllerClass} {@Autowired private $!{entity}Service $!{table.entityPath}Service;/*** 通过ID查询** @param id ID* @return $!{entity}*/@GetMapping("/{id}")public R<$!{entity}> get(@PathVariable Integer id) {return new R<>($!{table.entityPath}Service.selectById(id));}/*** 分页查询信息** @param params 分页对象* @return 分页对象*/@RequestMapping("/page")public Page page(@RequestParam Map<String, Object> params) {params.put(CommonConstant.DEL_FLAG, CommonConstant.STATUS_NORMAL);return $!{table.entityPath}Service.selectPage(new Query<>(params), new EntityWrapper<>());}/*** 添加* @param  $!{table.entityPath}  实体* @return success/false*/@PostMappingpublic R<Boolean> add(@RequestBody $!{entity} $!{table.entityPath}) {return new R<>($!{table.entityPath}Service.insert($!{table.entityPath}));}/*** 删除* @param id ID* @return success/false*/@DeleteMapping("/{id}")public R<Boolean> delete(@PathVariable Integer id) {$!{entity} $!{table.entityPath} = new $!{entity}();$!{table.entityPath}.setId(id);$!{table.entityPath}.setUpdateTime(new Date());$!{table.entityPath}.setDelFlag(CommonConstant.STATUS_DEL);return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath}));}/*** 编辑* @param  $!{table.entityPath}  实体* @return success/false*/@PutMappingpublic R<Boolean> edit(@RequestBody $!{entity} $!{table.entityPath}) {$!{table.entityPath}.setUpdateTime(new Date());return new R<>($!{table.entityPath}Service.updateById($!{table.entityPath}));}
}

缓存

在部分实现类中,我们看到了作者使用了spring cache相关的注解。现在我们回忆一下相关缓存注解的含义:

@Cacheable:用来定义缓存的。常用到是value,key;分别用来指明缓存的名称和方法中参数,对于value你也可以使用cacheName,在查看源代码是我们可以看到:两者是指的同一个东西。

@CacheEvict:用来清理缓存。常用有cacheNames,allEntries(默认值false);分别代表了要清除的缓存名称和是否全部清除(true代表全部清除)。

@CachePut:用来更新缓存,用它来注解的方法都会被执行,执行完后结果被添加到缓存中。该方法不能和@Cacheable同时在同一个方法上使用。

后台跑批定时任务模块

Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架。去掉了和dd-job中的监控和ddframe接入规范部分。该项目基于成熟的开源产品Quartz和Zookeeper及其客户端Curator进行二次开发。主要功能如下:

  • 定时任务: 基于成熟的定时任务作业框架Quartz cron表达式执行定时任务。
  • 作业注册中心: 基于Zookeeper和其客户端Curator实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。
  • 作业分片: 将一个任务分片成为多个小任务项在多服务器上同时执行。
  • 弹性扩容缩容: 运行中的作业服务器崩溃,或新增加n台作业服务器,作业框架将在下次作业执行前重新分片,不影响当前作业执行。
  • 支持多种作业执行模式: 支持OneOff,Perpetual和SequencePerpetual三种作业模式。
  • 失效转移: 运行中的作业服务器崩溃不会导致重新分片,只会在下次作业启动时分片。启用失效转移功能可以在本次作业执行过程中,监测其他作业服务器空闲,抓取未完成的孤儿分片项执行。
  • 运行时状态收集: 监控作业运行时状态,统计最近一段时间处理的数据成功和失败数量,记录作业上次运行开始时间,结束时间和下次运行时间。
  • **作业停止,恢复和禁用:**用于操作作业启停,并可以禁止某作业运行(上线时常用)。
  • **被错过执行的作业重触发:**自动记录错过执行的作业,并在上次作业完成后自动触发。可参考Quartz的misfire。
  • **多线程快速处理数据:**使用多线程处理抓取到的数据,提升吞吐量。
  • **幂等性:**重复作业任务项判定,不重复执行已运行的作业任务项。由于开启幂等性需要监听作业运行状态,对瞬时反复运行的作业对性能有较大影响。
  • **容错处理:**作业服务器与Zookeeper服务器通信失败则立即停止作业运行,防止作业注册中心将失效的分片分项配给其他作业服务器,而当前作业服务器仍在执行任务,导致重复执行。
  • **Spring支持:**支持spring容器,自定义命名空间,支持占位符。
  • **运维平台:**提供运维界面,可以管理作业和注册中心。

配置

作者直接使用了开源项目的配置,我顺着他的pom文件找到了这家的github,地址如下

https://github.com/xjzrc/elastic-job-lite-spring-boot-starter

工作流作业配置

@ElasticJobConfig(cron = "0 0 0/1 * * ? ", shardingTotalCount = 3, shardingItemParameters = "0=Beijing,1=Shanghai,2=Guangzhou")
public class PigDataflowJob implements DataflowJob<Integer> {@Overridepublic List<Integer> fetchData(ShardingContext shardingContext) {return null;}@Overridepublic void processData(ShardingContext shardingContext, List<Integer> list) {}
}

测试代码

@Slf4j
@ElasticJobConfig(cron = "0 0 0/1 * * ?", shardingTotalCount = 3,shardingItemParameters = "0=pig1,1=pig2,2=pig3",startedTimeoutMilliseconds = 5000L,completedTimeoutMilliseconds = 10000L,eventTraceRdbDataSource = "dataSource")
public class PigSimpleJob implements SimpleJob {/*** 业务执行逻辑** @param shardingContext 分片信息*/@Overridepublic void execute(ShardingContext shardingContext) {log.info("shardingContext:{}", shardingContext);}
}

开源版对这个支持有限,等到拿到收费版我在做分析。

消息中心

这里的消息中心主要是集成了钉钉服务和阿里大鱼短息服务

钉钉

配置

钉钉是相当简单了,只需要一个webhook信息就够了。

webhook是一种web回调或者http的push API,是向APP或者其他应用提供实时信息的一种方式。Webhook在数据产生时立即发送数据,也就是你能实时收到数据。这一种不同于典型的API,需要用了实时性需要足够快的轮询。这无论是对生产还是对消费者都是高效的,唯一的缺点是初始建立困难。Webhook有时也被称为反向API,因为他提供了API规则,你需要设计要使用的API。Webhook将向你的应用发起http请求,典型的是post请求,应用程序由请求驱动。

@Data
@Configuration
@ConfigurationProperties(prefix = "sms.dingtalk")
public class DingTalkPropertiesConfig {/*** webhook*/private String webhook;
}

消息模板

/*** @author lengleng* @date 2018/1/15* 钉钉消息模板* msgtype : text* text : {"content":"服务: pig-upms-service 状态:UP"}*/
@Data
@ToString
public class DingTalkMsgTemplate implements Serializable {private String msgtype;private TextBean text;public String getMsgtype() {return msgtype;}public void setMsgtype(String msgtype) {this.msgtype = msgtype;}public TextBean getText() {return text;}public void setText(TextBean text) {this.text = text;}public static class TextBean {/*** content : 服务: pig-upms-service 状态:UP*/private String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}}
}

监听

使用队列时时监听

@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE)
public class DingTalkServiceChangeReceiveListener {@Autowiredprivate DingTalkMessageHandler dingTalkMessageHandler;@RabbitHandlerpublic void receive(String text) {long startTime = System.currentTimeMillis();log.info("消息中心接收到钉钉发送请求-> 内容:{} ", text);dingTalkMessageHandler.process(text);long useTime = System.currentTimeMillis() - startTime;log.info("调用 钉钉网关处理完毕,耗时 {}毫秒", useTime);}
}

发送

使用队列发送

@Slf4j
@Component
public class DingTalkMessageHandler {@Autowiredprivate DingTalkPropertiesConfig dingTalkPropertiesConfig;/*** 业务处理** @param text 消息*/public boolean process(String text) {String webhook = dingTalkPropertiesConfig.getWebhook();if (StrUtil.isBlank(webhook)) {log.error("钉钉配置错误,webhook为空");return false;}DingTalkMsgTemplate dingTalkMsgTemplate = new DingTalkMsgTemplate();dingTalkMsgTemplate.setMsgtype("text");DingTalkMsgTemplate.TextBean textBean = new DingTalkMsgTemplate.TextBean();textBean.setContent(text);dingTalkMsgTemplate.setText(textBean);String result = HttpUtil.post(webhook, JSONObject.toJSONString(dingTalkMsgTemplate));log.info("钉钉提醒成功,报文响应:{}", result);return true;}
}

阿里大鱼短息服务

配置

@Data
@Configuration
@ConditionalOnExpression("!'${sms.aliyun}'.isEmpty()")
@ConfigurationProperties(prefix = "sms.aliyun")
public class SmsAliyunPropertiesConfig {/*** 应用ID*/private String accessKey;/*** 应用秘钥*/private String secretKey;/*** 短信模板配置*/private Map<String, String> channels;
}

监听

@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE)
public class MobileServiceChangeReceiveListener {@Autowiredprivate Map<String, SmsMessageHandler> messageHandlerMap;@RabbitHandlerpublic void receive(MobileMsgTemplate mobileMsgTemplate) {long startTime = System.currentTimeMillis();log.info("消息中心接收到短信发送请求-> 手机号:{} -> 信息体:{} ", mobileMsgTemplate.getMobile(), mobileMsgTemplate.getContext());String channel = mobileMsgTemplate.getChannel();SmsMessageHandler messageHandler = messageHandlerMap.get(channel);if (messageHandler == null) {log.error("没有找到指定的路由通道,不进行发送处理完毕!");return;}messageHandler.execute(mobileMsgTemplate);long useTime = System.currentTimeMillis() - startTime;log.info("调用 {} 短信网关处理完毕,耗时 {}毫秒", mobileMsgTemplate.getType(), useTime);}
}

发送

不错的模板

@Slf4j
@Component(CommonConstant.ALIYUN_SMS)
public class SmsAliyunMessageHandler extends AbstractMessageHandler {@Autowiredprivate SmsAliyunPropertiesConfig smsAliyunPropertiesConfig;private static final String PRODUCT = "Dysmsapi";private static final String DOMAIN = "dysmsapi.aliyuncs.com";/*** 数据校验** @param mobileMsgTemplate 消息*/@Overridepublic void check(MobileMsgTemplate mobileMsgTemplate) {Assert.isBlank(mobileMsgTemplate.getMobile(), "手机号不能为空");Assert.isBlank(mobileMsgTemplate.getContext(), "短信内容不能为空");}/*** 业务处理** @param mobileMsgTemplate 消息*/@Overridepublic boolean process(MobileMsgTemplate mobileMsgTemplate) {//可自助调整超时时间System.setProperty("sun.net.client.defaultConnectTimeout", "10000");System.setProperty("sun.net.client.defaultReadTimeout", "10000");//初始化acsClient,暂不支持region化IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsAliyunPropertiesConfig.getAccessKey(), smsAliyunPropertiesConfig.getSecretKey());try {DefaultProfile.addEndpoint("cn-hou", "cn-hangzhou", PRODUCT, DOMAIN);} catch (ClientException e) {log.error("初始化SDK 异常", e);e.printStackTrace();}IAcsClient acsClient = new DefaultAcsClient(profile);//组装请求对象-具体描述见控制台-文档部分内容SendSmsRequest request = new SendSmsRequest();//必填:待发送手机号request.setPhoneNumbers(mobileMsgTemplate.getMobile());//必填:短信签名-可在短信控制台中找到request.setSignName(mobileMsgTemplate.getSignName());//必填:短信模板-可在短信控制台中找到request.setTemplateCode(smsAliyunPropertiesConfig.getChannels().get(mobileMsgTemplate.getTemplate()));//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"request.setTemplateParam(mobileMsgTemplate.getContext());request.setOutId(mobileMsgTemplate.getMobile());//hint 此处可能会抛出异常,注意catchtry {SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);log.info("短信发送完毕,手机号:{},返回状态:{}", mobileMsgTemplate.getMobile(), sendSmsResponse.getCode());} catch (ClientException e) {log.error("发送异常");e.printStackTrace();}return true;}/*** 失败处理** @param mobileMsgTemplate 消息*/@Overridepublic void fail(MobileMsgTemplate mobileMsgTemplate) {log.error("短信发送失败 -> 网关:{} -> 手机号:{}", mobileMsgTemplate.getType(), mobileMsgTemplate.getMobile());}
}

资源认证服务器 (单点登陆功能)

由于作者在认证中心使用了spring security oauth框架,所以需要在微服务的客户端实现一个资源认证服务器,来完成SSO需求。

配置

暴露监控信息

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();}}

接口

@EnableOAuth2Sso
@SpringBootApplication
public class PigSsoClientDemoApplication {public static void main(String[] args) {SpringApplication.run(PigSsoClientDemoApplication.class, args);}}

监控模块

springboot admin配置

RemindingNotifier会在应用上线或宕掉的时候发送提醒,也就是把notifications发送给其他的notifier,notifier的实现很有意思,不深究了,从类关系可以知道,我们可以以这么几种方式发送notifications:Pagerduty、Hipchat 、Slack 、Mail、 Reminder

@Configurationpublic static class NotifierConfig {@Bean@Primarypublic RemindingNotifier remindingNotifier() {RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(loggerNotifier()));notifier.setReminderPeriod(TimeUnit.SECONDS.toMillis(10));return notifier;}@Scheduled(fixedRate = 1_000L)public void remind() {remindingNotifier().sendReminders();}@Beanpublic FilteringNotifier filteringNotifier(Notifier delegate) {return new FilteringNotifier(delegate);}@Beanpublic LoggingNotifier loggerNotifier() {return new LoggingNotifier();}}

短信服务下线通知

继承AbstractStatusChangeNotifier,将短信服务注册到spring boot admin中。

@Slf4j
public class StatusChangeNotifier extends AbstractStatusChangeNotifier {private RabbitTemplate rabbitTemplate;private MonitorPropertiesConfig monitorMobilePropertiesConfig;public StatusChangeNotifier(MonitorPropertiesConfig monitorMobilePropertiesConfig, RabbitTemplate rabbitTemplate) {this.rabbitTemplate = rabbitTemplate;this.monitorMobilePropertiesConfig = monitorMobilePropertiesConfig;}/*** 通知逻辑** @param event 事件* @throws Exception 异常*/@Overrideprotected void doNotify(ClientApplicationEvent event) {if (event instanceof ClientApplicationStatusChangedEvent) {log.info("Application {} ({}) is {}", event.getApplication().getName(),event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getTo().getStatus());String text = String.format("应用:%s 服务ID:%s 状态改变为:%s,时间:%s", event.getApplication().getName(), event.getApplication().getId(), ((ClientApplicationStatusChangedEvent) event).getTo().getStatus(), DateUtil.date(event.getTimestamp()).toString());JSONObject contextJson = new JSONObject();contextJson.put("name", event.getApplication().getName());contextJson.put("seid", event.getApplication().getId());contextJson.put("time", DateUtil.date(event.getTimestamp()).toString());//开启短信通知if (monitorMobilePropertiesConfig.getMobile().getEnabled()) {log.info("开始短信通知,内容:{}", text);rabbitTemplate.convertAndSend(MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE,new MobileMsgTemplate(CollUtil.join(monitorMobilePropertiesConfig.getMobile().getMobiles(), ","),contextJson.toJSONString(),CommonConstant.ALIYUN_SMS,EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getSignName(),EnumSmsChannelTemplate.SERVICE_STATUS_CHANGE.getTemplate()));}if (monitorMobilePropertiesConfig.getDingTalk().getEnabled()) {log.info("开始钉钉通知,内容:{}", text);rabbitTemplate.convertAndSend(MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE, text);}} else {log.info("Application {} ({}) {}", event.getApplication().getName(),event.getApplication().getId(), event.getType());}}}

zipkin 链路追踪

由于zipkin是侵入式,因此这部分组件没有代码,只有相关依赖。下面分享一下作者的yaml

DB

server:port: 5003# datasoure默认使用JDBC
spring:datasource:driver-class-name: com.mysql.jdbc.Driverusername: rootpassword: ENC(gc16brBHPNq27HsjaULgKGq00Rz6ZUji)url: jdbc:mysql://127.0.0.1:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=falsezipkin:collector:rabbitmq:addresses: 127.0.0.1:5682password: lenglengusername: pigqueue: zipkinstorage:type: mysql

ELK

server:port: 5002zipkin:collector:rabbitmq:addresses: 127.0.0.1:5682password: lenglengusername: pigqueue: zipkinstorage:type: elasticsearchelasticsearch:hosts: 127.0.0.1:9200cluster: elasticsearchindex: zipkinmax-requests: 64index-shards: 5index-replicas: 1

续1s时间

全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的?个人博客 ?吧!

谢谢你那么可爱,还一直关注着我~❤?

看spring cloud开源项目Pig的云踩坑记相关推荐

  1. GitHub上最火的7个spring cloud开源项目,对新手太友好了

    微服务是什么? 微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦.你可以将其看作是在架构层次而非获取服务的类上 ...

  2. idea 包拆分_idea springboot项目拆分多模块踩坑记(1)

    先说一下项目背景吧,我这个项目主要分为后台管理和前台应用两部分,后台管理用的是ruoyi,基于springboot的,前台是自己搭的一套springcloud,连接的数据库是同一个,所以就出现了前后端 ...

  3. 一大波震撼的资源向你袭来,优秀的Spring Boot开源项目,你值得拥有!

    史上最全的spring cloud 开源项目 1.mall-SpringBoot+MyBatis 电商系统 mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyB ...

  4. spring cloud微服务分布式云架构 - Spring Cloud集成项目简介(三)

    点击上面 免费订阅本账号! 本文作者:it菲菲 原文:https://yq.aliyun.com/articles/672242 点击阅读全文前往 Spring Cloud集成项目有很多,下面我们列举 ...

  5. 从天气项目看Spring Cloud微服务治理

    网上搜集的资源,个人感觉还行,分享了 从天气项目看Spring Cloud微服务治理 网盘地址:https://pan.baidu.com/s/1ggn5uld 密码: n6bn 备用地址(腾讯微云) ...

  6. spring cloud微服务分布式云架构 - Spring Cloud集成项目简介

    Spring Cloud集成项目有很多,下面我们列举一下和Spring Cloud相关的优秀项目,我们的企业架构中用到了很多的优秀项目,说白了,也是站在巨人的肩膀上去整合的.在学习Spring Clo ...

  7. 关于SpringCloud微服务云架构构建B2B2C电子商务平台之- Spring Cloud集成项目简介(三)...

    2019独角兽企业重金招聘Python工程师标准>>> Spring Cloud集成项目有很多,下面我们列举一下和Spring Cloud相关的优秀项目,我们的企业架构中用到了很多的 ...

  8. spring cloud 实战项目搭建

    spring cloud 实战项目搭建 Spring Cloud简介 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现.断 ...

  9. (三)java版spring cloud+spring boot 社交电子商务平台 - Spring Cloud集成项目简介

    2019独角兽企业重金招聘Python工程师标准>>> 电子商务平台源码请加企鹅求求:一零三八七七四六二六.Spring Cloud集成项目有很多,下面我们列举一下和Spring C ...

最新文章

  1. 一个把ListString转化为以,隔开的字符串的方法
  2. 信号与系统 第一章 郭宝龙
  3. 前端学习(892):bom概述
  4. python测试代码与模块_测量Python代码运行时间
  5. Codeforces 948D Perfect Security
  6. mips的旁路_低功耗设计二之Bypassing(旁路)
  7. android 广告字幕,Android编程实现类似天气预报图文字幕垂直滚动效果的方法
  8. Prescan(七):prescan中air传感器的配置
  9. Qt+opencv二值化
  10. java springboot实现pdf在线盖章,签字的功能
  11. oracle大写数字转小写,求助oracle小写金额转换大写金额的函数
  12. python计算log函数
  13. 简单的sql注入之2WP
  14. SAP跨公司销售经典场景
  15. Beer Bill(签到题)
  16. 基于python的国内外研究现状怎么写_毕业论文指之国内外研究现状的写法与范文...
  17. 2022年中国前10大互联网公司广告营收榜
  18. 大学行政职务、各类学生以及教师职称的英文翻译
  19. 小白如何学习操作系统?
  20. 微软升至live.com后hotmail邮箱不能登录的应急方法。

热门文章

  1. 海量数据搜索---搜索引擎 1
  2. 【Pytorch】RuntimeError: one of the variables needed for gradient computation has been modified by
  3. 如何理解卡耐基的人际关系-----如何看清自身能力的积累?
  4. 微软 Win快捷键大全
  5. 必须掌握sprintf函数的用法
  6. 9s oppo r 怎么root_OPPO R811 ROOT过程总结
  7. 计及光伏波动性的主动配电网有功无功协调优化(Matlab代码实现)
  8. python自动交易脚本_python交易库 Python自动化交易 - 网银支付 - 服务器之家
  9. python库排行榜_使用python landport库快速实现排行榜
  10. UserCF与ItemCF杂想之余弦相似度