Spring boot строит фоновую базовую сервисную архитектуру с нуля, вы не пожалеете

Spring Boot

В настоящее время большинство проектов отделены от фронтенда и бекенда. В процессе разработки сервисов фонового интерфейса нам часто нужно сначала создать базовый сервис, такой как функции входа и регистрации, автоматическая проверка безопасности токена для всех интерфейсов и т. д., что может предотвратить возникновение проблем с безопасностью. И таким образом, последующие коллеги могут сосредоточиться только на разработке бизнес-кода, и им не нужно заботиться о реализации инфраструктурных сервисов.

На этот раз я собираюсь создать простую фоновую службу, используяspring boot + mysql + jjwt + mybatis.

1. Соберите загрузочный проект Spring

Во-первых, мы используем функцию проекта инициализации, которая поставляется с IDEA, чтобы создать загрузочный проект Spring, как показано на рисунке:

или сгенерировано онлайн,нажмите, чтобы войти

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>
    <groupId>com.zz</groupId>
    <artifactId>rest-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rest-api</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
            <scope>compile</scope>
        </dependency>

        <!-- Use MySQL Connector-J -->

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>


        <!-- image to base64 -->

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

        <!-- jjwt支持 -->

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <!--commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.terran4j</groupId>
            <artifactId>terran4j-commons-api2doc</artifactId>
            <version>1.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>com.itextpdf.tool</groupId>
            <artifactId>xmlworker</artifactId>
            <version>5.5.10</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.15</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>3.15</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

maven изменен на внутренний облачный образ Alibaba, который работает быстрее

settings.xml:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
    https://maven.apache.org/xsd/settings-1.0.0.xsd">
    
    <mirrors>
        <mirror>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
            <mirrorOf>central</mirrorOf>
        </mirror>
    </mirrors>
</settings>


Рекомендуемые плагины Mybatis:

application.yml

# mysql
spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
  profiles:
    active: dev
  # 静态资源配置
  mvc:
    static-path-pattern: /**
  resources:
    static-locations: file:/Users/wz/projects/blog/uploadFile/,classpath:/static/,classpath:/resources/,classpath:/file/,classpath:/templates/

mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.zz.entity
#自定义
my:
  tokenURL: "55555"
  authURL: "88888"

application-dev.yml

# mysql
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: ******
server:
  port: 8080

Примерная структура каталогов выглядит следующим образом

Детали конструкции повторяться не будут;

2. Прочтите пользовательский файл конфигурации

com.zz.config.MyConfiguration

package com.zz.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "my")
public class MyConfiguration {

    private String tokenURL;

    private String authURL;

    public String getAuthURL() {
        return this.authURL;
    }

    public void setAuthURL(String authURL) {
        this.authURL = authURL;
    }

    public String getTokenURL() {
        return this.tokenURL;
    }

    public void setTokenURL(String tokenURL) {
        this.tokenURL = tokenURL;
    }
}

3. Конфигурация веб-сервиса

com.zz.config.MyConfiguration

package com.zz.config;

import com.zz.common.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求,通过判断是否有 @passToken 注解 决定是否需要跳过登录
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
    
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
    
}

4. Настройте и верните ответ унифицированного класса сущностей

com.zz.model.Response

package com.zz.model;


public class Response {

    private int code;
    private String msg;
    private Object data;
    
    public Object getData() {
        return data;
    }
    
    public void setData(Object data) {
        this.data = data;
    }
    
    public int getCode() {
        return code;
    }
    
    public void setCode(int code) {
        this.code = code;
    }
    
    public String getMsg() {
        return msg;
    }
    
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

5. Использует класс открытого метода

com.zz.utils.HttpUtils

Получить запрос, ответ, сеанс

package com.zz.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 获取 Request 和 Response
 */
public class HttpUtils {
    
    // 获取 request
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) return null;
        return requestAttributes.getRequest();
    }
    
    // 获取 response
    public static HttpServletResponse getResponse() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) return null;
        return requestAttributes.getResponse();
    }
    
    // 获取 session
    public static HttpSession getSession(){
        HttpServletRequest request = getRequest();
        if(request == null) return null;
        return request.getSession();
    }
}

