Pig, spring Security Oauth, Zuul Gateway implementation, And Elastice-Job timing tasks are some of the most interesting open source projects I’ve seen recently. This article is intended as a personal memo. If something is wrong, please criticize. 😭

Due to the length of each module, and part of the content overlaps with the previous text, there are few dry goods and pictures, so it is better to use the navigation function beside the reading. πŸ˜‰

Want to unlock more new poses? Please visit the blog. Tengshe789. Tech /

instructions

This article is an analysis of pig1 based on Spring Boot1.5, not pigx 2.

Open Source project address

gitee.com/log4j/pig

Configuration center: gitee.com/cqzqxq_lxh/…

Cold official address

Pig4cloud.com/zh-cn/index…

Experience the address

pigx.pig4cloud.com/#/wel/index

Project startup sequence

Ensure the startup sequence (start the authentication center first, then start the gateway)

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

Certification center

As always, look at the code from top to bottom, starting at the interface layer

Requesting rest Interfaces

@RestController
@RequestMapping("/authentication")
public class AuthenticationController {
    @Autowired
    @Qualifier("consumerTokenServices")
    private ConsumerTokenServices consumerTokenServices;

    /** * Authentication page *@return ModelAndView
     */
    @GetMapping("/require")
    public ModelAndView require(a) {
        return new ModelAndView("ftl/login");
    }

    /** * User information verification *@paramThe authentication information *@returnUser information */
    @RequestMapping("/user")
    public Object user(Authentication authentication) {
        return authentication.getPrincipal();
    }

    /** * Clear accesstoken refreshToken ** from Redis@param accesstoken  accesstoken
     * @return true/false
     */
    @PostMapping("/removeToken")
    @CacheEvict(value = SecurityConstants.TOKEN_USER_DETAIL, key = "#accesstoken")
    public R<Boolean> removeToken(String accesstoken) {
        return newR<>( consumerTokenServices.revokeToken(accesstoken)); }}Copy the code

The interface layer has three interface paths, the first one should be useless, the other two are /user to verify user information and /removeToken to remove accesstoken and refreshToken in Redis

Framework configuration

Framework configuration

The following code configures various Spring Security configurations, including the login interface url “/authentication/require”. If you use your own page instead of the default popup, the form action is “/authentication/form”. Use your own filter rules. CSRF is disabled (search CSRF by yourself, JWT validation is not cross-domain proof, but XSS filtering is required). Login configuration using mobile phone.

@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<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); }}Copy the code

Verifying User Information

Read configuration class and interface layer, we know that the general logic is that the user login, using the Spring Security framework authentication to obtain permissions.

Let’s go step by step and guess as we go. Freemarker (freemarker, freemarker, Freemarker, Freemarker, Freemarker, Freemarker, Freemarker, Freemarker) I don’t see any custom code at all.

Originally, the authors used the Spring Security framework to implement validation information using the framework.

Check PigAuthorizationConfig under config package to find out.

Implement the authorization server using Spring Security

Noted, read the module need to request basis, blog. Tengshe789. Tech / 2018/12/02 /…

As a quick mention, Spring Security OAuth has two concepts, authorization server and resource server.

The authorization server issues access tokens to clients based on authorization and provides authentication and authorization services.

The resource server needs to validate this access token before the client can access the corresponding service.

Customer details service configuration

ClientDetailsServiceConfigurer (AuthorizationServerConfigurer a callback configuration items) to implement customer details to be able to use memory or JDBC service (ClientDetailsService), Spring Security OAuth2 Configuration approach is to write @ AuthorizationServerConfigurerAdapter Configuration class inheritance, Then rewrite the void the configure (ClientDetailsServiceConfigurer clients) method

The main logic of the following code is to query the details of the client using a simple SQL connector encapsulated in the Spring Security framework πŸ‘‡

	@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);
    }
Copy the code

The related SQL statement is as follows. Due to the large degree of coupling, I have changed the SQL statement.

 /** * The default query statement */
    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";

    /** * select */ by condition 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 = ?";
Copy the code

The related database information is as follows:

Authorization server endpoint configurator

What is the endPoints argument? All requests for tokens will be handled in Spring MVC Controller endpoints

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token enhanced configuration
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(
                Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));

        endpoints
                .tokenStore(redisTokenStore())
                .tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager)
                .reuseRefreshTokens(false)
                .userDetailsService(userDetailsService);
    }
Copy the code
Token enhancer (customizing the information carried in token messages)

Sometimes additional information needs to be added to the token return. This section can also be customized. In this case, we can customize a TokenEnhancer to generate the information carried by the token from the definition. The TokenEnhancer interface provides a Enhance (OAuth2AccessToken VAR1, OAuth2Authentication VAR2) method for adding token information, This information is from OAuth2Authentication.

The author added his name to the accessToken, added the userId

@Bean
    public TokenEnhancer tokenEnhancer(a) {
        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;
        };
    }
Copy the code
JWT converter (information added to custom token message)

In JWT, we need to carry additional information in the token so that some user information can be shared between services. Spring Security adds user_name to JWT tokens by default. If we need additional information, we need to customize this part.

JwtAccessTokenConverter is a converter that uses JWT to replace the default Token, which by default has a signature that the resource server needs to verify. There are two methods of encryption and verification:

  • Symmetric encryption

  • Asymmetric encryption (public key)

