苍穹外卖技术点学习
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/)。 它的主要作用是:
- 使得前后端分离开发更加方便,有利于团队协作
- 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
- 功能测试
使用方式
-
导入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来实例化对象, 通过链式调用,创建对象的代码更加直观和易读。
优点:
- 可读性强:通过链式调用,创建对象的代码更加直观和易读。
- 灵活性高:允许不必在对象创建时提供所有参数,只需设置需要的字段。
- 避免构造器重载:避免了因为参数过多或组合不同而导致的构造器重载问题。
- 不可变性:在构建完成后,生成的对象通常是不可变的,有助于编写线程安全的代码。
适用场景:
- 对象属性较多:如配置类、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();