com.zz.utils.JWTUtils

JWT генерирует токен, проверяет токен

package com.zz.utils;

import com.zz.entity.User;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JWTUtils {
    
    // 生成签名的时候使用的秘钥secret
    private static final String SECRETKEY = "KJHUhjjJYgYUllVbXhKDHXhkSyHjlNiVkYzWTBac1Yxkjhuad";
    
    // expirationDate 生成jwt的有效期,单位秒
    private static long expirationDate = 2 * 60 * 60;
    
    
    /**
     * 由字符串生成加密key
     *
     * @return SecretKey
     */
    private static SecretKey generalKey(String stringKey) {
        byte[] encodedKey = Base64.decodeBase64(stringKey);
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }
    
    /**
     * 创建 jwt
     *
     * @param user 登录成功后的用户信息
     * @return jwt token
     */
    public static String createToken(User user) {
        
        // 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        
        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        
        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getUserId());
        claims.put("userName", user.getUserName());
        claims.put("password", user.getPassword());
        
        // 生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
        SecretKey key = generalKey(SECRETKEY + user.getPassword());
        
        // 生成签发人
        // json形式字符串或字符串,增加用户非敏感信息存储,如用户id或用户账号,与token解析后进行对比,防止乱用
        HashMap<String, Object> storeInfo = new HashMap<String, Object>();
        storeInfo.put("userId", user.getUserId());
        storeInfo.put("userName", user.getUserName());
        String subject = storeInfo.toString();
        
        // 下面就是在为payload添加各种标准声明和私有声明了
        // 这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 唯一随机UUID
                // 设置JWT ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
                .setId(UUID.randomUUID().toString())
                // jwt的签发时间
                .setIssuedAt(now)
                // 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志
                .setSubject(subject)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, key);
        
        if (expirationDate >= 0) {
            long expMillis = nowMillis + expirationDate * 1000;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }
    
    /**
     * 解密token,获取声明的实体
     *
     * @param token 加密后的token
     * @return claims
     */
    public static Claims parseToken(String token, User user) {
        // 签名秘钥,和生成的签名的秘钥要保持一模一样
        SecretKey key = generalKey(SECRETKEY + user.getPassword());
        
        // 获取私有声明
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(key)
                // 设置需要解析的token
                .parseClaimsJws(token).getBody();
        
        return claims;
    }
    
    
    /**
     * 校验token
     *
     * @param token 加密后的token
     * @param user  用户信息
     * @return true|false
     */
    public static Boolean verify(String token, User user) {
        
        // 获取私有声明的实体
        Claims claims = parseToken(token, user);
        
        return claims.get("password").equals(user.getPassword());
    }      
}

6. Запрос класса сущности запроса

Все сервисные запросы используют унифицированный соответствующий класс сущностей.

Например:

com.zz.query.UserQuery

Сущность пользовательского запроса

package com.zz.query;

public class UserQuery {
    
    private String userName;
    
    private String password;
    
    private long userId;
    
    private boolean showPassword;
    
    public boolean isShowPassword() {
        return showPassword;
    }
    
    public void setShowPassword(boolean showPassword) {
        this.showPassword = showPassword;
    }
    
    public long getUserId() {
        return userId;
    }
    
    public void setUserId(long userId) {
        this.userId = userId;
    }
    
    public String getUserName() {
        return userName;
    }
    
    public void setUserName(String userName) {
        this.userName = userName;
    }
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
}

7. Вернуть класс сущности после запроса

Все возвращаемые сервисные запросы используют унифицированный соответствующий класс сущностей.

Например:

com.zz.entity.User

Объект возврата пользовательских данных

package com.zz.entity;

public class User {
    
    private long userId;
    
    private String userName;
    
    private String token;
    
    private String password;
    
    public String getPassword() {
        return password;
    }
    
    public void setPassword(String password) {
        this.password = password;
    }
    
    public String getToken() {
        return token;
    }
    
    public void setToken(String token) {
        this.token = token;
    }
    
    public long getUserId() {
        return userId;
    }
    