Symmetric encryption requires that the authorization server and resource server store the same key value, whereas asymmetric encryption can be encrypted using a key to expose the public key for signature verification on the resource server


    public class PigJwtAccessTokenConverter extends JwtAccessTokenConverter {
    @Override
    publicMap<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;
    }

    @Override
    public OAuth2AccessToken extractAccessToken(String value, Map
       
         map)
       ,> {
        return super.extractAccessToken(value, map);
    }

    @Override
    public OAuth2Authentication extractAuthentication(Map
       
         map)
       ,> {
        return super.extractAuthentication(map); }}Copy the code
Redis and token

The endpoint that uses authentication puts the token with its own name into redis, the Srping Data Redis framework used by the Redis connector

 /** * TokenStore customization **@return1. PigRedisTokenStore * PigRedisTokenStore TokenStore = new PigRedisTokenStore();  * tokenStore.setRedisTemplate(redisTemplate); * /
    @Bean
    public TokenStore redisTokenStore(a) {
        RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
        tokenStore.setPrefix(SecurityConstants.PIG_PREFIX);
        return tokenStore;
    }
Copy the code

Authorization server security configurator

@Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .allowFormAuthenticationForClients()
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()");
    }
Copy the code

User-defined mobile phone number authentication service

The interface layer

The interface layer is linked to pig-upms-service and is given three paths through which users can log in using their mobile phone numbers to send requests

@FeignClient(name = "pig-upms-service", fallback = UserServiceFallbackImpl.class)
public interface UserService {
    /** * Query user and role information by user name **@paramUsername username *@return UserVo
     */
    @GetMapping("/user/findUserByUsername/{username}")
    UserVO findUserByUsername(@PathVariable("username") String username);

    /** * Query user and role information by mobile phone number **@paramMobile Phone number *@return UserVo
     */
    @GetMapping("/user/findUserByMobile/{mobile}")
    UserVO findUserByMobile(@PathVariable("mobile") String mobile);

    /** * Query user information based on OpenId *@param openId openId
     * @return UserVo
     */
    @GetMapping("/user/findUserByOpenId/{openId}")
    UserVO findUserByOpenId(@PathVariable("openId") String openId);
}
Copy the code

The configuration class

Rewrite SecurityConfigurerAdapter method, through the HTTP request, find out about the phone number of token, use token to find relevant user information, save the Authentication way. Once you have the information, use the filter to verify it

@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 = newMobileAuthenticationProvider(); mobileAuthenticationProvider.setUserService(userService); http.authenticationProvider(mobileAuthenticationProvider) .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Mobile phone number MobileAuthenticationProvider login validation logic

In Spring Security, AuthenticationManage manages a series of AuthenticationProviders, And every Provider UserDetailsService and UserDetail motivated to return to a MobileAuthenticationToken implementation of Authentication with users and permissions

The logic here is that UserService is used to find the mobile phone number of an existing user and generate the corresponding UserDetails. The mobile phone Authentication is generated using UserDetails

@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("Mobile phone number does not exist :" + 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);
    }
Copy the code
Mobile phone number login MobileAuthenticationToken token class

MobileAuthenticationToken inheritance AbstractAuthenticationToken Authentication So when the mobile phone in the page after the first into the MobileAuthenticationToken validation (Authentication), then generate the Authentication will be managed by I said above the 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
        authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getPrincipal(a) {
        return this.principal;
    }

    @Override
    public Object getCredentials(a) {
        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(a) {
        super.eraseCredentials(); }}Copy the code

Mobile phone number Login verification filter

Check whether the HTTP request is A POST, or return an error.

Obtain the Moblie information according to the request, and use the Moblie information to return the oauth token with the mobile phone number.

@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);
    }
Copy the code

Mobile processor MobileLoginSuccessHandler log in successfully

The handler can return the oauth token for the successful login with the mobile number, but the oAuth token must be matched with the mobile number login verification filter to transfer the oAuth token

The logic is all in the comments

