基于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,笔芯。

原文地址:https://www.cnblogs.com/HuaiyinMarquis/p/11382145.html

时间: 2024-08-27 02:38:44

基于Spring Boot自建分布式基础应用的相关文章

微服务中基于Spring Boot的maven分布式项目框架的搭建

项目介绍 这里搭建的是基于 maven 的分布式工程,因为在一个项目中,多个微服务是属于同一个工程,只不过是提供不同的服务而已,再加上 IDEA 是默认一个窗口打开一个项目工程(这点和 eclipse 不同),如果项目大,不用 maven 聚合工程的话,那估计会打开十几个窗口--会崩溃--而且在架构上,也应该使用 maven 分布式工程来搭建微服务架构.这里手把手教大家在 IDEA 中搭建基于 maven 分布式的 Spring Cloud 微服务工程架构. maven分布式工程架构首先来看一下

基于Spring Boot框架企业级应用系统开发全面实战

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.通过这种方式,Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者.    教程由浅入深,一步一步学习Spring Boot,最后学到的不单单是基础! 使用Spring Boot 进行Web 开发.数据访问.安全控制.批处理.异步消息.系统集

基于Spring Boot和Spring Cloud实现微服务架构学习(四)

Spring Cloud介绍 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全局锁.决策竞选.分布式会话和集群状态管理等操作提供了一种简单的开发方式. Spring Cloud与Dubbo对比 提到Dubbo,我想顺便提下ESB,目前央视新华社也在用ESB来做任务编排,这里先比较下Dubbo和ESB: ESB(企业数据总线),一般采用集中式转发请求,适合大量异构系统集成,侧重任务

基于Spring Boot和Spring Cloud实现微服务架构学习

Spring Cloud介绍 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全局锁.决策竞选.分布式会话和集群状态管理等操作提供了一种简单的开发方式. Spring Cloud与Dubbo对比 提到Dubbo,我想顺便提下ESB,目前央视新华社也在用ESB来做任务编排,这里先比较下Dubbo和ESB: ESB(企业数据总线),一般采用集中式转发请求,适合大量异构系统集成,侧重任务

基于Spring Boot和Shiro的后台管理系统FEBS

FEBS是一个简单高效的后台权限管理系统.项目基础框架采用全新的Java Web开发框架 -- Spring Boot 2.0.3,消除了繁杂的XML配置,使得二次开发更为简单:数据访问层采用Mybatis,同时引入了通用Mapper和PageHelper插件,可快速高效的对单表进行增删改查操作,消除了大量传统XML配置SQL的代码:安全框架采用时下流行的Apache Shiro,可实现对按钮级别的权限控制:前端页面使用Bootstrap构建,主题风格为时下Google最新设计语言Materia

基于Spring Boot+Cloud构建微云架构

前言 首先,最想说的是,当你要学习一套最新的技术时,官网的英文文档是学习的最佳渠道.因为网上流传的多数资料是官网翻译而来,很多描述的重点也都偏向于作者自身碰到的问题,这样就很容易让你理解和操作出现偏差,最开始我就进入了这样误区.官网的技术导读真的描述的很详细,虽然对于我们看英文很费劲,但如果英文不是很差,请选择沉下心去读,你一定能收获好多. 我的学习是先从Spring boot开始的,然后接触到微服务架构,当然,这一切最大的启迪还是感谢我的一个老师,是他给我指明了新的道路,让我眼前一亮,再次感谢

基于Spring Boot,使用JPA动态调用Sql查询数据

在<基于Spring Boot,使用JPA操作Sql Server数据库完成CRUD>,<基于Spring Boot,使用JPA调用Sql Server数据库的存储过程并返回记录集合>完成了CRUD,调用存储过程查询数据. 很多复杂的情况下,会存在要直接执行SQL来获取数据. 通过“EntityManager”创建NativeQuery方法来执行动态SQL. 1.查询结果集映射 在包“com.kxh.example.demo.domain”下的“Contact”实体上编写命名的结果

Https系列之三:让服务器同时支持http、https,基于spring boot

Https系列会在下面几篇文章中分别作介绍: 一:https的简单介绍及SSL证书的生成二:https的SSL证书在服务器端的部署,基于tomcat,spring boot三:让服务器同时支持http.https,基于spring boot四:https的SSL证书在Android端基于okhttp,Retrofit的使用 所有文章会优先在:微信公众号"颜家大少"中发布转载请标明出处 前面已介绍了:"https在服务器端的部署,基于tomcat,spring boot&quo

基于 spring boot 和 spring mvc 的快速开发框架 summer-boot

summer-boot 详细介绍此项目目的在于提供一个简化.简洁.迅速的开发架构. 它是基于spring boot和spring mvc高度封装的快速开发框架,数据库操作工具summerDao是基于jdbcTemplate高度封装简化.拥有超级简单实用的ORM功能.和ibatis一样强大但更简单.无需映射配置的dao工具,视图层采用的是Rythm(最简洁的java模板引擎.可以用它来做web项目.微服务.socket服务,且同一套代码同时兼容这三种方式. 它的优点如下:基本建立在spring一套