    public void setUserId(long userId) {
        this.userId = userId;
    }
    
    public String getUserName() {
        return userName;
    }
    
    public void setUserName(String userName) {
        this.userName = userName;
    }
    
}

8. В интерфейсе реализована трехуровневая архитектура

Мы используем трехуровневую архитектуру: контроллер —> сервис —> картограф;

Если мы хотим написать интерфейс класса User, сначала объявите уровень управления маршрутизацией UserController, затем вызовите в нем метод класса реализации UserService, а затем вызовите уровень персистентности сопоставления в CRUD (добавление, поиск, удаление и модификация mysql).

9. Начните создавать функцию зарегистрированного пользователя

Сначала приостанавливается основное строительство, и начинается вычитание основных дел;

О подключении mysql говорить особо нечего;

Давайте начнем путешествие реализации;

com.zz.newController.UserController

Регистрация пользователя

package com.zz.newController;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;


/**
 * 登录
 * author: wz
 */
@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /*
     * @param userName
     * @param password
     * @return response
     */
    @PostMapping("/add")
    @PassToken
    public Response addUser(@RequestParam String userName, @RequestParam String password, Response response) {
        UserQuery query = new UserQuery();
        User userData = null;
        
        query.setUserName(userName);
        query.setPassword(password);
        
        int result;
        String message = "";
        
        // 判断用户是否已经存在
        UserQuery findUserQuery = new UserQuery();
        findUserQuery.setUserName(userName);
        User existUser = this.userService.findUserByName(findUserQuery);
        if (existUser == null) {
            
            // 插入用户
            try {
                result = this.userService.addUser(query);
                message = "success";
            } catch (Exception e) {
                result = 0;
                message = "error";
                e.printStackTrace();
            }
            
            // 插入用户成功后返回用户信息
            if (result == 1) {
                userData = this.userService.findUserByName(findUserQuery);
                
                // 生成token
                String token = null;
                
                // 当前用户
                User currentUser = new User();
                if (userData != null) {
                    currentUser.setUserId(userData.getUserId());
                    currentUser.setUserName(userData.getUserName());
                    currentUser.setPassword(password);
                    token = JWTUtils.createToken(currentUser);
                }
                
                if (token != null) {
                    userData.setToken(token);
                    
                    // 获取token用户信息
                    // Claims userDataFromToken = JWTUtils.parseToken(token, currentUser);
                }
            }
            
        } else {
            message = "用户已经存在";
        }
        
        response.setData(userData);
        response.setMsg(message);
        return response;
    }
    
}

com.zz.service.UserService

Пользовательский интерфейс интерфейса

package com.zz.service;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;
import java.util.Map;

public interface UserService {
    
    // 添加用户
    int addUser(UserQuery query);
    
    
}

com.zz.service.impl.UserServiceImpl

Класс реализации пользовательского интерфейса

package com.zz.service.impl;

import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public int addUser(UserQuery query){
        return this.userMapper.insert(query);
    }
    
}

com.zz.mapper.UserMapper

mapper

package com.zz.mapper;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;

public interface UserMapper {
    
    int insert(UserQuery query);
    
}

resources/mapper/UserMapper.xml

Имена до и после должны соответствовать

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">

    <resultMap id="BaseResult" type="com.zz.entity.User">
        <id column="user_id" property="userId"></id>
        <id column="user_name" property="userName"></id>
    </resultMap>

    <sql id="base">
        user_id,
        user_name
        <if test="showPassword">
            , password
        </if>
    </sql>

    <sql id="base_condition">
        <where>
            <if test="userName!=null and userName!=''">
                user_name=#{userName}
            </if>
            <if test="password!=null and password!=''">
                and password=#{password}
            </if>
        </where>

    </sql>

    <insert id="insert">
        INSERT INTO user(
        user_name,
        password
        ) VALUES (
        #{userName},
        #{password}
        )
    </insert>


</mapper>

На этом весь процесс написания интерфейса завершен, то есть весь процесс написания интерфейса под текущую архитектуру.

10. Создайте веб-экземпляр — зарегистрированные пользователи