@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 information in request header is empty");
        }

        try {
            String[] tokens = AuthUtils.extractAndDecodeHeader(header);
            assert tokens.length == 2;
            String clientId = tokens[0];

            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

            / / check 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");

            / / check the scope
            new DefaultOAuth2RequestValidator().validateScope(tokenRequest, clientDetails);
            OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
            OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
            log.info("Succeeded in obtaining 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"); }}/** * From the header request clientId/ ClientSecect **@paramHeader Parameter * in the 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)};
    }
Copy the code

Other configuration

Redis cluster

Good template, collect it

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(a) {
        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;
    }

    @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) {

            intseconds = 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<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);
    }

    @Override
    public 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);
        }

        returnCollections.unmodifiableCollection(accessTokens); }}Copy the code

Service Gateway module

The gateway body is under the package pig\pig-gateway\ SRC \main\ Java \com\github\pig\gateway

Zuul, Netflix’s open source microservice gateway, can be used with Eureka,Ribbon,Hystrix and other components.

At the heart of the Zuul component is a series of filters that perform the following functions:

  • Authentication and security: Identify authentication requirements for each resource and reject those that do not comply

  • Review and monitoring:

  • Dynamic routing: Requests are dynamically routed to different back-end clusters

  • Stress test: Incrementally increase traffic to the cluster to understand performance

  • Load allocation: Allocate capacity for each load type and discard requests that exceed the limit

  • Static response processing: The edge location responds to avoid forwarding to the internal cluster

  • Multi-region elasticity: ElasticLoad Balancing (ELB) implements request routing across AWS regions to achieve diverse usage

Multi-function filter filter

At the heart of the Zuul component is a series of filters, starting with filters.

Unified gateway exception filter

@Component
public class ErrorHandlerFilter extends ZuulFilter {
    @Autowired
    private LogSendService logSendService;
    @Override
    public String filterType(a) {
        return ERROR_TYPE;
    }
    @Override
    public int filterOrder(a) {
        return SEND_RESPONSE_FILTER_ORDER + 1;
    }
    @Override
    public boolean shouldFilter(a) {
        RequestContext requestContext = RequestContext.getCurrentContext();
        returnrequestContext.getThrowable() ! =null;
    }
    @Override
    public Object run(a) {
        RequestContext requestContext = RequestContext.getCurrentContext();
        logSendService.send(requestContext);
        return null; }}Copy the code

The authors add a log configuration based on the native Zuul filter with a priority of +1, and a higher number indicates a lower priority.

XSS filter

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);
    }
Copy the code

Override springMVC’s OncePerRequestFilter class to ensure that only one filter is passed in a request, add a gitee.com/renrenio/re…

@Override
    public ServletInputStream getInputStream(a) throws IOException {... slightly/ / XSS filtering
        json = xssEncode(json);
        final ByteArrayInputStream bis = new ByteArrayInputStream(json.getBytes("utf-8"));
        return newServletInputStream() {Β·Β·Β· slightly}}; }Copy the code

Password filter DecodePasswordFilter

This filter has a priority of +2. Aes decoder password is resolved whenever a request is not requested at /oauth/token or /mobile/token.

@Override
    public Object run(a) {
        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 decryption failed :{}", password);
            }
            params.put(PASSWORD, CollUtil.newArrayList(password.trim()));
        }
        ctx.setRequestQueryParams(params);
        return null;
    }
Copy the code

Verification code filter ValidateCodeFilter

The logical authors are written in the comments, where redis is used as the server side cache of the captcha

** * Whether to verify the verification code *1.Check whether the verification code switch is enabled *2.Determine whether the request is a login request *2.1Determine whether the request is refreshed (do not create a refresh client separately) *3.Check whether the terminal supports * *@return true/false* /@Override
    public boolean shouldFilter(a) {
        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("Failed to parse terminal information", e);
        }

        return true;
    }

    @Override
    public Object run(a) {
        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;
    }

    /** * check code **@param httpServletRequest request
     * @throwsValidateCodeException Verification code is abnormal */
    private void checkCode(HttpServletRequest httpServletRequest) throws ValidateCodeException {
        String code = httpServletRequest.getParameter("code");
        if (StrUtil.isBlank(code)) {
            throw new ValidateCodeException("Please enter the verification code.");
        }

        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("Verification code error, please re-enter");
        }

        redisTemplate.delete(key);
    }
Copy the code

Gray released

Grayscale publishing is not a very new concept. If a product needs rapid iterative development and launch, it needs to ensure quality and ensure that the newly launched system can quickly control the impact surface once problems occur. Therefore, it is necessary to design a grayscale release system.

The function of grayscale publishing system is that it can guide the user’s traffic to the new online system according to its own configuration, to quickly verify the new function modification, and once there is A problem, it can also be immediately restored. In short, it is A set of A/BTest system.

Initialize the

Here is the grayscale route initialization class:

@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(a) {
        return newMetadataCanaryRuleHandler(); }}Copy the code

Grayscale publishing is about AccessFilter

First override the filterOrder() method so that the filter runs before the RateLimitPreFilter without null pointer problems. The priority is 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(a) {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder(a) {
        return FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter(a) {
        return true;
    }

    @Override
    public Object run(a) {
        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; }}Copy the code

The core method on run() receives the request first, gets its version constraint information, and then adds the token as selected

Routing micro service assertions MetadataCanaryRuleHandler processor

Custom Ribbon routing rules match multiple version requests for grayscale distribution. Compound judge the performance of the area where the server is located and the availability of the server select the server. That is, use ZoneAvoidancePredicate and AvailabilityPredicate to determine whether a server is selected. The former predicate determines whether the operating performance of a zone is available. The unavailable zones (all servers) are excluded. AvailabilityPredicate is used to filter out servers that have too many connections.

The logic here is

  1. Eureka metadata (host name, IP address, port number, status page health check information, or user-defined metadata through the configuration file) exists when the version is defined
  2. Returns true if metadata does not exist
@Override
    public AbstractServerPredicate getPredicate(a) {
        return new AbstractServerPredicate() {
            @Override
            public boolean apply(PredicateKey predicateKey) {
                String targetVersion = RibbonVersionHolder.getContext();
                RibbonVersionHolder.clearContext();
                if (StrUtil.isBlank(targetVersion)) {
                    log.debug("Client is not configured with target version direct routing");
                    return true;
                }

                DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer();
                final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
                if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) {
                    log.debug("Current microservice {} not configured with version Direct routing");
                    return true;
                }

                if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) {
                    return true;
                } else {
                    log.debug("Current microservice {} version is {}, target version {} failed to match", server.getInstanceInfo().getAppName()
                            , metadata.get(SecurityConstants.VERSION), targetVersion);
                    return false; }}}; }Copy the code

Dynamic routing

