zoukankan      html  css  js  c++  java
  • 基于Spring Boot自建分布式基础应用

      目前刚入职了一家公司,要求替换当前系统(单体应用)以满足每日十万单量和一定系统用户负载以及保证开发质量和效率。由我来设计一套基础架构和建设基础开发测试运维环境,github地址

      出于本公司开发现状及成本考虑,我摒弃了市面上流行的Spring Cloud以及Dubbo分布式基础架构,舍弃了集群的设计,以Spring Boot和Netty为基础自建了一套RPC分布式应用架构。可能这里各位会有疑问,为什么要舍弃应用的高可用呢?其实这也是跟公司的产品发展有关的,避免过度设计是非常有必要的。下面是整个系统的架构设计图。

      这里简单介绍一下,这里ELK或许并非最好的选择,可以另外采用zabbix或者prometheus,我只是考虑了后续可能的扩展。数据库采用了两种存储引擎,便是为了因对上面所说的每天十万单的大数据量,可以采用定时脚本的形式完成数据的转移。

      权限的设计主要是基于JWT+Filter+Redis来做的。Common工程中的com.imspa.web.auth.Permissions定义了所有需要的permissions:

     1 package com.imspa.web.auth;
     2 
     3 /**
     4  * @author Pann
     5  * @description TODO
     6  * @date 2019-08-12 15:09
     7  */
     8 public enum Permissions {
     9     ALL("/all", "所有权限"),
    10     ROLE_GET("/role/get/**", "权限获取"),
    11     USER("/user", "用户列表"),
    12     USER_GET("/user/get", "用户查询"),
    13     RESOURCE("/resource", "资源获取"),
    14     ORDER_GET("/order/get/**","订单查询");
    15 
    16     private String url;
    17     private String desc;
    18 
    19     Permissions(String url, String desc) {
    20         this.url = url;
    21         this.desc = desc;
    22     }
    23 
    24     public String getUrl() {
    25         return this.url;
    26     }
    27 
    28     public String getDesc() {
    29         return this.desc;
    30     }
    31 }

      如果你的没有为你的接口在这里定义权限,那么系统是不会对该接口进行权限的校验的。在数据库中User与Role的设计如下:

     1 CREATE TABLE IF NOT EXISTS `t_user` (
     2   `id`                   VARCHAR(36)  NOT NULL,
     3   `name`                 VARCHAR(20)  NOT NULL UNIQUE,
     4   `password_hash`        VARCHAR(255) NOT NULL,
     5   `role_id`              VARCHAR(36)  NOT NULL,
     6   `role_name`            VARCHAR(20)  NOT NULL,
     7   `last_login_time`      TIMESTAMP(6) NULL,
     8   `last_login_client_ip` VARCHAR(15)  NULL,
     9   `created_time`         TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    10   `created_by`           VARCHAR(36)  NOT NULL,
    11   `updated_time`         TIMESTAMP(6) NULL,
    12   `updated_by`           VARCHAR(36)  NULL,
    13   PRIMARY KEY (`id`)
    14 );
    15 
    16 CREATE TABLE IF NOT EXISTS `t_role` (
    17   `id`           VARCHAR(36)  NOT NULL,
    18   `role_name`    VARCHAR(20)  NOT NULL UNIQUE,
    19   `description`  VARCHAR(90)  NULL,
    20   `permissions`  TEXT         NOT NULL, #其数据格式类似于"/role/get,/user"或者"/all"
    21   `created_time` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    22   `created_by`   VARCHAR(36)  NOT NULL,
    23   `updated_time` TIMESTAMP(6) NULL,
    24   `updated_by`   VARCHAR(36)  NULL,
    25   PRIMARY KEY (`id`)
    26 );

      需要注意的是"/all"代表了所有权限,表示root权限。我们通过postman调用登陆接口可以获取相应的token:

      这个token是半个小时失效的,如果你需要更长一些的话,可以通过com.imspa.web.auth.TokenAuthenticationService进行修改:

     1 package com.imspa.web.auth;
     2 
     3 import com.imspa.web.util.WebConstant;
     4 import io.jsonwebtoken.Jwts;
     5 import io.jsonwebtoken.SignatureAlgorithm;
     6 
     7 import java.util.Date;
     8 import java.util.Map;
     9 
    10 /**
    11  * @author Pann
    12  * @description TODO
    13  * @date 2019-08-14 23:24
    14  */
    15 public class TokenAuthenticationService {
    16     static final long EXPIRATIONTIME = 30 * 60 * 1000; //TODO
    17 
    18     public static String getAuthenticationToken(Map<String, Object> claims) {
    19         return "Bearer " + Jwts.builder()
    20                 .setClaims(claims)
    21                 .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
    22                 .signWith(SignatureAlgorithm.HS512, WebConstant.WEB_SECRET)
    23                 .compact();
    24     }
    25 }

       Refresh Token目前还没有实现,后续我会更新,请关注我的github。如果你跟踪登陆逻辑代码,你可以看到我把role和user都缓存到了Redis:

     1     public User login(String userName, String password) {
     2         UserExample example = new UserExample();
     3         example.createCriteria().andNameEqualTo(userName);
     4 
     5         User user = userMapper.selectByExample(example).get(0);
     6         if (null == user)
     7             throw new UnauthorizedException("user name not exist");
     8 
     9         if (!StringUtils.equals(password, user.getPasswordHash()))
    10             throw new UnauthorizedException("user name or password wrong");
    11 
    12         roleService.get(user.getRoleId()); //for role cache
    13 
    14         hashOperations.putAll(RedisConstant.USER_SESSION_INFO_ + user.getName(), hashMapper.toHash(user));
    15         hashOperations.getOperations().expire(RedisConstant.USER_SESSION_INFO_ + user.getName(), 30, TimeUnit.MINUTES);
    16 
    17         return user;
    18     }

      在Filter中,你可以看到过滤器的一系列逻辑,注意返回http状态码401,403和404的区别:

      1 package com.imspa.web.auth;
      2 
      3 import com.imspa.web.Exception.ForbiddenException;
      4 import com.imspa.web.Exception.UnauthorizedException;
      5 import com.imspa.web.pojo.Role;
      6 import com.imspa.web.pojo.User;
      7 import com.imspa.web.util.RedisConstant;
      8 import com.imspa.web.util.WebConstant;
      9 import io.jsonwebtoken.Claims;
     10 import io.jsonwebtoken.Jwts;
     11 import org.apache.commons.lang3.StringUtils;
     12 import org.apache.logging.log4j.LogManager;
     13 import org.apache.logging.log4j.Logger;
     14 import org.springframework.data.redis.core.HashOperations;
     15 import org.springframework.data.redis.hash.HashMapper;
     16 import org.springframework.util.AntPathMatcher;
     17 
     18 import javax.servlet.Filter;
     19 import javax.servlet.FilterChain;
     20 import javax.servlet.FilterConfig;
     21 import javax.servlet.ServletException;
     22 import javax.servlet.ServletOutputStream;
     23 import javax.servlet.ServletRequest;
     24 import javax.servlet.ServletResponse;
     25 import javax.servlet.http.HttpServletRequest;
     26 import javax.servlet.http.HttpServletResponse;
     27 import java.io.IOException;
     28 import java.util.Date;
     29 import java.util.HashMap;
     30 import java.util.Map;
     31 import java.util.Optional;
     32 import java.util.concurrent.TimeUnit;
     33 
     34 /**
     35  * @author Pann
     36  * @description TODO
     37  * @date 2019-08-16 14:39
     38  */
     39 public class SecurityFilter implements Filter {
     40     private static final Logger logger = LogManager.getLogger(SecurityFilter.class);
     41     private AntPathMatcher matcher = new AntPathMatcher();
     42     private HashOperations<String, byte[], byte[]> hashOperations;
     43     private HashMapper<Object, byte[], byte[]> hashMapper;
     44 
     45     public SecurityFilter(HashOperations<String, byte[], byte[]> hashOperations, HashMapper<Object, byte[], byte[]> hashMapper) {
     46         this.hashOperations = hashOperations;
     47         this.hashMapper = hashMapper;
     48     }
     49 
     50     @Override
     51     public void init(FilterConfig filterConfig) throws ServletException {
     52 
     53     }
     54 
     55     @Override
     56     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
     57         HttpServletRequest request = (HttpServletRequest) servletRequest;
     58         HttpServletResponse response = (HttpServletResponse) servletResponse;
     59 
     60         Optional<String> optional = PermissionUtil.getAllPermissionUrlItem().stream()
     61                 .filter(permissionItem -> matcher.match(permissionItem, request.getRequestURI())).findFirst();
     62         if (!optional.isPresent()) { //TODO some api not config permission will direct do
     63             chain.doFilter(servletRequest, servletResponse);
     64             return;
     65         }
     66 
     67         try {
     68             validateAuthentication(request, optional.get());
     69             flushSessionAndToken(((User) request.getAttribute("userInfo")), response);
     70             chain.doFilter(servletRequest, servletResponse);
     71         } catch (ForbiddenException e) {
     72             logger.debug("occur forbidden exception:{}", e.getMessage());
     73             response.setStatus(403);
     74             ServletOutputStream output = response.getOutputStream();
     75             output.print(e.getMessage());
     76             output.flush();
     77         } catch (UnauthorizedException e) {
     78             logger.debug("occur unauthorized exception:{}", e.getMessage());
     79             response.setStatus(401);
     80             ServletOutputStream output = response.getOutputStream();
     81             output.print(e.getMessage());
     82             output.flush();
     83         }
     84     }
     85 
     86     @Override
     87     public void destroy() {
     88 
     89     }
     90 
     91     private void validateAuthentication(HttpServletRequest request, String permission) {
     92         String authHeader = request.getHeader("Authorization");
     93         if (StringUtils.isEmpty(authHeader))
     94             throw new UnauthorizedException("no auth header");
     95 
     96         Claims claims;
     97         try {
     98             claims = Jwts.parser().setSigningKey(WebConstant.WEB_SECRET)
     99                     .parseClaimsJws(authHeader.replace("Bearer ", ""))
    100                     .getBody();
    101         } catch (Exception e) {
    102             throw new UnauthorizedException(e.getMessage());
    103         }
    104 
    105         String userName = (String) claims.get("user");
    106         String roleId = (String) claims.get("role");
    107 
    108         if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(roleId))
    109             throw new UnauthorizedException("token error,user:" + userName);
    110 
    111         if (new Date().getTime() > claims.getExpiration().getTime())
    112             throw new UnauthorizedException("token expired,user:" + userName);
    113 
    114 
    115         User user = (User) hashMapper.fromHash(hashOperations.entries(RedisConstant.USER_SESSION_INFO_ + userName));
    116         if (user == null)
    117             throw new UnauthorizedException("session expired,user:" + userName);
    118 
    119 
    120         if (validateRolePermission(permission, user))
    121             request.setAttribute("userInfo", user);
    122     }
    123 
    124     private Boolean validateRolePermission(String permission, User user) {
    125         Role role = (Role) hashMapper.fromHash(hashOperations.entries(RedisConstant.ROLE_PERMISSION_MAPPING_ + user.getRoleId()));
    126         if (role.getPermissions().contains(Permissions.ALL.getUrl()))
    127             return Boolean.TRUE;
    128 
    129         if (role.getPermissions().contains(permission))
    130             return Boolean.TRUE;
    131 
    132         throw new ForbiddenException("do not have permission for this request");
    133     }
    134 
    135     private void flushSessionAndToken(User user, HttpServletResponse response) {
    136         hashOperations.getOperations().expire(RedisConstant.USER_SESSION_INFO_ + user.getName(), 30, TimeUnit.MINUTES);
    137 
    138         Map<String, Object> claimsMap = new HashMap<>();
    139         claimsMap.put("user", user.getName());
    140         claimsMap.put("role", user.getRoleId());
    141         response.setHeader("Authorization",TokenAuthenticationService.getAuthenticationToken(claimsMap));
    142     }
    143 
    144 }

      下面是RPC的内容,我是用Netty来实现整个RPC的调用的,其中包含了心跳检测,自动重连的过程,基于Spring Boot的实现,配置和使用都还是很方便的。

      我们先看一下service端的写法,我们需要先定义好对外服务的接口,这里我们在application.yml中定义:

    1 service:
    2   addr: localhost:8091
    3   interfaces:
    4     - 'com.imspa.api.OrderRemoteService'

      其中service.addr是对外发布的地址,service.interfaces是对外发布的接口的定义。然后便不需要你再定义其他内容了,是不是很方便?其实现你可以根据它的配置类com.imspa.config.RPCServiceConfig来看:

     1 package com.imspa.config;
     2 
     3 import com.imspa.rpc.core.RPCRecvExecutor;
     4 import com.imspa.rpc.model.RPCInterfacesWrapper;
     5 import org.springframework.beans.factory.annotation.Value;
     6 import org.springframework.boot.context.properties.ConfigurationProperties;
     7 import org.springframework.boot.context.properties.EnableConfigurationProperties;
     8 import org.springframework.context.annotation.Bean;
     9 import org.springframework.context.annotation.Configuration;
    10 
    11 /**
    12  * @author Pann
    13  * @description config order server's RPC service method
    14  * @date 2019-08-08 14:51
    15  */
    16 @Configuration
    17 @EnableConfigurationProperties
    18 public class RPCServiceConfig {
    19     @Value("${service.addr}")
    20     private String addr;
    21 
    22     @Bean
    23     @ConfigurationProperties(prefix = "service")
    24     public RPCInterfacesWrapper serviceContainer() {
    25         return new RPCInterfacesWrapper();
    26     }
    27 
    28     @Bean
    29     public RPCRecvExecutor recvExecutor() {
    30         return new RPCRecvExecutor(addr);
    31     }
    32 
    33 }

      在client端,我们也仅仅只需要在com.imspa.config.RPCReferenceConfig中配置一下我们这个工程所需要调用的service 接口(注意所需要配置的内容哦):

     1 package com.imspa.config;
     2 
     3 import com.imspa.api.OrderRemoteService;
     4 import com.imspa.rpc.core.RPCSendExecutor;
     5 import org.springframework.context.annotation.Bean;
     6 import org.springframework.context.annotation.Configuration;
     7 
     8 /**
     9  * @author Pann
    10  * @Description config this server need's reference bean
    11  * @Date 2019-08-08 16:55
    12  */
    13 @Configuration
    14 public class RPCReferenceConfig {
    15     @Bean
    16     public RPCSendExecutor orderService() {
    17         return new RPCSendExecutor<OrderRemoteService>(OrderRemoteService.class,"localhost:8091");
    18     }
    19 
    20 }

      然后你就可以在代码里面正常的使用了

     1 package com.imspa.resource.web;
     2 
     3 import com.imspa.api.OrderRemoteService;
     4 import com.imspa.api.order.OrderDTO;
     5 import com.imspa.api.order.OrderVO;
     6 import org.springframework.beans.factory.annotation.Autowired;
     7 import org.springframework.web.bind.annotation.GetMapping;
     8 import org.springframework.web.bind.annotation.PathVariable;
     9 import org.springframework.web.bind.annotation.RequestMapping;
    10 import org.springframework.web.bind.annotation.RestController;
    11 
    12 import java.math.BigDecimal;
    13 import java.util.Arrays;
    14 import java.util.List;
    15 
    16 /**
    17  * @author Pann
    18  * @Description TODO
    19  * @Date 2019-08-08 16:51
    20  */
    21 @RestController
    22 @RequestMapping("/resource")
    23 public class ResourceController {
    24     @Autowired
    25     private OrderRemoteService orderRemoteService;
    26 
    27     @GetMapping("/get/{id}")
    28     public OrderVO get(@PathVariable("id")String id) {
    29         OrderDTO orderDTO = orderRemoteService.get(id);
    30         return new OrderVO().setOrderId(orderDTO.getOrderId()).setOrderPrice(orderDTO.getOrderPrice())
    31                 .setProductId(orderDTO.getProductId()).setProductName(orderDTO.getProductName())
    32                 .setStatus(orderDTO.getStatus()).setUserId(orderDTO.getUserId());
    33     }
    34 
    35     @GetMapping()
    36     public List<OrderVO> list() {
    37         return Arrays.asList(new OrderVO().setOrderId("1").setOrderPrice(new BigDecimal(2.3)).setProductName("西瓜"));
    38     }
    39 }

      以上是本基础架构的大概内容,还有很多其他的内容和后续更新请关注我的github,笔芯。

  • 相关阅读:
    1379. Find a Corresponding Node of a Binary Tree in a Clone of That Tree
    1389. Create Target Array in the Given Order
    1385. Find the Distance Value Between Two Arrays
    1382. Balance a Binary Search Tree
    PHP的 __DIR__ 作用【转】
    PHP:Allowed memory size of 134217728 bytes exhausted问题解决方法【转】
    PHP: POST Content-Length of xxx bytes exceeds the limit of 8388608 bytes【转】
    PhpStorm 如何快速查找文件【转】
    .gitkeep的作用【转】
    php copy 函数使用 【转】
  • 原文地址:https://www.cnblogs.com/HuaiyinMarquis/p/11382145.html
Copyright © 2011-2022 走看看