Поскольку мы настроили путь к статическим ресурсам в файле конфигурации, мы можем написать неотделимый веб-экземпляр в ресурсах для доступа.

resources/static/regist.html

страница регистрации

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
    <title>注册用户</title>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="css/regist.css"/>
    <link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">
    
</head>
<body>
<div class="container">
    <div class="page form_page js_show">
        <div class="weui-form">
            <div class="weui-form__text-area">
                <h2 class="weui-form__title">注册新用户</h2>
            </div>
            <div class="weui-form__control-area">
                <div class="weui-cells__group weui-cells__group_form">
                    <div class="weui-cells weui-cells_form">
                        <div class="weui-cell">
                            <div class="weui-cell__hd"><label class="weui-label">用户名</label></div>
                            <div class="weui-cell__bd">
                                <input id="js_input——user" class="weui-input" placeholder="请输入要设置的用户名">
                            </div>
                        </div>
                        <div class="weui-cell">
                            <div class="weui-cell__hd"><label class="weui-label">密码</label></div>
                            <div class="weui-cell__bd">
                                <input id="js_input——pwd" type="password" class="weui-input" placeholder="请输入要设置的密码">
                            </div>
                        </div>
                        <div class="weui-cell">
                            <div class="weui-cell__hd"><label class="weui-label">确认密码</label></div>
                            <div class="weui-cell__bd">
                                <input id="js_input——pwd2" type="password" class="weui-input" placeholder="请再次输入设置的密码" type="number" pattern="[0-9]*">
                            </div>
                        </div>
                    </div>
                </div>
            </div>
<!--            <div class="weui-form__tips-area">-->
<!--                <p class="weui-form__tips">-->
<!--                    表单页提示,居中对齐-->
<!--                </p>-->
<!--            </div>-->
            <div class="weui-form__opr-area">
                <a class="weui-btn weui-btn_primary" href="javascript:" id="submit">确定</a>
            </div>

            <div class="weui-form__extra-area">
                <div class="weui-footer">
<!--                    <p class="weui-footer__links">-->
<!--                        <a href="javascript:void(0);" class="weui-footer__link">底部链接文本</a>-->
<!--                    </p>-->
                    <p class="weui-footer__text">Copyright © 2019 alex wong</p>
                </div>
            </div>
        </div>
        <div id="js_toast" style="display: none;">
            <div class="weui-mask_transparent"></div>
            <div class="weui-toast">
                <i class="weui-icon-success-no-circle weui-icon_toast"></i>
                <p class="weui-toast__content">已完成</p>
            </div>
        </div>
    </div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/regist.js"></script>
</html>

static/js/dataService.js

const APIURL = '/';

window.dataService = {

	//GET
	get: (url, params = {}) => {

		const searchArr = [];

		Object.keys(params).forEach(n => {
			searchArr.push(`${n}=${params[n]}`);
		});

		const searchStr = searchArr.length ? '?' + searchArr.join('&') : '';
		const token = utils.getCookie('token');

		return fetch(APIURL + url + searchStr, {
			method: 'GET',
			headers: {
				token
			}
		}).then(res => res.json());
	},

	//POST
	post: (url, params = {}) => {

		const formData = new FormData();

		Object.keys(params).forEach(n => {
			formData.append(n, params[n]);
		});

		const token = utils.getCookie('token');

		return fetch(APIURL + url, {
			method: 'POST',
			headers: {
				token
			},
			body: formData
		}).then(res => res.json());
	},

	// 注册
	addUser(params) {
		return this.post('user/add', params);
	},

	// 登录
	login(params) {
		return this.post('user/login', params);
	},

	// 用户信息
	getUserInfo(params) {
		return this.get('user/info', params);
	},

};

static/js/utils.js