configuration

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;
    }

    /** * Rewrite route configuration * <p> * 1. properties configuration. * 2. Default eureka configuration. * 3. Configure the DB database. * *@returnRouting table * /
    @Override
    protected LinkedHashMap<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        // Read the properties configuration, eureka default configuration
        routesMap.putAll(super.locateRoutes());
        log.debug("Initial default route configuration complete.");
        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;
    }

    /** * Save in Redis, not pulled from UPMS, avoid startup link dependency problem, gateway dependency business module problem **@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("Failed to load route configuration from database", e);
            }
            log.debug("Add database custom route configuration,path :{}, serviceId:{}", zuulRoute.getPath(), zuulRoute.getServiceId());
            routes.put(zuulRoute.getPath(), zuulRoute);
        }
        returnroutes; }}Copy the code

Gateway Log Processing

The logic is clearly written in the code comments

@Slf4j
@Component
public class LogSendServiceImpl implements LogSendService {
    private static final String SERVICE_ID = "serviceId";
    @Autowired
    private AmqpTemplate rabbitTemplate;

    /** * 1. Get the request information from requestContext * 2. If the return status is not OK, get the error message * in the return message. 3. Send to MQ * *@paramRequestContext Context object */
    @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());
        }

        // Normal send service exception resolution
        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("Response flow parsing exception:", e);
                throw new RuntimeException(e);
            } finally{ IoUtil.close(stream1); IoUtil.close(baos); IoUtil.close(inputStream); }}// An internal gateway exception occurs
        Throwable throwable = requestContext.getThrowable();
        if(throwable ! =null) {
            log.error("Gateway exception", throwable);
            sysLog.setException(throwable.getMessage());
        }
        // Save sending to MQ (save authorization only)
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(authentication ! =null && StrUtil.isNotBlank(authentication.getName())) {
            LogVO logVo = newLogVO(); sysLog.setCreateBy(authentication.getName()); logVo.setSysLog(sysLog); logVo.setUsername(authentication.getName()); rabbitTemplate.convertAndSend(MqQueueConstant.LOG_QUEUE, logVo); }}}Copy the code

Multi-dimensional current limiting

Current limiting relegation ZuulRateLimiterErrorHandler processor

Rewrite zuul default current-limiting processor DefaultRateLimiterErrorHandler, log content

@Bean
    public RateLimiterErrorHandler rateLimitErrorHandler(a) {
        return new DefaultRateLimiterErrorHandler() {
            @Override
            public void handleSaveError(String key, Exception e) {
                log.error("Save key:[{}] exception", key, e);
            }

            @Override
            public void handleFetchError(String key, Exception e) {
                log.error("Routing failure :[{}] exception", key);
            }

            @Override
            public void handleError(String msg, Exception e) {
                log.error("Current limiting exception :[{}]", msg, e); }}; }Copy the code

Integrate single sign-on with spring Security oAuth methods

Authorization denial handler PigAccessDeniedHandler

Rewrite the Srping security request to provide single sign-on authentication rejected OAuth2AccessDeniedHandler interface, use the information to PigDeniedException R package failure

@Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
        log.info("Authorization failed, access forbidden {}", request.getRequestURI());
        response.setCharacterEncoding(CommonConstant.UTF8);
        response.setContentType(CommonConstant.CONTENT_TYPE);
        R<String> result = new R<>(new PigDeniedException("Authorization failed. Access forbidden."));
        response.setStatus(HttpStatus.SC_FORBIDDEN);
        PrintWriter printWriter = response.getWriter();
        printWriter.append(objectMapper.writeValueAsString(result));
    }
Copy the code

Menu management

MenuService

@FeignClient(name = "pig-upms-service", fallback = MenuServiceFallbackImpl.class)
public interface MenuService {
    /** * Query menu ** by role name@paramRole Role name *@returnMenu list */
    @GetMapping(value = "/menu/findMenuByRole/{role}")
    Set<MenuVO> findMenuByRole(@PathVariable("role") String role);
}
Copy the code

Connect to the pig system’s menu microservice using Feign

Menu permissions

@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 cross-domain configuration. Currently, it is handled through the front-end configuration proxy. There are risks if you do not use this method
// 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("Role list is empty: {}", authentication.getPrincipal());
                return false;
            }

            Set<MenuVO> urls = newHashSet<>(); 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));
        }
        returnhasPermission.get(); }}Copy the code

The gateway to summarize

The zuul gateway module of Pig is integrated with Feign, ribbon, Spring Security and Eurasia. It has completed or partially completed dynamic routing, gray level publishing, menu permission management, service flow limiting, gateway log processing. Very worth learning!

UPMs Permission management system module

UPMS is a User Permissions Management System

Database design

Department of table

Departmental relation table

A dictionary table

/** ** id */
	@TableId(value="id", type= IdType.AUTO)
	private Integer id;
    /** * Data value */
	private String value;
    /** * Label name */
	private String label;
    /** * type */
	private String type;
    /** * description */
	private String description;
    /**
     * ζŽ’εΊοΌˆε‡εΊοΌ‰
     */
	private BigDecimal sort;
    /** * create time */
	@TableField("create_time")
	private Date createTime;
    /** * update time */
	@TableField("update_time")
	private Date updateTime;
    /** * Note information */
	private String remarks;
    /** * delete the flag */
	@TableField("del_flag")
	private String delFlag;
Copy the code

The log table

