最近看到一个有趣的开源项目pig,主要的技术点在认证授权中心,spring security oauth,zuul网关实现,Elastic-Job定时任务,趁着刚刚入门微服务,赶快写个博客分析一下。此篇文章主要用于个人备忘。如果有不对,请批评。?
由于每个模块篇幅较长,且部分内容和前文有重叠,干货和图片较少,阅读时使用旁边的导航功能体验较佳。?
想要解锁更多新姿势?请访问https://blog.tengshe789.tech/
说明
本篇文章是对基于spring boot
1.5的pig 1
版本做的分析,不是收费的pigx 2
版本。
开源项目地址
gitee.com/log4j/pig
配置中心:gitee.com/cqzqxq_lxh/…
冷冷官方地址
pig4cloud.com/zh-cn/index…
体验地址
pigx.pig4cloud.com/#/wel/index
项目启动顺序
请确保启动顺序(要先启动认证中心,再启动网关)
- eureka
- config
- auth
- gateway
- 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 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 {
@Autowired
private FilterIgnorePropertiesConfig filterIgnorePropertiesConfig;
@Autowired
private MobileSecurityConfigurer mobileSecurityConfigurer;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer.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基础,blog.tengshe789.tech/2018/12/02/…
这里简单提一下,spring security oauth
里有两个概念,授权服务器和资源服务器。
授权服务器是根据授权许可给访问的客户端发放access token
令牌的,提供认证、授权服务;
资源服务器需要验证这个access token
,客户端才能访问对应服务。
客户详细信息服务配置
ClientDetailsServiceConfigurer
(AuthorizationServerConfigurer
的一个回调配置项) 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService),Spring Security OAuth2
的配置方法是编写@Configuration
类继承AuthorizationServerConfigurerAdapter
,然后重写void configure(ClientDetailsServiceConfigurer clients)
方法
下面代码主要逻辑是,使用spring security
框架封装的简单sql连接器,查询客户端的详细信息?
@Override
public 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
中进行处理
@Override
public 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
@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
final Map 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 {
@Override
public Map convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
Map representation = (Map) super.convertAccessToken(token, authentication);
representation.put("license", SecurityConstants.PIG_LICENSE);
return representation;
}
@Override
public OAuth2AccessToken extractAccessToken(String value, Map map) {
return super.extractAccessToken(value, map);
}
@Override
public OAuth2Authentication extractAuthentication(Map 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);
*/
@Bean
public TokenStore redisTokenStore() {
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(SecurityConstants.PIG_PREFIX);
return tokenStore;
}
复制代码
授权服务器安全配置器
@Override
public 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> {
@Autowired
private AuthenticationSuccessHandler mobileLoginSuccessHandler;
@Autowired
private UserService userService;
@Override
public 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
都会通UserDetailsService
和UserDetail
来返回一个 以MobileAuthenticationToken
实现的带用户以及权限的Authentication
此处逻辑是,通过UserService
查找已有用户的手机号码,生成对应的UserDetails
,使用UserDetails生成手机验证Authentication
@Override
public 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);
}
@Override
public 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);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public 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);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
复制代码
手机号登录验证filter
判断http请求是否是post,不是则返回错误。
根据request请求拿到moblie信息,使用moblie信息返回手机号码登陆成功的oauth token。
@Override
public 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
逻辑都在注释中
@Override
public 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);
//校验secret
if (!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");
//校验scope
new 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 redisTemplate;
public RedisTemplate getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
@Override
public 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;
}
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}
@Override
public OAuth2Authentication readAuthentication(String token) {
return (OAuth2Authentication) this.redisTemplate.opsForValue().get(AUTH + token);
}
@Override
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return readAuthenticationForRefreshToken(token.getValue());
}
public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
return (OAuth2Authentication) this.redisTemplate.opsForValue().get(REFRESH_AUTH + token);
}
@Override
public 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);
}
@Override
public void removeAccessToken(OAuth2AccessToken accessToken) {
removeAccessToken(accessToken.getValue());
}
@Override
public 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 that
OAuth2Authentication 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));
}
}
@Override
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
this.redisTemplate.opsForValue().set(REFRESH + refreshToken.getValue(), refreshToken);
this.redisTemplate.opsForValue().set(REFRESH_AUTH + refreshToken.getValue(), authentication);
}
@Override
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
return (OAuth2RefreshToken) this.redisTemplate.opsForValue().get(REFRESH + tokenValue);
}
@Override
public 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);
}
@Override
public 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);
}
}
@Override
public Collection findTokensByClientIdAndUserName(String clientId, String userName) {
List
服务网关模块
网关主体在包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 {
@Autowired
private LogSendService logSendService;
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER + 1;
}
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
return requestContext.getThrowable() != null;
}
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
logSendService.send(requestContext);
return null;
}
}
复制代码
作者以原生zuul过滤器为基础加了日志配置,优先级为+1,数字越大优先级越低。
XSS过滤器
public class XssSecurityFilter extends OncePerRequestFilter {
@Override
protected 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
为过滤链条。
@Override
public 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
。
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Map> params = ctx.getRequestQueryParams();
if (params == null) {
return null;
}
List 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
*/
@Override
public 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;
}
@Override
public Object run() {
try {
checkCode(RequestContext.getCurrentContext().getRequest());
} catch (ValidateCodeException e) {
RequestContext ctx = RequestContext.getCurrentContext();
R 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;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FORM_BODY_WRAPPER_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public 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。
此处逻辑是
- eureka metadata (主机名,IP地址,端口号,状态页健康检查等信息,或者通过配置文件自定义元数据)存在版本定义时候进行判断
- 不存在 metadata 直接返回true
@Override
public AbstractServerPredicate getPredicate() {
return new AbstractServerPredicate() {
@Override
public 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 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;
}
/**
* 重写路由配置
*
* 1. properties 配置。
* 2. eureka 默认配置。
* 3. DB数据库配置。
*
* @return 路由表
*/
@Override
protected LinkedHashMap locateRoutes() {
LinkedHashMap routesMap = new LinkedHashMap<>();
//读取properties配置、eureka默认配置
routesMap.putAll(super.locateRoutes());
log.debug("初始默认的路由配置完成");
routesMap.putAll(locateRoutesFromDb());
LinkedHashMap values = new LinkedHashMap<>();
for (Map.Entry 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 locateRoutesFromDb() {
Map routes = new LinkedHashMap<>();
Object obj = redisTemplate.opsForValue().get(CommonConstant.ROUTE_KEY);
if (obj == null) {
return routes;
}
List results = (List) 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 sensitiveHeadersList = StrUtil.splitTrim(result.getSensitiveheadersList(), ",");
if (sensitiveHeadersList != null) {
Set 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";
@Autowired
private AmqpTemplate rabbitTemplate;
/**
* 1. 获取 requestContext 中的请求信息
* 2. 如果返回状态不是OK,则获取返回信息中的错误信息
* 3. 发送到MQ
*
* @param requestContext 上下文对象
*/
@Override
public 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
,使之记录日志内容
@Bean
public RateLimiterErrorHandler rateLimitErrorHandler() {
return new DefaultRateLimiterErrorHandler() {
@Override
public void handleSaveError(String key, Exception e) {
log.error("保存key:[{}]异常", key, e);
}
@Override
public void handleFetchError(String key, Exception e) {
log.error("路由失败:[{}]异常", key);
}
@Override
public void handleError(String msg, Exception e) {
log.error("限流异常:[{}]", msg, e);
}
};
}
复制代码
与spring security oAuth方法整合单点登陆
授权拒绝处理器 PigAccessDeniedHandler
重写Srping security oAuth
提供单点登录验证拒绝OAuth2AccessDeniedHandler
接口,使用R包装失败信息到PigDeniedException
@Override
public 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 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 findMenuByRole(@PathVariable("role") String role) ;
}
复制代码
使用feign连接pig系统的菜单微服务
菜单权限
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {
@Autowired
private MenuService menuService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//ele-admin options 跨域配置,现在处理是通过前端配置代理,不使用这种方式,存在风险
// if (HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
// return true;
// }
Object principal = authentication.getPrincipal();
List authorityList = (List) authentication.getAuthorities();
AtomicBoolean hasPermission = new AtomicBoolean(false);
if (principal != null) {
if (CollUtil.isEmpty(authorityList)) {
log.warn("角色列表为空:{}", authentication.getPrincipal());
return false;
}
Set urls = new HashSet<>();
authorityList.stream().filter(authority ->
!StrUtil.equals(authority.getAuthority(), "ROLE_USER"))
.forEach(authority -> {
Set 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";
@Bean
public 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缓存
/**
* 发送验证码
*
* 1. 先去redis 查询是否 60S内已经发送
* 2. 未发送: 判断手机号是否存 ? false :产生4位数字 手机号-验证码
* 3. 发往消息中心-》发送信息
* 4. 保存redis
*
* @param mobile 手机号
* @return true、false
*/
@Override
public R 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 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 List bulid(List treeNodes, Object root) {
List trees = new ArrayList();
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.add(it);
}
}
}
return trees;
}
/**
* 使用递归方法建树
*
* @param treeNodes
* @return
*/
public static List buildByRecursive(List treeNodes, Object root) {
List trees = new ArrayList();
for (T treeNode : treeNodes) {
if (root.equals(treeNode.getParentId())) {
trees.add(findChildren(treeNode, treeNodes));
}
}
return trees;
}
/**
* 递归查找子节点
*
* @param treeNodes
* @return
*/
public static T findChildren(T treeNode, List treeNodes) {
for (T it : treeNodes) {
if (treeNode.getId() == it.getParentId()) {
if (treeNode.getChildren() == null) {
treeNode.setChildren(new ArrayList());
}
treeNode.add(findChildren(it, treeNodes));
}
}
return treeNode;
}
/**
* 通过sysMenu创建树形节点
*
* @param menus
* @param root
* @return
*/
public static List bulidTree(List menus, int root) {
List trees = new ArrayList();
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 ResultMap
gc.setBaseResultMap(true);
// XML columList
gc.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() {
@Override
public void initMap() {
}
};
// 生成的模版路径,不存在时需要先新建
File viewDir = new File(viewOutputDir);
if (!viewDir.exists()) {
viewDir.mkdirs();
}
List focList = new ArrayList();
focList.add(new FileOutConfig("/templates/listvue.vue.vm") {
@Override
public 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
/**
*
* $!{table.comment} 前端控制器
*
*
* @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 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
*/
@PostMapping
public R add(@RequestBody $!{entity} $!{table.entityPath}) {
return new R<>($!{table.entityPath}Service.insert($!{table.entityPath}));
}
/**
* 删除
* @param id ID
* @return success/false
*/
@DeleteMapping("/{id}")
public R 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
*/
@PutMapping
public R 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,地址如下
github.com/xjzrc/elast…
工作流作业配置
@ElasticJobConfig(cron = "0 0 0/1 * * ? ", shardingTotalCount = 3, shardingItemParameters = "0=Beijing,1=Shanghai,2=Guangzhou")
public class PigDataflowJob implements DataflowJob<Integer> {
@Override
public List fetchData(ShardingContext shardingContext) {
return null;
}
@Override
public void processData(ShardingContext shardingContext, List 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 分片信息
*/
@Override
public 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 {
@Autowired
private DingTalkMessageHandler dingTalkMessageHandler;
@RabbitHandler
public 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 {
@Autowired
private 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 channels;
}
复制代码
监听
@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE)
public class MobileServiceChangeReceiveListener {
@Autowired
private Map messageHandlerMap;
@RabbitHandler
public 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 {
@Autowired
private SmsAliyunPropertiesConfig smsAliyunPropertiesConfig;
private static final String PRODUCT = "Dysmsapi";
private static final String DOMAIN = "dysmsapi.aliyuncs.com";
/**
* 数据校验
*
* @param mobileMsgTemplate 消息
*/
@Override
public void check(MobileMsgTemplate mobileMsgTemplate) {
Assert.isBlank(mobileMsgTemplate.getMobile(), "手机号不能为空");
Assert.isBlank(mobileMsgTemplate.getContext(), "短信内容不能为空");
}
/**
* 业务处理
*
* @param mobileMsgTemplate 消息
*/
@Override
public 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 此处可能会抛出异常,注意catch
try {
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
log.info("短信发送完毕,手机号:{},返回状态:{}", mobileMsgTemplate.getMobile(), sendSmsResponse.getCode());
} catch (ClientException e) {
log.error("发送异常");
e.printStackTrace();
}
return true;
}
/**
* 失败处理
*
* @param mobileMsgTemplate 消息
*/
@Override
public void fail(MobileMsgTemplate mobileMsgTemplate) {
log.error("短信发送失败 -> 网关:{} -> 手机号:{}", mobileMsgTemplate.getType(), mobileMsgTemplate.getMobile());
}
}
复制代码
资源认证服务器 (单点登陆功能)
由于作者在认证中心使用了spring security oauth框架,所以需要在微服务的客户端实现一个资源认证服务器,来完成SSO需求。
配置
暴露监控信息
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public 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
@Configuration
public static class NotifierConfig {
@Bean
@Primary
public 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();
}
@Bean
public FilteringNotifier filteringNotifier(Notifier delegate) {
return new FilteringNotifier(delegate);
}
@Bean
public 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 异常
*/
@Override
protected 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.Driver
username: root
password: ENC(gc16brBHPNq27HsjaULgKGq00Rz6ZUji)
url: jdbc:mysql://127.0.0.1:3309/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
zipkin:
collector:
rabbitmq:
addresses: 127.0.0.1:5682
password: lengleng
username: pig
queue: zipkin
storage:
type: mysql
复制代码
ELK
server:
port: 5002
zipkin:
collector:
rabbitmq:
addresses: 127.0.0.1:5682
password: lengleng
username: pig
queue: zipkin
storage:
type: elasticsearch
elasticsearch:
hosts: 127.0.0.1:9200
cluster: elasticsearch
index: zipkin
max-requests: 64
index-shards: 5
index-replicas: 1
复制代码
续1s时间
全片结束,觉得我写的不错?想要了解更多精彩新姿势?赶快打开我的?个人博客 ?吧!
谢谢你那么可爱,还一直关注着我~❤?