window.utils = {

	// md5
	generateMd5(userName, password) {
		const salt = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p@!.";
		const asciStr = userName + salt + password;
		const asciArr = asciStr.split('');
		const asciResult = [];
		asciArr.forEach(n => {
			asciResult.push(n.charCodeAt());
		});
		const ascireusltStr = asciResult.join(salt);
		return hex_md5(ascireusltStr);
	},

	// setCookie
	setCookie(name, value) {
		var time = 2 * 60 * 60 * 1000;
		var exp = new Date();
		exp.setTime(exp.getTime() + time);
		document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
	},

	// getCookie
	getCookie(name) {
		var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
		if (arr = document.cookie.match(reg))
			return unescape(arr[2]);
		else
			return null;
	}

};

static/js/regist.js

// 获取相关用户信息
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const passwordConfirmInput = document.getElementById("js_input——pwd2");
const submitBtn = document.getElementById("submit");

// submit
submitBtn.onclick = () => {

	const userName = userNameInput.value;
	const password = passwordInput.value;
	const confirmPassword = passwordConfirmInput.value;

	// verify
	if (!userName) {
		weui.topTips('用户姓名不能为空');
		return;
	} else if (!password) {
		weui.topTips('用户密码不能为空');
		return;
	} else if (confirmPassword !== password) {
		weui.topTips('前后密码不一致,请重试');
		return;
	}

	// 加密密码
	const newPassword = utils.generateMd5(userName, password);

	// 注册
	dataService.addUser({
		userName,
		password: newPassword,
	}).then(res => {
		const {code, data, msg} = res;
		if (!data) {
			weui.topTips(msg);
		} else {
			weui.topTips(`注册成功,欢迎 ${data.userName}`);
			window.location.href = location.origin + '/login.html';
		}
	})
};

Эффект показан на рисунке:

Добавьте несколько основных проверок

Зашифрованная передача пароля пользователя и проверка регистрации нового пользователя

mysql просмотреть таблицу пользователей

11. Back-end - функция входа пользователя

Как упоминалось в шаге 9 выше, добавьте следующий контент непосредственно в указанную выше службу и больше не отображайте весь код.

com.zz.newController.UserController

Сначала определите, существует ли пользователь. Если да, верните основную информацию и верните токен учетных данных пользователя.

/**
     * 登录
     *
     * @param userName 用户名
     * @param password 密码
     * @return {}
     */
    @PostMapping("/login")
    @PassToken
    public Response login(@RequestParam String userName, @RequestParam String password, Response response) {
        
        UserQuery query = new UserQuery();
        query.setUserName(userName);
        query.setPassword(password);
        
        // 验证用户和密码
        try {
            // 判断用户是否已经存在
            User existUser = this.userService.findUserByName(query);
            
            // 生成token
            String token = null;
            
            // 当前用户
            User currentUser = new User();
            if (existUser != null) {
                currentUser.setUserId(existUser.getUserId());
                currentUser.setUserName(existUser.getUserName());
                currentUser.setPassword(password);
               // 生成用户凭证
                token = JWTUtils.createToken(currentUser);
                if (token != null) {
                    existUser.setToken(token);
                }
                response.setMsg("success");
                response.setData(existUser);
            } else {
                // 登录失败
                response.setMsg("登录失败,请检查用户名和密码");
                response.setData(null);
            }
            
        } catch (Exception e) {
            response.setMsg("login failed");
            response.setData(null);
            e.printStackTrace();
        }
        return response;
    }

com.zz.service.UserService

package com.zz.service;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;
import java.util.Map;

public interface UserService {
    
    // 添加用户
    int addUser(UserQuery query);
    
    //查找单个用户
    User findUserById(UserQuery query);
    
    User findUserByName(UserQuery query);
    
}

com.zz.service.impl.UserServiceImpl

package com.zz.service.impl;

import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public int addUser(UserQuery query){
        return this.userMapper.insert(query);
    }
    
    @Override
    public User findUserById(UserQuery query) {
        return this.userMapper.findUserById(query);
    }
    
    @Override
    public User findUserByName(UserQuery query) {
        return this.userMapper.findUserByName(query);
    }
    
    @Override
    public List<User> findAllUser(UserQuery query) {
        return this.userMapper.findAllUser(query);
    }
}

com.zz.mapper.UserMapper

package com.zz.mapper;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;

public interface UserMapper {
    
    int insert(UserQuery query);
    
    User findUserById(UserQuery query);
    