@Data
public class SysLog implements Serializable {
    private static final long serialVersionUID = 1L;
    /** ** id */
    @TableId(type = IdType.ID_WORKER)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    /** * Log type */
    private String type;
    /** * Log title */
    private String title;
    /** * creator */
    private String createBy;
    /** * create time */
    private Date createTime;
    /** * update time */
    private Date updateTime;
    /** * Operation IP address */
    private String remoteAddr;
    /** * User agent */
    private String userAgent;
    /** * request URI */
    private String requestUri;
    /** ** Operation mode */
    private String method;
    /** * Data submitted by operation */
    private String params;
    /**
     * ζ‰§θ‘Œζ—Άι—΄
     */
    private Long time;
    /** * delete the flag */
    private String delFlag;
    /** * Exception message */
    private String exception;
    /** * Service ID */
    private String serviceId; }}
Copy the code

Menu permission list

Character sheet

Relationship between roles and departments

slightly

Mapping between roles and menu permissions

slightly

The users table

/** * primary key ID */
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
/** * User name */
private String username;

private String password;
/** * random salt */
@JsonIgnore
private String salt;
/** * create time */
@TableField("create_time")
private Date createTime;
/** * Change the time */
@TableField("update_time")
private Date updateTime;
/** * 0- Normal, 1- delete */
@TableField("del_flag")
private String delFlag;

/** ** /
private String phone;
/** ** avatar */
private String avatar;

/** * Department ID */
@TableField("dept_id")
private Integer deptId;
Copy the code

Dynamic routing configuration table

The business logic

All CRUD based on Mybatis Plus, a bit much. Most people in this business know that, so I won’t go into all the details.

Verification code

create

ValidateCodeController can find the code associated with creating a captcha

/** * Create a verification code **@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, "Machine code cannot be empty.");
        response.setHeader("Cache-Control"."no-store, no-cache");
        response.setContentType("image/jpeg");
        // Generate a literal captcha
        String text = producer.createText();
        // Generate image captcha
        BufferedImage image = producer.createImage(text);
        userService.saveImageCode(randomStr, text);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image, "JPEG", out);
        IOUtils.closeQuietly(out);
    }
Copy the code

Producer uses Kaptcha. Here are the configuration classes

@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(a) {
        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);
        returndefaultKaptcha; }}Copy the code

Send the mobile verification code

Check the redis cache of the verification code first. If no cache is found, the verification code cache is not invalid and an error is returned.

If no verification code is found, the user information is obtained from the database according to the mobile phone number, and a 4-bit verification code is generated. The RABbBITMQ queue is used to save the SMS verification code to the queue, and the redis cache of the mobile verification code is added

/** * Send verification code * <p> * 1. Go to Redis to check whether the verification code has been sent within 60 seconds. Not sent: Check whether the mobile phone number is saved? False: Generate a 4-digit mobile phone number - Verification code * 3. Send a message to the message center - 4. Save redis * *@paramMobile Phone number *@returnTrue and false * /
    @Override
    public R<Boolean> sendSmsCode(String mobile) {
        Object tempCode = redisTemplate.opsForValue().get(SecurityConstants.DEFAULT_CODE_KEY + mobile);
        if(tempCode ! =null) {
            log.error("User :{} Verification code is not invalid {}", mobile, tempCode);
            return new R<>(false."Verification code is not invalid, please apply again after expiration.");
        }

        SysUser params = new SysUser();
        params.setPhone(mobile);
        List<SysUser> userList = this.selectList(new EntityWrapper<>(params));

        if (CollectionUtil.isEmpty(userList)) {
            log.error("Query user by user mobile number {} is null", mobile);
            return new R<>(false."Phone number does not exist.");
        }

        String code = RandomUtil.randomNumbers(4);
        JSONObject contextJson = new JSONObject();
        contextJson.put("code", code);
        contextJson.put("product"."Pig4Cloud");
        log.info("SMS send request Message Center -> Mobile phone number :{} -> Verification code :{}", 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);
    }
Copy the code

Tree node toolbar

public class TreeUtil {
    /** * two layers of loop implementation tree **@paramTreeNodes passed a list of 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(newArrayList<TreeNode>()); } treeNode.add(it); }}}return trees;
    }

    /** * use recursive method to build tree **@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;
    }

    /** * recursively finds the child node **@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(newArrayList<TreeNode>()); } treeNode.add(findChildren(it, treeNodes)); }}return treeNode;
    }

    /** * Create a tree node ** by 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);
        }
        returnTreeUtil.bulid(trees, root); }}Copy the code

Generate avUE template classes

public class PigResourcesGenerator {


    public static void main(String[] args) {
        String outputDir = "/Users/lengleng/work/temp";
        final String viewOutputDir = outputDir + "/view/";
        AutoGenerator mpg = new AutoGenerator();
        // Global configuration
        GlobalConfig gc = new GlobalConfig();
        gc.setOutputDir(outputDir);
        gc.setFileOverride(true);
        gc.setActiveRecord(true);
        // XML level 2 cache
        gc.setEnableCache(false);
        // XML ResultMap
        gc.setBaseResultMap(true);
        // XML columList
        gc.setBaseColumnList(true);
        gc.setAuthor("lengleng");
        mpg.setGlobalConfig(gc);

        // Data source configuration
        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);

        // Policy configuration
        StrategyConfig strategy = new StrategyConfig();
        // strategy.setCapitalMode(true); // Global uppercase name ORACLE note
        strategy.setSuperControllerClass("com.github.pig.common.web.BaseController");
        // Table name generation policy
        strategy.setNaming(NamingStrategy.underline_to_camel);
        mpg.setStrategy(strategy);

        / / package configuration
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.github.pig.admin");
        pc.setController("controller");
        mpg.setPackageInfo(pc);

        // Inject custom configuration, which can be set to cfg. ABC in the VM
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap(a) {}};// Create a template path if it does not exist
        File viewDir = new File(viewOutputDir);
        if(! viewDir.exists()) { viewDir.mkdirs(); } List<FileOutConfig> focList =new ArrayList<FileOutConfig>();
        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);// Generate controller correlation
        mpg.execute();
    }

    /** * Get the configuration file **@returnConfiguration Props * /
    private static Properties getProperties(a) {
        // Read the configuration file
        Resource resource = new ClassPathResource("/config/application.properties");
        Properties props = new Properties();
        try {
            props = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return props;
    }

    /** * the file name generated by the page */
    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(); }returnpath; }}Copy the code

