苍穹外卖技术点学习

1. Nignx实现反向代理和负载均衡

不使用Nignx的场景下,前端请求的接口直接对接后端的端口存在不安全,耦合度高等问题。使用Nignx进行反向代理将前端发送的请求动态转发到后端服务器

请求的接口地址:

http://localhost:8080/admin/employee/login - > http://localhost/api/employee/login

nignx反向代理的配置方式(nginx/conf/)

server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://localhost:8080/admin/; #反向代理
    }
}

nignx除了能进行反向代理,还能进行负载均衡,配置方式如下

# 轮询
upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
# weight 权重方式,默认为1,权重越高,被分配的客户端请求就越多
upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
# ip_hash:依据ip分配方式,这样每个访客可以固定访问一个后端服务
upstream webservers{
    ip_hash;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
# least_conn:依据最少连接方式,把请求优先分配给连接数少的后端服务
upstream webservers{
    least_conn;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
# url_hash:依据url分配方式,这样相同的url会被分配到同一个后端服务
upstream webservers{
    hash &request_uri;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
# fair: 依据响应时间方式,响应时间短的服务将会被优先分配
upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
    fair;
}
server{
    listen 80;
    server_name localhost;
    
    location /api/{
        proxy_pass http://webservers/admin;#负载均衡
    }
}

2. MD5密码加密

在开发过程中,一般对密码进行加密存储(这里需要考虑一个问题是否要加密传输, 这种一般前端处理). SpringBoot 里自带MD5的加密.

 password = DigestUtils.md5DigestAsHex(password.getBytes());

3. JWT鉴权

传统的有状态会话的状态信息保存到内存中, 当用户进行身份验证时,服务器会检查用户的凭据,并在成功验证后创建一个会话,并将会话ID存储在服务器端。每当用户进行请求时,服务器都需要查找并验证会话ID,并根据会话ID来检索与该用户相关的会话信息。

jwt的优点:

  • JWT中包含了用户的一些身份信息(称为声明),如用户ID、角色、权限等。这些信息是经过加密签名的,因此是可信的。
  • 当用户进行身份验证成功后,服务器会生成一个JWT,将用户的身份信息加密签名后发送给客户端。
  • 客户端在后续的请求中携带JWT,服务器接收到JWT后,解析其中的信息并进行验证,从而获取到用户的身份信息。
  • 由于JWT是无状态的,服务器无需在后端维护会话信息,因此实现了无状态的用户会话管理。

实现方式

  • JWT工具类
package com.sky.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

  • JWT拦截器

    import com.sky.constant.JwtClaimsConstant;
    import com.sky.context.BaseContext;
    import com.sky.properties.JwtProperties;
    import com.sky.utils.JwtUtil;
    import io.jsonwebtoken.Claims;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * jwt令牌校验的拦截器
     */
    @Component
    @Slf4j
    public class JwtTokenAdminInterceptor implements HandlerInterceptor {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        /**
         * 校验jwt
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断当前拦截到的是Controller的方法还是其他资源
            if (!(handler instanceof HandlerMethod)) {
                //当前拦截到的不是动态方法,直接放行
                return true;
            }
    
            //1、从请求头中获取令牌
            String token = request.getHeader(jwtProperties.getAdminTokenName());
    
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
                Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
                log.info("当前员工id:", empId);
    
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
        }
    }
     
     
    
  • JWT配置

    application.yaml

    sky:
      jwt:
        # 设置jwt签名加密时使用的秘钥
        admin-secret-key: itcast
        # 设置jwt过期时间
        admin-ttl: 7200000
        # 设置前端传递过来的令牌名称
        admin-token-name: token
    
    

    JwtProperties.Java

    @Component
    @ConfigurationProperties(prefix = "sky.jwt")
    @Data
    public class JwtProperties {
    
        /**
         * 管理端员工生成jwt令牌相关配置
         */
        private String adminSecretKey;
        private long adminTtl;
        private String adminTokenName;
    
        /**
         * 用户端微信用户生成jwt令牌相关配置
         */
        private String userSecretKey;
        private long userTtl;
        private String userTokenName;
    
    }
    
    
  • 生成示例

    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
    String token = JwtUtil.createJWT(
                    jwtProperties.getAdminSecretKey(),
                    jwtProperties.getAdminTtl(),
                    claims);
    

4. 基于Swagger的Knife4j方案

在编写接口及其文档是一件非常麻烦的事情, Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是:

  1. 使得前后端分离开发更加方便,有利于团队协作
  2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
  3. 功能测试

使用方式

  • 导入knife4j的maven坐标

    <dependency>
       <groupId>com.github.xiaoymin</groupId>
       <artifactId>knife4j-spring-boot-starter</artifactId>
    </dependency>
    
  • 在WebMvcConguration中配置knife4j

    /**
         * 通过knife4j生成接口文档
         * @return
    */
        @Bean
        public Docket docket() {
            ApiInfo apiInfo = new ApiInfoBuilder()
                    .title("苍穹外卖项目接口文档")
                    .version("2.0")
                    .description("苍穹外卖项目接口文档")
                    .build();
            Docket docket = new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo)
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                    .paths(PathSelectors.any())
                    .build();
            return docket;
        }
    
  • 在WebMvcConguration设置静态资源映射

/**
     * 设置静态资源映射
     * @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

常用的注解:

注解说明
@Api用在类上,例如Controller,表示对类的说明
@ApiModel用在类上,例如entity、DTO、VO
@ApiModelProperty用在属性上,描述属性信息
@ApiOperation用在方法上,例如Controller的方法,说明方法的用途、作用

5. 通过ThreadLocal设置线程唯一变量

在写项目的过程中通常会用到全局变量,在单个代码文件中通常是静态内部类,那么在整个项目中,每次的线程怎么设置专用的变量呢,比如用来记录当前请求的用户ID.

  • 构造ThreadLocal常量

    public class BaseContext {
    
        public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    
        public static void setCurrentId(Long id) {
            threadLocal.set(id);
        }
    
        public static Long getCurrentId() {
            return threadLocal.get();
        }
    
        public static void removeCurrentId() {
            threadLocal.remove();
        }
    
    }
    
    
  • 设置变量

    BaseContext.setCurrentId(empId);
    
  • 访问变量

    BaseContext.getCurrentId()
    

6. 通过Builder设计模式示例化对象

在SpringBoot等项目中, 往往采用Builder来实例化对象, 通过链式调用,创建对象的代码更加直观和易读。

优点:

  1. 可读性强:通过链式调用,创建对象的代码更加直观和易读。
  2. 灵活性高:允许不必在对象创建时提供所有参数,只需设置需要的字段。
  3. 避免构造器重载:避免了因为参数过多或组合不同而导致的构造器重载问题。
  4. 不可变性:在构建完成后,生成的对象通常是不可变的,有助于编写线程安全的代码。

适用场景:

  • 对象属性较多:如配置类、VO(Value Object)类等。
  • 对象创建过程复杂:需要经过多个步骤或存在多个可选参数。
  • 需要创建不可变对象:希望对象在创建后不能被修改。

使用方式

  • 加载lombok

    <dependency>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>${lombok}</version>
                </dependency>
    
  • 在实体类上面加上@Builder , 举例

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @ApiModel(description = "员工登录返回的数据格式")
    public class EmployeeLoginVO implements Serializable {
    
        @ApiModelProperty("主键值")
        private Long id;
    
        @ApiModelProperty("用户名")
        private String userName;
    
        @ApiModelProperty("姓名")
        private String name;
    
        @ApiModelProperty("jwt令牌")
        private String token;
    
    }
    
    
  • 通过Builder进行实例化

    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                    .id(employee.getId())
                    .userName(employee.getUsername())
                    .name(employee.getName())
                    .token(token)
                    .build();