    User findUserByName(UserQuery query);
    
    List<User> findAllUser(UserQuery query);
    
}

mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">

    <resultMap id="BaseResult" type="com.zz.entity.User">
        <id column="user_id" property="userId"></id>
        <id column="user_name" property="userName"></id>
    </resultMap>

    <sql id="base">
        user_id,
        user_name
        <if test="showPassword">
            , password
        </if>
    </sql>

    <sql id="base_condition">
        <where>
            <if test="userName!=null and userName!=''">
                user_name=#{userName}
            </if>
            <if test="password!=null and password!=''">
                and password=#{password}
            </if>
        </where>

    </sql>

    <!-- 查询所有user -->
    <select id="findAllUser" resultMap="BaseResult">
        select
        <include refid="base"/>
        from user
    </select>

    <!-- 查询user -->
    <select id="findUserById" resultMap="BaseResult">
        select
        <include refid="base"/>
        from user
        where
        user_id = #{userId}
    </select>

    <select id="findUserByName" resultMap="BaseResult">
        select
        <include refid="base"/>
        from user
        <include refid="base_condition"/>
    </select>

    <insert id="insert">
        INSERT INTO user(
        user_name,
        password
        ) VALUES (
        #{userName},
        #{password}
        )
    </insert>


</mapper>

12. Создайте веб-экземпляр — войдите в систему

static/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
    <title>login</title>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="css/regist.css"/>
    <link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">
    
</head>
<body>
<div class="container">
    <div class="page form_page js_show">
        <div class="weui-form">
            <div class="weui-form__text-area">
                <h2 class="weui-form__title">登录</h2>
            </div>
            <div class="weui-form__control-area">
                <div class="weui-cells__group weui-cells__group_form">
                    <div class="weui-cells weui-cells_form">
                        <div class="weui-cell">
                            <div class="weui-cell__hd"><label class="weui-label">用户名</label></div>
                            <div class="weui-cell__bd">
                                <input id="js_input——user" class="weui-input" placeholder="请输入用户名">
                            </div>
                        </div>
                        <div class="weui-cell">
                            <div class="weui-cell__hd"><label class="weui-label">密码</label></div>
                            <div class="weui-cell__bd">
                                <input id="js_input——pwd" type="password" class="weui-input" placeholder="请输入密码">
                            </div>
                        </div>
                        
                    </div>
                </div>
            </div>
<!--            <div class="weui-form__tips-area">-->
<!--                <p class="weui-form__tips">-->
<!--                    表单页提示,居中对齐-->
<!--                </p>-->
<!--            </div>-->
            <div class="weui-form__opr-area">
                <a class="weui-btn weui-btn_primary" href="javascript:" id="submit">确定</a>
            </div>

            <div class="weui-form__extra-area">
                <div class="weui-footer">
<!--                    <p class="weui-footer__links">-->
<!--                        <a href="javascript:void(0);" class="weui-footer__link">底部链接文本</a>-->
<!--                    </p>-->
                    <p class="weui-footer__text">Copyright © 2019 alex wong</p>
                </div>
            </div>
        </div>
        <div id="js_toast" style="display: none;">
            <div class="weui-mask_transparent"></div>
            <div class="weui-toast">
                <i class="weui-icon-success-no-circle weui-icon_toast"></i>
                <p class="weui-toast__content">已完成</p>
            </div>
        </div>
    </div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/login.js"></script>
</html>

static/js/login.js

// 获取相关用户信息
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const submitBtn = document.getElementById("submit");

// submit
submitBtn.onclick = () => {

	const userName = userNameInput.value;
	const password = passwordInput.value;

	// verify
	if (!userName) {
		weui.topTips('用户姓名不能为空');
		return;
	} else if (!password) {
		weui.topTips('用户密码不能为空');
		return;
	}

	// 加密密码
	const newPassword = utils.generateMd5(userName, password);

	// 注册
	dataService.login({
		userName,
		password: newPassword,
	}).then(res => {
		const {code, data, msg} = res;
		if (!data) {
			weui.topTips(msg);
		} else {
			weui.topTips(`登录成功,欢迎 ${data.userName}`);
			utils.setCookie('token', data.token);
			location.href = location.origin + '/home.html';
		}
	})
};