Velocity templates

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.ment} Front-end controller * </p> * *@author$! {author} *@since$! {date} */
@RestController
@RequestMapping("/$! {table.entityPath}")
public class $!{table.controllerName} extends $! {superControllerClass} {@Autowired private$! {entity}Service $! {table.entityPath}Service;/** * query ** by ID@param id ID
    * @return$! {entity} */
    @GetMapping("/{id}")
    publicR<$! {entity}> get(@PathVariable Integer id) {
        return newR<>($! {table.entityPath}Service.selectById(id)); }/** ** query information **@paramParams paging object *@returnPaging object */
    @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<>());
    }

    /** * add *@param$! * {table. EntityPath} entities@return success/false
     */
    @PostMapping
    public R<Boolean> add(@RequestBody$! {entity} $! {table.entityPath}) {
        return newR<>($! {table.entityPath}Service.insert($! {table.entityPath})); }/** * delete *@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(newDate()); $! {table.entityPath}.setDelFlag(CommonConstant.STATUS_DEL);return newR<>($! {table.entityPath}Service.updateById($! {table.entityPath})); }/** * edit *@param$! * {table. EntityPath} entities@return success/false
     */
    @PutMapping
    public R<Boolean> edit(@RequestBody$! {entity} $! {table.entityPath}) {
        $!{table.entityPath}.setUpdateTime(new Date());
        return newR<>($! {table.entityPath}Service.updateById($! {table.entityPath})); }}Copy the code

The cache

In part of the implementation class, we see the author using spring Cache-related annotations. Now let’s recall what the relevant cache annotations mean:

@cacheable: Defines the cache. We use value,key; You can also use cacheName for value, which means the same thing if you look at the source code.

@cacheevict: Used to clean the cache. Commonly used entries are cacheNames and allEntries (default: false); Represents the name of the cache to be cleared and whether all of it is cleared (true means all of it).

@cachePUT: Used to update the cache. Methods annotated with it are executed and the result is added to the cache. This method cannot be used on the same method as @cacheable.

Background running batch scheduled task module

Elastic-job is a distributed Elastic Job framework separated from the Job module of DD-Job in ddFrame. The monitoring and DDFrame access specifications in and DD-Job are deleted. This project is based on the secondary development of mature open source products Quartz and Zookeeper and their client Curator. The main functions are as follows:

  • Scheduled tasks: Perform scheduled tasks based on the mature Quartz cron expression.
  • Job registry: Global job registry control center based on Zookeeper and its client Curator implementation. Used to register, control, and coordinate distributed job execution.
  • Job sharding: Sharding a task into multiple small task items for simultaneous execution on multiple servers.
  • Elastic capacity expansion: If a running job server crashes or n new job servers are added, the job framework is fragmented before the next job execution, which does not affect the current job execution.
  • Support multiple job execution modes: OneOff, Perpetual and SequencePerpetual are supported.
  • Failover: A running job server crash does not result in resharding, only when the next job starts. The failover function can be enabled to monitor that other job servers are idle and capture orphan fragments that are not completed for execution during this job execution.
  • Runtime status collection: Monitors the running status of jobs, collects statistics on the number of data successes and failures processed in a recent period of time, and records the start time, end time, and next running time of jobs.
  • ** Job stop, Resume, and Disable: ** Used to start and stop an operation job and to disable a job (often used online).
  • ** Refiring of missed jobs: ** Automatically records missed jobs and triggers automatically after the last job has completed. See Quartz’s Misfire.
  • ** Multithreading fast data processing: ** Use multithreading to process captured data to improve throughput.
  • ** Idempotence: ** Repeat job task items, do not repeat the running job task items. Since enabling idempotency requires monitoring the running state of jobs, it has a great impact on the performance of jobs that run repeatedly instantaneously.
  • ** Fault tolerance processing: ** If the job server fails to communicate with the Zookeeper server, the job will be stopped immediately to prevent the job registry from allocating the invalid shard items to other job servers. However, the current job server is still performing tasks, resulting in repeated execution.
  • **Spring support: ** Supports Spring containers, custom namespaces, and placeholders.
  • ** Operation and maintenance platform: ** Provides an operation and maintenance interface to manage jobs and registries.

configuration

