目前刚入职了一家公司, 要求替换当前系统 (单体应用) 以满足每日十万单量和一定系统用户负载以及保证开发质量和效率. 由我来设计一套基础架构和建设基础开发测试运维环境, GitHub 地址.
出于本公司开发现状及成本考虑, 我摒弃了市面上流行的 Spring Cloud 以及 Dubbo 分布式基础架构, 舍弃了集群的设计, 以 Spring Boot 和 Netty 为基础自建了一套 RPC 分布式应用架构. 可能这里各位会有疑问, 为什么要舍弃应用的高可用呢? 其实这也是跟公司的产品发展有关的, 避免过度设计是非常有必要的. 下面是整个系统的架构设计图.
这里简单介绍一下, 这里 ELK 或许并非最好的选择, 可以另外采用 zabbix 或者 prometheus, 我只是考虑了后续可能的扩展. 数据库采用了两种存储引擎, 便是为了因对上面所说的每天十万单的大数据量, 可以采用定时脚本的形式完成数据的转移.
权限的设计主要是基于 JWT+Filter+Redis 来做的. Common 工程中的 com.imspa.Web.auth.Permissions 定义了所有需要的 permissions:
- package com.imspa.Web.auth;
- /**
- * @author Pann
- * @description TODO
- * @date 2019-08-12 15:09
- */
- public enum Permissions {
- ALL("/all", "所有权限"),
- ROLE_GET("/role/get/**", "权限获取"),
- USER("/user", "用户列表"),
- USER_GET("/user/get", "用户查询"),
- RESOURCE("/resource", "资源获取"),
- ORDER_GET("/order/get/**","订单查询");
- private String url;
- private String desc;
- Permissions(String url, String desc) {
- this.url = url;
- this.desc = desc;
- }
- public String getUrl() {
- return this.url;
- }
- public String getDesc() {
- return this.desc;
- }
- }
如果你的没有为你的接口在这里定义权限, 那么系统是不会对该接口进行权限的校验的. 在数据库中 User 与 Role 的设计如下:
- CREATE TABLE IF NOT EXISTS `t_user` (
- `id` VARCHAR(36) NOT NULL,
- `name` VARCHAR(20) NOT NULL UNIQUE,
- `password_hash` VARCHAR(255) NOT NULL,
- `role_id` VARCHAR(36) NOT NULL,
- `role_name` VARCHAR(20) NOT NULL,
- `last_login_time` TIMESTAMP(6) NULL,
- `last_login_client_ip` VARCHAR(15) NULL,
- `created_time` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
- `created_by` VARCHAR(36) NOT NULL,
- `updated_time` TIMESTAMP(6) NULL,
- `updated_by` VARCHAR(36) NULL,
- PRIMARY KEY (`id`)
- );
- CREATE TABLE IF NOT EXISTS `t_role` (
- `id` VARCHAR(36) NOT NULL,
- `role_name` VARCHAR(20) NOT NULL UNIQUE,
- `description` VARCHAR(90) NULL,
- `permissions` TEXT NOT NULL, #其数据格式类似于 "/role/get,/user" 或者 "/all"
- `created_time` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
- `created_by` VARCHAR(36) NOT NULL,
- `updated_time` TIMESTAMP(6) NULL,
- `updated_by` VARCHAR(36) NULL,
- PRIMARY KEY (`id`)
- );
需要注意的是 "/all" 代表了所有权限, 表示 root 权限. 我们通过 postman 调用登陆接口可以获取相应的 token:
这个 token 是半个小时失效的, 如果你需要更长一些的话, 可以通过 com.imspa.Web.auth.TokenAuthenticationService 进行修改:
- package com.imspa.Web.auth;
- import com.imspa.Web.util.WebConstant;
- import io.jsonwebtoken.Jwts;
- import io.jsonwebtoken.SignatureAlgorithm;
- import java.util.Date;
- import java.util.Map;
- /**
- * @author Pann
- * @description TODO
- * @date 2019-08-14 23:24
- */
- public class TokenAuthenticationService {
- static final long EXPIRATIONTIME = 30 * 60 * 1000; //TODO
- public static String getAuthenticationToken(Map<String, Object> claims) {
- return "Bearer" + Jwts.builder()
- .setClaims(claims)
- .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
- .signWith(SignatureAlgorithm.HS512, WebConstant.WEB_SECRET)
- .compact();
- }
- }
Refresh Token 目前还没有实现, 后续我会更新, 请关注我的. 如果你跟踪登陆逻辑代码, 你可以看到我把 role 和 user 都缓存到了 Redis:
- public User login(String userName, String password) {
- UserExample example = new UserExample();
- example.createCriteria().andNameEqualTo(userName);
- User user = userMapper.selectByExample(example).get(0);
- if (null == user)
- throw new UnauthorizedException("user name not exist");
- if (!StringUtils.equals(password, user.getPasswordHash()))
- throw new UnauthorizedException("user name or password wrong");
- roleService.get(user.getRoleId()); //for role cache
- hashOperations.putAll(RedisConstant.USER_SESSION_INFO_ + user.getName(), hashMapper.toHash(user));
- hashOperations.getOperations().expire(RedisConstant.USER_SESSION_INFO_ + user.getName(), 30, TimeUnit.MINUTES);
- return user;
- }
在 Filter 中, 你可以看到过滤器的一系列逻辑, 注意返回 http 状态码 401,403 和 404 的区别:
- package com.imspa.Web.auth;
- import com.imspa.Web.Exception.ForbiddenException;
- import com.imspa.Web.Exception.UnauthorizedException;
- import com.imspa.Web.pojo.Role;
- import com.imspa.Web.pojo.User;
- import com.imspa.Web.util.RedisConstant;
- import com.imspa.Web.util.WebConstant;
- import io.jsonwebtoken.Claims;
- import io.jsonwebtoken.Jwts;
- import org.apache.commons.lang3.StringUtils;
- import org.apache.logging.log4j.LogManager;
- import org.apache.logging.log4j.Logger;
- import org.springframework.data.Redis.core.HashOperations;
- import org.springframework.data.Redis.hash.HashMapper;
- import org.springframework.util.AntPathMatcher;
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletOutputStream;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
- import java.util.Date;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.Optional;
- import java.util.concurrent.TimeUnit;
- /**
- * @author Pann
- * @description TODO
- * @date 2019-08-16 14:39
- */
- public class SecurityFilter implements Filter {
- private static final Logger logger = LogManager.getLogger(SecurityFilter.class);
- private AntPathMatcher matcher = new AntPathMatcher();
- private HashOperations<String, byte[], byte[]> hashOperations;
- private HashMapper<Object, byte[], byte[]> hashMapper;
- public SecurityFilter(HashOperations<String, byte[], byte[]> hashOperations, HashMapper<Object, byte[], byte[]> hashMapper) {
- this.hashOperations = hashOperations;
- this.hashMapper = hashMapper;
- }
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- }
- @Override
- public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
- HttpServletRequest request = (HttpServletRequest) servletRequest;
- HttpServletResponse response = (HttpServletResponse) servletResponse;
- Optional<String> optional = PermissionUtil.getAllPermissionUrlItem().stream()
- .filter(permissionItem -> matcher.match(permissionItem, request.getRequestURI())).findFirst();
- if (!optional.isPresent()) { //TODO some API not config permission will direct do
- chain.doFilter(servletRequest, servletResponse);
- return;
- }
- try {
- validateAuthentication(request, optional.get());
- flushSessionAndToken(((User) request.getAttribute("userInfo")), response);
- chain.doFilter(servletRequest, servletResponse);
- } catch (ForbiddenException e) {
- logger.debug("occur forbidden exception:{}", e.getMessage());
- response.setStatus(403);
- ServletOutputStream output = response.getOutputStream();
- output.print(e.getMessage());
- output.flush();
- } catch (UnauthorizedException e) {
- logger.debug("occur unauthorized exception:{}", e.getMessage());
- response.setStatus(401);
- ServletOutputStream output = response.getOutputStream();
- output.print(e.getMessage());
- output.flush();
- }
- }
- @Override
- public void destroy() {
- }
- private void validateAuthentication(HttpServletRequest request, String permission) {
- String authHeader = request.getHeader("Authorization");
- if (StringUtils.isEmpty(authHeader))
- throw new UnauthorizedException("no auth header");
- Claims claims;
- try {
- claims = Jwts.parser().setSigningKey(WebConstant.WEB_SECRET)
- .parseClaimsJws(authHeader.replace("Bearer", ""))
- .getBody();
- } catch (Exception e) {
- throw new UnauthorizedException(e.getMessage());
- }
- String userName = (String) claims.get("user");
- String roleId = (String) claims.get("role");
- if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(roleId))
- throw new UnauthorizedException("token error,user:" + userName);
- if (new Date().getTime()> claims.getExpiration().getTime())
- throw new UnauthorizedException("token expired,user:" + userName);
- User user = (User) hashMapper.fromHash(hashOperations.entries(RedisConstant.USER_SESSION_INFO_ + userName));
- if (user == null)
- throw new UnauthorizedException("session expired,user:" + userName);
- if (validateRolePermission(permission, user))
- request.setAttribute("userInfo", user);
- }
- private Boolean validateRolePermission(String permission, User user) {
- Role role = (Role) hashMapper.fromHash(hashOperations.entries(RedisConstant.ROLE_PERMISSION_MAPPING_ + user.getRoleId()));
- if (role.getPermissions().contains(Permissions.ALL.getUrl()))
- return Boolean.TRUE;
- if (role.getPermissions().contains(permission))
- return Boolean.TRUE;
- throw new ForbiddenException("do not have permission for this request");
- }
- private void flushSessionAndToken(User user, HttpServletResponse response) {
- hashOperations.getOperations().expire(RedisConstant.USER_SESSION_INFO_ + user.getName(), 30, TimeUnit.MINUTES);
- Map<String, Object> claimsMap = new HashMap<>();
- claimsMap.put("user", user.getName());
- claimsMap.put("role", user.getRoleId());
- response.setHeader("Authorization",TokenAuthenticationService.getAuthenticationToken(claimsMap));
- }
- }
下面是 RPC 的内容, 我是用 Netty 来实现整个 RPC 的调用的, 其中包含了心跳检测, 自动重连的过程, 基于 Spring Boot 的实现, 配置和使用都还是很方便的.
我们先看一下 service 端的写法, 我们需要先定义好对外服务的接口, 这里我们在 application.YAML 中定义:
- service:
- addr: localhost:8091
- interfaces:
- - 'com.imspa.api.OrderRemoteService'
其中 service.addr 是对外发布的地址, service.interfaces 是对外发布的接口的定义. 然后便不需要你再定义其他内容了, 是不是很方便? 其实现你可以根据它的配置类 com.imspa.config.RPCServiceConfig 来看:
- package com.imspa.config;
- import com.imspa.rpc.core.RPCRecvExecutor;
- import com.imspa.rpc.model.RPCInterfacesWrapper;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import org.springframework.boot.context.properties.EnableConfigurationProperties;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- /**
- * @author Pann
- * @description config order server's RPC service method
- * @date 2019-08-08 14:51
- */
- @Configuration
- @EnableConfigurationProperties
- public class RPCServiceConfig {
- @Value("${service.addr}")
- private String addr;
- @Bean
- @ConfigurationProperties(prefix = "service")
- public RPCInterfacesWrapper serviceContainer() {
- return new RPCInterfacesWrapper();
- }
- @Bean
- public RPCRecvExecutor recvExecutor() {
- return new RPCRecvExecutor(addr);
- }
- }
在 client 端, 我们也仅仅只需要在 com.imspa.config.RPCReferenceConfig 中配置一下我们这个工程所需要调用的 service 接口(注意所需要配置的内容哦):
- package com.imspa.config;
- import com.imspa.API.OrderRemoteService;
- import com.imspa.rpc.core.RPCSendExecutor;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- /**
- * @author Pann
- * @Description config this server need's reference bean
- * @Date 2019-08-08 16:55
- */
- @Configuration
- public class RPCReferenceConfig {
- @Bean
- public RPCSendExecutor orderService() {
- return new RPCSendExecutor<OrderRemoteService>(OrderRemoteService.class,"localhost:8091");
- }
- }
然后你就可以在代码里面正常的使用了
- package com.imspa.resource.Web;
- import com.imspa.API.OrderRemoteService;
- import com.imspa.API.order.OrderDTO;
- import com.imspa.API.order.OrderVO;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.Web.bind.annotation.GetMapping;
- import org.springframework.Web.bind.annotation.PathVariable;
- import org.springframework.Web.bind.annotation.RequestMapping;
- import org.springframework.Web.bind.annotation.RestController;
- import java.math.BigDecimal;
- import java.util.Arrays;
- import java.util.List;
- /**
- * @author Pann
- * @Description TODO
- * @Date 2019-08-08 16:51
- */
- @RestController
- @RequestMapping("/resource")
- public class ResourceController {
- @Autowired
- private OrderRemoteService orderRemoteService;
- @GetMapping("/get/{id}")
- public OrderVO get(@PathVariable("id")String id) {
- OrderDTO orderDTO = orderRemoteService.get(id);
- return new OrderVO().setOrderId(orderDTO.getOrderId()).setOrderPrice(orderDTO.getOrderPrice())
- .setProductId(orderDTO.getProductId()).setProductName(orderDTO.getProductName())
- .setStatus(orderDTO.getStatus()).setUserId(orderDTO.getUserId());
- }
- @GetMapping()
- public List<OrderVO> list() {
- return Arrays.asList(new OrderVO().setOrderId("1").setOrderPrice(new BigDecimal(2.3)).setProductName("西瓜"));
- }
- }
以上是本基础架构的大概内容, 还有很多其他的内容和后续更新请关注我的, 笔芯.
来源: https://www.cnblogs.com/HuaiyinMarquis/p/11382145.html