Java笔记-26、软件设计与前后端环境搭建、令牌技术、密码加密、swagger

软件开发流程

需求分析:软件需求规格说明书、产品原型

设计:UI设计、数据库设计(大型项目需要架构师)、接口设计

编码:项目代码、单元测试

测试:测试用例、测试报告

上线运维

角色分工

需求分析:

项目经理:对整个项目负责,任务分配、把控进度

产品经理:进行需求调研,输出需求调研文档、产品原型等

设计:

UI设计师:根据产品原型输出界面效果图

架构师:项目整体架构设计、技术选型等

编码:

开发工程师:代码实现

测试:

测试工程师:编写测试用例,输出测试报告

上线运维:

运维工程师:软件环境搭建、项目上线

软件环境

开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问

测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问

生产环境(production):即线上环境,正式提供对外服务的环境

技术选型

技术选型:展示项目中使用到的技术框架和中间件等。

用户层:node.js、VUE.js 、ElementUI、微信小程序、apache echarts

网关层:nginx

应用层:Spring Boot、Spring MVC、Spring Task、httpclient、Spring Cache、JWT、阿里云OSS、Swagger 、POI、WebSocket

数据层:MySQL、Redis、mybatis、pagehelper、spring data redis

工具:Git、maven、Junit、postman

开发环境搭建

前后端分离开发

前端环境搭建

介绍:Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,在各大型互联网公司都有非常广泛的使用。

静态资源自己处理,动态资源交给Tomcat处理。

反向代理:把动态资源的请求发给Tomcat。

官网: https://nginx.org/

Nginx的目录

conf:配置文件目录

html:静态资源文件目录

logs:日志文件目录

temp:临时文件目录

ng反向代理的好处:提高访问速度、进行负载均衡、保证后端服务安全

负载均衡:把大量的请求按照我们指定的方式均衡地分配给集群中的每台服务器。

nginx 负载均衡策略:

轮询:默认方式,一般企业都是用轮询的。

weight:权重方式,默认为1,权重越高,被分配的客户端请求就越多。

ip_hash:依据ip分配方式,这样每个访客可以固定访问一个后端服务。

least_conn:依据最少连接方式,把请求优先分配给连接数少的后端服务。

url_hash:依据url分配方式,这样相同的url会被分配到同一个后端服务。

fair:依据响应时间方式,响应时间短的服务将会被优先分配。

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main  ' remote_user [ request" '
    #                  ' body_bytes_sent " http_user_agent" "$http_x_forwarded_for"';
    #access_log  logs/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
map $http_upgrade $connection_upgrade{
	default upgrade;
	'' close;
}

upstream webservers{
  server 127.0.0.1:8080 weight=90 ;
  #server 127.0.0.1:8088 weight=10 ;
}
    server {
        listen       80;         #nginx设置使用的端口
        server_name  localhost;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            root   html/sky;     #设置nginx/html/sky为发布静态资源的目录
            index  index.html index.htm;
        }
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        # 反向代理,处理管理端发送的请求
        location /api/ {
proxy_pass   http://localhost:8080/admin/;
            #proxy_pass   http://webservers/admin/;
        }
	# 反向代理,处理用户端发送的请求
        location /user/ {
            proxy_pass   http://webservers/user/;
        }
	# WebSocket
	location /ws/ {
            proxy_pass   http://webservers/ws/;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_set_header Upgrade  connection_upgrade";
        }
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ .php fastcgi_script_name;
        #    include        fastcgi_params;
        #}
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /.ht {
        #    deny  all;
        #}
    }
    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;
    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;
    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;
    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
}

后端环境搭建

模块划分

sky-take-out 父工程,统一管理依赖版本,聚合其他子模块

sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等

sky-pojo 子模块,存放实体类、VO、DTO等

sky-server maven 子模块,后端服务。存放的是配置文件、配置类、拦截器、controller、service、mapper、启动类等。

sky-common:存放所有模块都要用的。

下有constant、context、enumeration、exception、json、properties、result、utils包。

constant常量(public static final)。

sky-pojo的存放内容分类

Entity:实体,通常和数据库中的表一一对应。

DTO:数据传输对象,通常用于程序中各层之间传递数据。主要用于封装前端传给后端的数据

VO:视图对象,为前端展示数据提供的对象。主要用于封装前端需要的数据。

POJO :普通Java对象,只有属性和对应的getter和setter。

注意:接收前端数据用DTO,返回给前端用VO,mapper中数据库返回用Entity。

VO的Builder注解

在VO类上加入@Builder注解,这个注解是lombok的。

该注解用于实现链式创建对象

@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;
}
    @PostMapping("/login")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO, HttpSession session) {
        log.info("员工登录:{}", employeeLoginDTO);
        Employee employee = employeeService.login(employeeLoginDTO);
	//将登录数据员工id存储到session会话中
        session.setAttribute("employee",employee.getId());
        // 链式创建对象 
        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(employee.getId().toString())
                .build();
        return Result.success(employeeLoginVO);
    }

会话跟踪方案

Cookie