The author directly used the configuration of the open source project, and I followed his POM file to find the Github address as follows

Github.com/xjzrc/elast…

Workflow Job configuration

@ElasticJobConfig(cron = "0 0 0/1 * * ? ", shardingTotalCount = 3, shardingItemParameters = "0=Beijing,1=Shanghai,2=Guangzhou")
public class PigDataflowJob implements DataflowJob<Integer> {


    @Override
    public List<Integer> fetchData(ShardingContext shardingContext) {
        return null;
    }

    @Override
    public void processData(ShardingContext shardingContext, List<Integer> list) {}}Copy the code

The test code

@Slf4j
@ElasticJobConfig(cron = "0 0 0/1 * * ?" , shardingItemParameters shardingTotalCount = 3 = "0 = pig1, 1 = pig2, 2 = pig3", startedTimeoutMilliseconds = 5000 l, completedTimeoutMilliseconds = 10000L, eventTraceRdbDataSource = "dataSource")
public class PigSimpleJob implements SimpleJob {
    /** * Business execution logic **@paramShardingContext Indicates the fragment information */
    @Override
    public void execute(ShardingContext shardingContext) {
        log.info("shardingContext:{}", shardingContext); }}Copy the code

The open source version has limited support for this, I will do analysis when I get the paid version.

The message center

The message center here is mainly integrated nail nail service and Ali big fish short message service

nailing

configuration

Nailing is pretty simple, just a Webhook message is enough.

Webhook is a Web callback or HTTP push API that provides real-time information to an APP or other application. Webhook sends data as soon as it is generated, meaning you can receive it in real time. This is different from typical apis in that real-time polling is needed fast enough. This is efficient for both production and consumers, with the only drawback being the difficulty of initial setup. Webhook is sometimes called a reverse API because it provides API rules that you need to design to use. Webhook will make HTTP requests to your application, typically POST requests, and the application is request-driven.

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

The message template

/ * * *@author lengleng
 * @dateMsgtype: text * text: {"content":" pig-upms-service status: UP"} */
@Data
@ToString
public class DingTalkMsgTemplate implements Serializable {
    private String msgtype;
    private TextBean text;

    public String getMsgtype(a) {
        return msgtype;
    }

    public void setMsgtype(String msgtype) {
        this.msgtype = msgtype;
    }

    public TextBean getText(a) {
        return text;
    }

    public void setText(TextBean text) {
        this.text = text;
    }

    public static class TextBean {
        /** * content: pig-upms-service Status: UP */

        private String content;

        public String getContent(a) {
            return content;
        }

        public void setContent(String content) {
            this.content = content; }}}Copy the code

Listening to the

Use queues to listen constantly

@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("Message center received a spike send request -> Contents: {}", text);
        dingTalkMessageHandler.process(text);
        long useTime = System.currentTimeMillis() - startTime;
        log.info("Call nail gateway processing completed, time {} ms", useTime); }}Copy the code

send

Using queues to send

@Slf4j
@Component
public class DingTalkMessageHandler {
    @Autowired
    private DingTalkPropertiesConfig dingTalkPropertiesConfig;

    /** ** Business processing **@paramThe text message * /
    public boolean process(String text) {
        String webhook = dingTalkPropertiesConfig.getWebhook();
        if (StrUtil.isBlank(webhook)) {
            log.error("Nail configuration error, webhook is empty");
            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("Nailed alert successful, message response :{}", result);
        return true; }}Copy the code

Ali Big Fish SMS service

configuration

@Data
@Configuration
@ConditionalOnExpression("! '${sms.aliyun}'.isEmpty()")
@ConfigurationProperties(prefix = "sms.aliyun")
public class SmsAliyunPropertiesConfig {
    /**
     * 应用ID
     */
    private String accessKey;

    /** * apply the key */
    private String secretKey;

    /** * SMS template configuration */
    private Map<String, String> channels;
}
Copy the code

Listening to the

@Slf4j
@Component
@RabbitListener(queues = MqQueueConstant.MOBILE_SERVICE_STATUS_CHANGE)
public class MobileServiceChangeReceiveListener {
    @Autowired
    private Map<String, SmsMessageHandler> messageHandlerMap;


    @RabbitHandler
    public void receive(MobileMsgTemplate mobileMsgTemplate) {
        long startTime = System.currentTimeMillis();
        log.info("Message center receives SMS send request -> Mobile number: {} -> Message body: {}", mobileMsgTemplate.getMobile(), mobileMsgTemplate.getContext());
        String channel = mobileMsgTemplate.getChannel();
        SmsMessageHandler messageHandler = messageHandlerMap.get(channel);
        if (messageHandler == null) {
            log.error("No routing channel found, no send processing completed!");
            return;
        }

        messageHandler.execute(mobileMsgTemplate);
        long useTime = System.currentTimeMillis() - startTime;
        log.info("Call to {} SMS gateway completed, took {} ms", mobileMsgTemplate.getType(), useTime); }}Copy the code

send

Nice template