Интерфейс входа возвращает маркер учетных данных пользователя, который впоследствии используется для проверки пользовательского интерфейса и повышения безопасности.

13. Добавьте пользовательские аннотации и перехватчики

При регулярном развитии бизнеса помните, что сервис интерфейса не должен предоставляться никому, кто может получить к нему доступ, иначе другие могут просматривать или изменять ваши данные произвольно, что является очень серьезным вопросом. В дополнение к обычному ограничению диапазона фиксированных IP-адресов клиентов от IP-адресов сегмента сети, сам интерфейс также должен повысить безопасность проверки.На данный момент нам нужно использовать ранее сгенерированный токен учетных данных пользователя;

Вопрос в том, если мы настроим контроль, какие интерфейсы нужно проверять, а какие интерфейсы не нужно проверять? Некоторые люди могут сказать, что недостаточно проверить их все напрямую, так зачем беспокоиться. Однако в реальном бизнесе некоторые интерфейсы не могут быть принудительно верифицированы, например, интерфейс, которым некоторые пользователи делятся в WeChat, не может добавить верификацию, иначе расшаренная страница не может нормально отображаться.

Таким образом, мы можем настроить аннотацию @PassToken и добавить интерфейс этой аннотации, чтобы проверка токена не требовалась.

com.zz.common.annotation.PassToken

Аннотация PassToken

package com.zz.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 是否跳过token验证
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

добавить перехватчик

com.zz.common.interceptor.AuthenticationInterceptor

При отправке запроса добавьте токен в заголовок запроса, а затем получите токен из заголовка при проверке

Если нет токена, не подскажите ничего токена;

Если он существует, используйте JWT, чтобы убедиться, что токен существует, и проверьте правильность пароля пользователя.

package com.zz.common.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.mongodb.util.JSON;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

// 拦截器
public class AuthenticationInterceptor implements HandlerInterceptor {
    
    @Autowired
    private UserService userService;
    
    /**
     * response返回信息
     *
     * @param code
     * @param message
     * @return
     * @throws JSONException
     */
    public JSONObject getJsonObject(int code, String message) throws JSONException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msg", message);
        jsonObject.put("code", code);
        return jsonObject;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        
        // 从 http 请求头中取出 token
        String token = BaseApplicationController.getToken();
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //检查是否有PassToken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        
        // 默认执行认证
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        if (token == null || token.equals("null")) {
            JSONObject jsonObject = getJsonObject(403, "无token,请重新登录");
            httpServletResponse.getWriter().write(jsonObject.toString());
            return false;
            // throw new RuntimeException("无token,请重新登录");
        }
        
        // 获取 token 中的 user id
        long userId;
        try {
            userId = BaseApplicationController.getCurrentUserId();
        } catch (JWTDecodeException j) {
            JSONObject jsonObject = getJsonObject(500, "访问异常, token不正确,请重新登录");
            httpServletResponse.getWriter().write(jsonObject.toString());
            return false;
            // throw new RuntimeException("访问异常!");
        }
        
        // 验证用户是否存在
        UserQuery query = new UserQuery();
        query.setUserId(userId);
        query.setShowPassword(Boolean.TRUE);
        User user = userService.findUserById(query);
        
        if (user == null) {
            JSONObject jsonObject = getJsonObject(500, "用户不存在,请重新登录");
            httpServletResponse.getWriter().write(jsonObject.toString());
            return false;
            // throw new RuntimeException("用户不存在,请重新登录");
        }
        
        // 验证token是否有效
        Boolean verify = JWTUtils.verify(token, user);
        if (!verify) {
            JSONObject jsonObject = getJsonObject(500, "非法访问,请重新登录");
            httpServletResponse.getWriter().write(jsonObject.toString());
            return false;
            // throw new RuntimeException("非法访问!");
        }
        
        return true;
    }
}

Давайте посмотрим на эффект на примере:

com.zz.newController.UserController

package com.zz.newController;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;


/**
 * 登录
 * autho: alex wong
 */