Cookie ,客户端会话。

优点:HTTP协议中支持的技术。

缺点:移动端APP无法使用Cookie;不安全,用户可以自己禁用Cookie;Cookie不能跨域。

跨域区分三个维度:协议、IP/域名、端口。

Session

Session,服务器端会话。

优点:存储在服务端,安全。

缺点:服务器集群环境下无法直接使用Session;cookie的缺点。

用于单机无移动端。

jwt令牌(主流方案)

优点:

  1. 支持PC端、移动端
  2. 解決集群环境下的认证问题
  3. 减轻服务器端存储压力

缺点:需要自己实现

JSON Web Token

定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

jwt组成

  1. 第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg": "HS256", "type": "JWT"}
  2. 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id": "1", "username": "Tom"}
  3. 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload、指定秘钥(secret),通过指定签名算法计算而来。

Base64:是一种基于64个可打印字符(A-Z a-z 0-9 +/)来表示二进制数据的编码方式。

登录认证步骤

  1. 登录成功后,生成令牌
  2. 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
<dependency>
  <groupld>io.jsonwebtoken </groupld>
  <artifactld>jjwt</artifactld>
  <version>0.9.1</version>
</dependency>

生成令牌语法:

// 自定义有效载荷
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "tom");

String jwt = Jwts.builder()
   .setClaims(claims) // 设置载荷
   .signWith(SignatureAlgorithm.HS256, "subeipo") // 设置加密算法和密钥
   .setExpiration(new Date(System.currentTimeMillis() + 1000*1800*2))  // 设置有效时间为一小时
   .compact();

有效时间单位为毫秒,由于防止穷举破解出设置的密钥,通常有效时间为半小时到一小时

解析令牌语法:

Claims claims_id = Jwts.parser()
    .setSigningKey("subeipo")
    .parseClaimsJws("jwt字符串")
    .getBody();

Integer id = (Integer) claims.get("id");
String username = claims.get("username").toString();
System.out.println("id:" + id);
System.out.println("username:" + username);

JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。

如果JWT令牌解析校验时报错,则说明JWT令牌被篡改 或 失效了,令牌非法。

应用

首先设置jwt配置,在application.yml中加入:

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: subeipo
    # 设置jwt过期时间 2小时
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称,是和前端约定好的
    admin-token-name: token

前端传来的令牌是在请求头里的,键值对,键和前端约定。

其次,在sky-common中的properties包新建实体类JwtProperties映射application.yml的配置。

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;
}

然后,在定义JwtUtils工具类,编写相应的生成和解析方法。

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;
    }

}

然后,在请求登录的Controller中,生成令牌。

@PostMapping("/login")
 public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO, HttpSession session) {
    log.info("员工登录:{}", employeeLoginDTO);
    
    // 拿到对应的员工数据
    Employee employee = employeeService.login(employeeLoginDTO);

    // 生成令牌
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
    String token = JwtUtil.createJWT(
            jwtProperties.getAdminSecretKey(),
            jwtProperties.getAdminTtl(),
            claims
    );
    
    // 构建返回给前端的VO对象
    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
            .id(employee.getId())
            .userName(employee.getUsername())
            .name(employee.getName())
            .token(token)
            .build();

    return Result.success(employeeLoginVO);
}

在sky-server的interceptor包下新建类JwtTokenAdminInterceptor设置过滤器或拦截器,校验jwt。

/**
 * 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 {
        //1. 判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        // 2. 获取请求头中约定好的key的令牌数据
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        try {
            // 3. 解析令牌得到载荷数据
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long employeeId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("校验得到的员工id:{}", employeeId);
            // 3.1 没问题,就通过放行
            return true;
        } catch (Exception e) {
            // 3.2 有问题,响应401
            response.setStatus(401);
            return false;
        }
    }
}

最后,注册自定义拦截器:

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");
    }
}

密码加密

员工表里的密码是明文存储,安全性太低。

使用MD5加密方式对明文密码加密。

  1. 修改数据库中明文密码,改为MD5加密后的密文
  2. 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对
//密码比对
String md5 = DigestUtils.md5DigestAsHex(password.getBytes());
if (!md5.equals(employee.getPassword())) {
   //密码错误
   throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

Swagger

使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。

官网:https://swagger.io/

Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。

<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-spring-boot-starter</artifactId>
  <version>3.0.2</version>
</dependency>

如何使用

首先设置资源映射和接口文档生成

在sky-server的config包下的WebMvcConfiguration类中添加以下代码:

    /**
     * 通过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;
    }

    /**
     * 设置静态资源映射
     * @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/");
    }

使用方式:

接口文档访问路径为 http://ip:port/doc.html

swagger的注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

@Api:用在类上,例如Controller,表示对类的说明

@ApiModel:用在类上,例如entity、DTO、VO

@ApiModelProperty:用在属性上,描述属性信息

@ApiOperation:用在方法上,例如Controller的方法,说明方法的用途、作用

posted @ 2025-04-23 22:56  subeipo  阅读(51)  评论(0)    收藏  举报