@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";

    /** * data verification **@paramMobileMsgTemplate news * /
    @Override
    public void check(MobileMsgTemplate mobileMsgTemplate) {
        Assert.isBlank(mobileMsgTemplate.getMobile(), "Cell phone number cannot be empty.");
        Assert.isBlank(mobileMsgTemplate.getContext(), "Text messages cannot be empty.");
    }

    /** ** Business processing **@paramMobileMsgTemplate news * /
    @Override
    public boolean process(MobileMsgTemplate mobileMsgTemplate) {
        // You can adjust the timeout duration by yourself
        System.setProperty("sun.net.client.defaultConnectTimeout"."10000");
        System.setProperty("sun.net.client.defaultReadTimeout"."10000");

        // Initialize acsClient. Regionalization is not supported
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsAliyunPropertiesConfig.getAccessKey(), smsAliyunPropertiesConfig.getSecretKey());
        try {
            DefaultProfile.addEndpoint("cn-hou"."cn-hangzhou", PRODUCT, DOMAIN);
        } catch (ClientException e) {
            log.error("Initialization SDK exception", e);
            e.printStackTrace();
        }
        IAcsClient acsClient = new DefaultAcsClient(profile);

        // Assemble the request object - see the console - documentation section for details
        SendSmsRequest request = new SendSmsRequest();
        // Required: Mobile phone number to be sent
        request.setPhoneNumbers(mobileMsgTemplate.getMobile());

        // Required: SMS signature - can be found in SMS console
        request.setSignName(mobileMsgTemplate.getSignName());

        // Required: SMS template - Available in the SMS console
        request.setTemplateCode(smsAliyunPropertiesConfig.getChannels().get(mobileMsgTemplate.getTemplate()));

        // Optional: Replace the JSON string with a variable in the template, such as "dear ${name}, your verification code is ${code}"
        request.setTemplateParam(mobileMsgTemplate.getContext());
        request.setOutId(mobileMsgTemplate.getMobile());

        //hint may throw an exception
        try {
            SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
            log.info("SMS sent, mobile phone number: {}, return status: {}", mobileMsgTemplate.getMobile(), sendSmsResponse.getCode());
        } catch (ClientException e) {
            log.error("Send exception");
            e.printStackTrace();
        }
        return true;
    }

    /** * Failed to process **@paramMobileMsgTemplate news * /
    @Override
    public void fail(MobileMsgTemplate mobileMsgTemplate) {
        log.error("SMS sending failed -> Gateway: {} -> Mobile number: {}", mobileMsgTemplate.getType(), mobileMsgTemplate.getMobile()); }}Copy the code

Resource Authentication server (single sign-on)

Since the author uses the Spring Security OAuth framework in the authentication center, a resource authentication server needs to be implemented on the client side of the microservice to fulfill SSO requirements.

configuration

Exposing Monitoring Information

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

interface

@EnableOAuth2Sso
@SpringBootApplication
public class PigSsoClientDemoApplication {

    public static void main(String[] args) { SpringApplication.run(PigSsoClientDemoApplication.class, args); }}Copy the code

Monitoring module

Springboot admin configuration

RemindingNotifier can be used to send notifications to other notifiers when an application is up or down. RemindingNotifier can be used to send notifications to other notifiers when an application is up or down. Notifications can be sent in several ways: Pagerduty, Hipchat, Slack, Mail, Reminder

@Configuration
    public static class NotifierConfig {
        @Bean
        @Primary
        public RemindingNotifier remindingNotifier(a) {
            RemindingNotifier notifier = new RemindingNotifier(filteringNotifier(loggerNotifier()));
            notifier.setReminderPeriod(TimeUnit.SECONDS.toMillis(10));
            return notifier;
        }

        @Scheduled(fixedRate = 1_000L)
        public void remind(a) {
            remindingNotifier().sendReminders();
        }

        @Bean
        public FilteringNotifier filteringNotifier(Notifier delegate) {
            return new FilteringNotifier(delegate);
        }

        @Bean
        public LoggingNotifier loggerNotifier(a) {
            return newLoggingNotifier(); }}Copy the code

Notification of offline SMS service

Inheriting AbstractStatusChangeNotifier, SMS service registry to spring the boot in the 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;
    }

    /** * Notification logic **@paramThe event event *@throwsThe Exception 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("Application :%s Service ID:%s Status changed to :%s, time :%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());

            // Enable SMS notification
            if (monitorMobilePropertiesConfig.getMobile().getEnabled()) {
                log.info("Start SMS notification, content: {}", 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("Start nailing notification, content: {}", text); rabbitTemplate.convertAndSend(MqQueueConstant.DINGTALK_SERVICE_STATUS_CHANGE, text); }}else {
            log.info("Application {} ({}) {}", event.getApplication().getName(), event.getApplication().getId(), event.getType()); }}}Copy the code

Zipkin link tracking

Because Zipkin is intrusive, there is no code for this part of the component, only related dependencies. The author’s YAML is shared below

DB

server:
  port: 5003

# datasoure uses JDBC by default
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. 01.: 5682
      password: lengleng
      username: pig
      queue: zipkin
  storage:
    type: mysql
Copy the code

ELK

server:

  port: 5002



zipkin:

  collector:

    rabbitmq:

      addresses: 127.0. 01.: 5682

      password: lengleng

      username: pig

      queue: zipkin

  storage:

    type: elasticsearch

    elasticsearch:

      hosts: 127.0. 01.: 9200

      cluster: elasticsearch

      index: zipkin

      max-requests: 64

      index-shards: 5

      index-replicas: 1
Copy the code

It’s over. You think I did a good job? Want to learn more about awesome new poses? Open my πŸ‘‰ personal blog πŸ‘ˆ!

Thank you for being so cute and keeping an eye on me ~❀😝