@RestController
@RequestMapping("/user")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /*
     * @param userName
     * @param password
     * @return response
     */
    @PostMapping("/add")
    @PassToken
    public Response addUser(@RequestParam String userName, @RequestParam String password, Response response) {
        UserQuery query = new UserQuery();
        User userData = null;
        
        query.setUserName(userName);
        query.setPassword(password);
        
        int result;
        String message = "";
        
        // 判断用户是否已经存在
        UserQuery findUserQuery = new UserQuery();
        findUserQuery.setUserName(userName);
        User existUser = this.userService.findUserByName(findUserQuery);
        if (existUser == null) {
            
            // 插入用户
            try {
                result = this.userService.addUser(query);
                message = "success";
            } catch (Exception e) {
                result = 0;
                message = "error";
                e.printStackTrace();
            }
            
            // 插入用户成功后返回用户信息
            if (result == 1) {
                userData = this.userService.findUserByName(findUserQuery);
                
                // 生成token
                String token = null;
                
                // 当前用户
                User currentUser = new User();
                if (userData != null) {
                    currentUser.setUserId(userData.getUserId());
                    currentUser.setUserName(userData.getUserName());
                    currentUser.setPassword(password);
                    token = JWTUtils.createToken(currentUser);
                }
                
                if (token != null) {
                    userData.setToken(token);
                    
                    // 获取token用户信息
                    // Claims userDataFromToken = JWTUtils.parseToken(token, currentUser);
                }
            }
            
        } else {
            message = "用户已经存在";
        }
        
        response.setData(userData);
        response.setMsg(message);
        return response;
    }
    
    /**
     * 登录
     *
     * @param userName 用户名
     * @param password 密码
     * @return {}
     */
    @PostMapping("/login")
    @PassToken
    public Response login(@RequestParam String userName, @RequestParam String password, Response response) {
        
        UserQuery query = new UserQuery();
        query.setUserName(userName);
        query.setPassword(password);
        
        // 验证用户和密码
        try {
            // 判断用户是否已经存在
            User existUser = this.userService.findUserByName(query);
            
            // 生成token
            String token = null;
            
            // 当前用户
            User currentUser = new User();
            if (existUser != null) {
                currentUser.setUserId(existUser.getUserId());
                currentUser.setUserName(existUser.getUserName());
                currentUser.setPassword(password);
                token = JWTUtils.createToken(currentUser);
                if (token != null) {
                    existUser.setToken(token);
                }
                response.setMsg("success");
                response.setData(existUser);
            } else {
                // 登录失败
                response.setMsg("登录失败,请检查用户名和密码");
                response.setData(null);
            }
            
        } catch (Exception e) {
            response.setMsg("login failed");
            response.setData(null);
            e.printStackTrace();
        }
        return response;
    }
    
    /**
     * 获取个人信息
     *
     * @return {}
     */
    @GetMapping("/info")
    public Response getUserInfo(Response response) {
        // 获取token
        String token = BaseApplicationController.getToken();
        User userData2 = BaseApplicationController.getCurrentUser();
        Map<String, Object> headerData = BaseApplicationController.getHeader();
        if (token != null && !token.equals("null")) {
            User userData = new User();
            DecodedJWT claims = JWT.decode(token);
            userData.setUserName(claims.getClaim("userName").asString());
            userData.setUserId(claims.getClaim("userId").asLong());
            response.setData(userData);
            response.setMsg("success");
        } else {
            response.setMsg("token不存在");
        }
        return response;
    }
    
}

Мы написали новый интерфейс для получения информации о пользователе, как указано выше, и остальной код повторяться не будет;

Успешно получена информация о пользователе

удалить токен

Токен сознательно делает ошибки

На данный момент процесс проверки проходит успешно;

14. Резюме

Вышеупомянутый процесс — это просто создание простого сервиса.Реальный сервис требует дополнительной настройки, такой как конфигурация XSS для предотвращения XSS-атак, данных из нескольких источников и т. д. Что ж, на этом эта статья заканчивается, пожалуйста, потерпите меня, если есть какое-то неясное описание.