Spring Boot + JWT + Mybatis Plus搭建通用快速開發模板

本文將從如何創建Spring Boot項目開始講解如何搭建一個較爲通用的快速開發模板,方便在以後的開發中可以快速的應用,做一個高效的cv工程師,避免每次寫都要去以前的項目裏翻工具類和通用配置。

做高效cv工程師,從搭建自己的常用通用開發模板開始吧,本文的項目源碼都放在我的github
上了,有用就給上一個Star

創建一個Spring Boot項目

開發工具使用idea
採用idea中的Spring Initializr創建項目(如果第一次使用下載依賴會比較慢)

  • 選擇 Spring Initializr方式構建
    在這裏插入圖片描述
  • 配合項目名包路徑
    在這裏插入圖片描述
  • 依賴就選擇一個web的就可以,需要什麼依賴之後手動加入
    在這裏插入圖片描述
  • 設置項目存放位置,選擇自己的工作空間
    在這裏插入圖片描述
  • 點擊finish開始創建,第一次可能會比較久
  • 創建完成後的目錄結構
    在這裏插入圖片描述
  • 修改maven倉庫位置,默認創建的是在C盤中的倉庫,重新定位到自己的倉庫(可不改)
    file - > settings ->搜索maven
    選中自己的本地倉庫地址,點擊apply
    在這裏插入圖片描述
    一個Spring Boot的項目就創建好了

基礎配置編寫

  • 引入常用Maven依賴
		<!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <!--mysql驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1.tmp</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.1.tmp</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <!--模板引擎-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.1</version>
        </dependency>

        <!--guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
        <!-- pagehelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
        <!--通用工具包commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <!--數據源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>
        <!-- 引入swagger-bootstrap-ui包 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.8.5</version>
        </dependency>
        <!--熱部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <!--依賴不傳遞-->
            <optional>true</optional>
        </dependency>
        <!--jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
  • 創建常用的包
    在這裏插入圖片描述

  • 配置BasedemoApplication
    BasedemoApplication是整個web項目的啓動類
    配置一下mapper接口的掃描

@SpringBootApplication
@MapperScan("com.example.basedemo.dao")
public class BasedemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(BasedemoApplication.class, args);
    }
}
  • 創建配置文件
    在這裏插入圖片描述
    默認創建了application.properties
    手動創建開發環境配置:application-dev.properties
    手動創建生產環境配置:application-prov.properties
    application.properties內容:

    spring.profiles.active=dev
    

application-dev.properties和application-prov.properties根據需求自己配置

#端口
server.port = 3036
#xml文件路徑
mybatis.mapper-locations=classpath:mapper/*.xml
#數據類連接
spring.datasource.url=jdbc:mysql://localhost:3306/crm?useUnicode=true&characterEncoding=UTF-8&sessionVariables=time_zone='%2B8:00'
spring.datasource.username=root
spring.datasource.password=root
#使用druid數據源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#連接驅動
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
        
#時間格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

日誌配置

Spring Boot默認採用的是SLF4J + LogBack,這也是比較優的一種搭配了,就直接用這個
引入配置文件logback-spring.xml
在這裏插入圖片描述
日誌寫入根目錄下的log目錄,每天生成一個日誌文件
具體配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

    <!--定義日誌文件的存儲地址 勿在 LogBack 的配置中使用相對路徑-->
    <property name="LOG_HOME" value="log" />

    <!--控制檯日誌, 控制檯輸出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度,%msg:日誌消息,%n是換行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--文件日誌, 按照每天生成日誌文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日誌文件輸出的文件名-->
            <FileNamePattern>${LOG_HOME}/klchen_summary.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日誌文件保留天數-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度%msg:日誌消息,%n是換行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日誌文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>


    <!--myibatis log configure-->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- 日誌輸出級別 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE"/>
    </root>
</configuration>

接口文檔Swagger2配置

在utils目錄下創建Swagger2類,在裏面編寫配置文件,內容如下:
必須要修改的是basePackage("com.klchen.hos.controller"))
修改爲自己的接口包的路徑,才能被掃描到
apiInfo內配置的是文檔上顯示的參數

@Configuration
@EnableSwagger2
public class Swagger2 {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.klchen.hos.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("存儲系統API文檔")
                .description("基於HBase的文件存儲系統API文檔")
                .contact(new Contact("klChen","https://blog.csdn.net/qq_41170102","[email protected]"))
                .version("2.0.1")
                .build();
    }
}

啓動我們的項目,可以查看一下文檔
官方的ui的路徑是:http://localhost:3036/swagger-ui.html,其中3036是配置的項目的port,swagger-ui.html是路徑
在這裏插入圖片描述
在前面引包裏面我們引入了一個包,如下

		 <!-- 引入swagger-bootstrap-ui包 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.8.5</version>
        </dependency>

這個包是一個比較簡潔的ui,訪問路由是http://localhost:3036/doc.html
在這裏插入圖片描述
swagger2常用註解:

  • @Api()用於類;
    表示標識這個類是swagger的資源
  • @ApiOperation()用於方法;
    表示一個http請求的操作
  • @ApiParam()用於方法,參數,字段說明;
    表示對參數的添加元數據(說明或是否必填等)
  • @ApiModel()用於類
    表示對類進行說明,用於參數用實體類接收
  • @ApiModelProperty()用於方法,字段
    表示對model屬性的說明或者數據操作更改
  • @ApiIgnore()用於類,方法,方法參數
    表示這個方法或者類被忽略
  • @ApiImplicitParam() 用於方法
    表示單獨的請求參數
  • @ApiImplicitParams() 用於方法,包含多個 @ApiImplicitParam

自定義全局異常處理配置

首先定義一個接口CommonError,裏面定義三個方法,分別是設置錯誤信息,獲取錯誤信息,獲取錯誤編碼的方法

public interface CommonError {
    public int getErrCode();
    public String getErrMsg();
    public CommonError setErrMsg(String errMsg);
}

定義一個枚舉類,在裏面可以定義我們常用的異常,並實現自定義的CommonError 接口,方便通過枚舉類來獲取錯誤編碼和錯誤信息

public enum EmBusinssError implements CommonError{

    //通用錯誤類型
    PARAMETER_VALIDATION_ERROR(10001,"參數不合法"),
    UNKNOWN_ERROR(10002,"未知錯誤"),
    NOTFIND(10003,"查詢結果爲空"),
    //以20000開頭爲用戶信息相關錯誤定義
    USER_NOT_EXIST(20001,"用戶不存在"),
    PRIMARY_KEY_EXIST(20002,"主鍵衝突"),
    USER_Login_Fail(20003,"用戶賬號或密碼不正確"),
    LOGINOUTTIME(30001,"登錄狀態過期,請重新登錄"),

    ERROR_PERMISSION_DENIED(403,"權限錯誤"),
    ERROR_FILE_NOT_FOUND(404,"未能找到"),
    ERROR_HBASE(500,"hbase發生錯誤"),
    ERROR_HDFS(501,"hdfs發出錯誤")
    ;

    private int errCode;
    private String errMsg;

    EmBusinssError(int errCode, String errMsg) {
        this.errCode = errCode;
        this.errMsg = errMsg;
    }

    @Override
    public int getErrCode() {
        return this.errCode;
    }

    @Override
    public String getErrMsg() {
        return this.errMsg;
    }

    @Override
    public CommonError setErrMsg(String errMsg) {
        this.errMsg = errMsg;
        return this;
    }
}

創建一個包裝器業務異常類,用來包裝異常信息,拋出異常時可以統一使用BusinessException 就可以了

//包裝器業務異常類實現
public class  BusinessException extends Exception implements CommonError {

    private CommonError commonError;

    //直接接收embussinesserror的傳參,用於構造業務異常
    public BusinessException(CommonError commonError) {
        super();
        this.commonError = commonError;
    }
    //接收自定義errMsg的方式構造業務異常
    public BusinessException(CommonError commonError ,String errMsg){
        super();
        this.commonError = commonError;
        this.commonError.setErrMsg(errMsg);
    }

    @Override
    public int getErrCode() {
        return this.commonError.getErrCode();
    }

    @Override
    public String getErrMsg() {
        return this.commonError.getErrMsg();
    }

    @Override
    public CommonError setErrMsg(String errMsg) {
        this.commonError.setErrMsg(errMsg);
        return this;
    }
}

項目中把他們放在error目錄下
在這裏插入圖片描述
爲了捕獲異常,還需要定義個一個頂層的捕獲器 ,將其定義在BaseController中,所有的Controller都繼承BaseController,所有業務拋出的異常最終都會到BaseController進行處理,然後將錯誤信息返回

public class BaseController {


    //定義exceptionhandler解決未被controller層吸收的exception
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Object handlerException(HttpServletRequest request, Exception ex){
        Map<String, Object> responseData = new HashMap<>();

        if (ex instanceof BusinessException){
            BusinessException businessException = (BusinessException)ex;
            responseData.put("errCode",businessException.getErrCode());
            responseData.put("errMsg",businessException.getErrMsg());

        }else {
            responseData.put("errCode", EmBusinssError.UNKNOWN_ERROR.getErrCode());
            responseData.put("errMsg",EmBusinssError.UNKNOWN_ERROR.getErrMsg());
        }

        return CommonreturnType.create(responseData,"fail");


    }

}

通用返回包裝類配置

爲了統一返回的JSON數據的格式,方便使用,需要定義一個通用的返回包裝類
包裝類中

  • 定義了兩個字段,請求狀態和返回數據
    如果是正常的請求,狀態碼爲success,如果是拋出了異常,狀態碼是fail
  • 定義了兩個構造方法,一個是傳入參數爲一個Object的方法,默認爲成功的請求返回,請求狀態是success;一個是傳入兩個參數的方法,可指定請求的狀態

這裏的@Data註解是lambok中的,可以生成所有屬性的getter,setter,tostring方法,使用時需要安裝lambok插件並導入包,後面專門寫一篇lambok使用的文章

@Data
@ApiModel(value = "通用返回類型")
public class CommonreturnType<T> {
    //表明請求的返回處理結果“success”或“fail”
    @ApiModelProperty(value = "狀態碼")
    private String status;

    //若status爲success,返回前端需要的json數據
    //若status爲fail,返回data內使用通用的錯誤格式碼
    @ApiModelProperty(value = "返回數據")
    private T data;

    //定義一個通用的創建方法
    public static CommonreturnType create(Object result){
        return CommonreturnType.create(result,"success");
    }
    public static CommonreturnType create(Object result, String status){
        CommonreturnType type = new CommonreturnType();
        type.setStatus(status);
        type.setData(result);
        return type;
    }
}

PageHelper分頁插件配置

我們主要是使用Mybatis Plus來實現CRUD,並且Mybatis Plus已經存在了只需要配置就能用的分頁插件,但是Mybatis Plus只針對單表操作很方便,多表聯查的場景還是需要自己實現,出於編碼習慣,多表聯查的分頁查詢還是使用pagehelper的分頁插件
首先要導入對應的依賴,前面也已經導入過了

		<!-- pagehelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>

編寫一個自定義的PageInfo用於包裝返回數據,如果不自己設置就是使用默認的,參數會比較多而且名字不能變
爲了方便接口調用者,我們把PageInfo內的參數名儘量和Mybatis Plus中的分頁的參數名一致

@SuppressWarnings({"rawtypes", "unchecked"})
@Data
public class PageInfo<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    //當前頁
    private int current;
    //每頁的數量
    private int size;
    //總記錄數
    private long total;
    //總頁數
    private int pages;
    //結果集
    private List<T> records;
    //是否爲第一頁
    private boolean isFirstPage = false;
    //是否爲最後一頁
    private boolean isLastPage = false;


    public PageInfo() {
    }

    /**
     * 包裝Page對象
     *
     * @param records
     */
    public PageInfo(List<T> records) {
        if (records instanceof Page) {
            Page page = (Page) records;
            this.current = page.getPageNum();
            this.size = page.getPageSize();

            this.pages = page.getPages();
            this.records = page;
            this.total = page.getTotal();
        } else if (records instanceof Collection) {
            this.current = 1;
            this.size = records.size();

            this.pages = 1;
            this.records = records;
            this.total = records.size();
        }
        if (records instanceof Collection) {
            //判斷頁面邊界
            judgePageBoudary();
        }
    }

    /**
     * 判定頁面邊界
     */
    private void judgePageBoudary() {
        isFirstPage = current == 1;
        isLastPage = current == pages;
    }
    
}

經過以上兩步匹配,之後就可以使用了

Mybatis Plus分頁配置

這是Mybatis Plus自帶的分頁插件,用起來非常方便,只需要注入一個bean就可以了

@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

    /**
     * 分頁插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

Mybatis Plus自動生成模板與配置

Mybatis Plus之所以強大,一方面就是他的自動生成模板,只需少量配置,就可以生成可以實現幾乎所有單表查詢的dao層和server層,讓程序猿可以專注於業務邏輯而不是繁瑣的編碼

在test下的根目錄下添加如下的文件,文件可以在我的github
中下載
在這裏插入圖片描述
templates下的模板都是配置好的,基本不需要修改,我覺得是非常好用

CodeGenerator就是我們的代碼生成器了,配置好了之後運行就可以生成我們的通用dao層和通用server層,只需要學會他的調用方法就可以了

如果只是簡單使用,需要配置的內容如下

將數據庫連接和項目根路徑修改爲自己的

	private static final String dbUrl = "jdbc:mysql://localhost:3306/crm?useUnicode=true&characterEncoding=UTF-8&sessionVariables=time_zone='%2B8:00'";//數據庫連接url
    private static final String dbDriver = "com.mysql.cj.jdbc.Driver";//數據庫驅動
    private static final String dbUsername = "root";//數據庫賬號
    private static final String dbPassword = "root";//數據庫密碼
    private static final String basePackage = "cn.chenkl";//項目包路徑

在最下面找到這一行代碼,將需要自動生成的表名加入到裏面,以逗號隔開
在這裏插入圖片描述
完成上面兩步配置就可以啓動CodeGenerator了,處於測試,我就只生成一張表tn_user_info的。啓動後,生成了如下的五個文件,對應了實體類,dao層和service層的接口和實現類
在這裏插入圖片描述
自動生成就配置完畢,後面的實際操作中會講解具體的用法

相比之前用過的Mybatis-generator和通用mapper,針對單表操作,Mybatis-Plus真的太強大了
從別的博主處搬來一張對比圖,感興趣的可以去看看對比的博文
在這裏插入圖片描述
圖片出處https://blog.csdn.net/m0_37524586/article/details/88351833

JWT加密解密配置

JSON Web Token(JWT)是目前最流行的無狀態跨域身份驗證解決方案。

JWT又三部分組成,第一部分我們稱它爲頭部(header),第二部分我們稱其爲載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

  • header
    頭部承載了兩部分信息,分別是令牌類型和簽名算法
{
  'typ': 'JWT',
  'alg': 'HS256'
}

頭部進行base64加密形式第一部分的密文

  • playload

載荷就是存放有效信息的地方。分爲標準中註冊的聲明,公共的聲明,私有的聲明

  • 註冊中的聲明包括以下七種

    iss: jwt簽發者
    sub: jwt所面向的用戶
    aud: 接收jwt的一方
    exp: jwt的過期時間,這個過期時間必須要大於簽發時間
    nbf: 定義在什麼時間之前,該jwt都是不可用的.
    iat: jwt的簽發時間
    jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。

不強制被要求使用

  • 公共的聲明 :
    公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因爲該部分在客戶端可解密.

  • 私有的聲明 :
    私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因爲base64是對稱解密的,意味着該部分信息可以歸類爲明文信息。

將所有的聲明進行base64加密生成第二部分密文

  • signature
    jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

    header (base64後的)
    payload (base64後的)
    secret

將加密後的header和加密後的payload進行字符串拼接,之後通過頭部聲明的算法進行再次加密,加密時加鹽secret

secret是服務器端私有的一個密匙,不能泄露,因爲base64加密是對稱加密,有了secret之後可以將token破解,導致信息泄露

實踐一下:

創建一個playload實體用於存儲我們的聲明

@Data
public class Payload {
    private String issuer;//發佈者
    private String subject;//主題
    private List<String> audience;//簽名的觀衆 也可以理解誰接受簽名的
    private Date issuedAt;//發佈時間
    private Date expiresAt;//過期時間
    private Date notBefore;//開始使用時間
    private Map<String, String> claims;

    //重載這個方法
    public void setAudience(List<String> audience) {
        this.audience = audience;
    }
    public void setAudience(String... audienceStr) {
        List<String> audiences = new ArrayList<String>();
        for (String string : audienceStr) {
            audiences.add(string);
        }
        this.audience = audiences;
    }
}

聲明參數採用的是註冊中的聲明,claims中可以自定義存放數據,通常是業務相關的一些非重要信息,如用戶名,用戶角色等

創建一個加密解密業務類JWTService,在裏面創建生成簽名和翻譯簽名的方法方便調用

生成簽名的過程:封裝Payload 屬性,對頭部加密,對負荷加密,對簽名加密

 /**
     * 創建 hour小時後過期的Token
     *
     * @param claims
     * @param hour
     * @return
     */
    public String createToken(Map<String, String> claims, int hour) throws UnsupportedEncodingException {
        Payload createPayload = this.createPayload(hour);//封裝公有負載
        createPayload.setClaims(claims);//封裝私有負載
        Algorithm hmac256 = Algorithm.HMAC256(this.getSecret());//構建密匙信息
        return createToken(createPayload, hmac256);
    }

    /**
     * 根據負載和算法創建Token
     *
     * @param payload
     * @param algorithm
     * @return
     */
    public String createToken(Payload payload, Algorithm algorithm) {

        Builder headBuilder = createHeaderBuilder(algorithm);//頭部加密
        Builder publicClaimbuilder = addPublicClaimBuilder(headBuilder, payload);//公有負載加密
        Builder privateClaimbuilder = addPrivateClaimbuilder(publicClaimbuilder, payload);//私有負載加密
        String token = privateClaimbuilder.sign(algorithm);//生成簽名
        return token;
    }

校驗(解密)token的過程:解碼簽名獲得payload,解析payload獲取對應的負荷信息封裝到payload對象

	/**
     * 校驗Token
     *
     * @param token
     * @return
     */
    public Payload verifyToken(String token) throws UnsupportedEncodingException {
        DecodedJWT jwt = null;
        Payload payload = null;
        try {
            jwt = getDecodedJWT(token);//解碼token獲得jwt
            payload = getPublicClaim(jwt);//jwt解析出公有負載
            payload = getPrivateClaim(jwt, payload);//jwt解析出私有負載
        } catch (AlgorithmMismatchException e) {
            throw e;
        }
        return payload;//返回解析對象
    }

具體的方法實現參照我的github

登錄攔截器配置

定義一個登陸攔截器LoginInterceptor實現HandlerInterceptor接口

只需要具體實現下preHandle方法

從header中獲取token的信息

String token = request.getHeader("X-Auth-Token");

如果token爲空重定向到一個返回沒有登錄的信息的接口

  //token is null
  if (StringUtils.isEmpty(token)) {
       String url = "/toLogin";
       response.sendRedirect(url);
       return false;
  }

爲了實現退出登錄功能,登錄完成後需要將token存放到ServletContext中,所以我們還需要看一下ServletContext中有沒有數據,如果沒有,說明調用過退出登錄的接口,當前token就邏輯上失效了

		String tokenInServletContext = (String)request.getServletContext().getAttribute(token);

        //未登錄或者過期   ServletContext中找不到這個token
        if(StringUtils.isEmpty(tokenInServletContext)) {
            String url = "/toLogin";
            response.sendRedirect(url);
            return false;
        }

通過上面兩層校驗之後,調用JWTServer中的解密的方法將JWT解密,並捕獲異常,如果發生異常說明解密失敗。token不正確,如果正確解密,返回一個Payload對象,可以存入ThreadLocal中,方便調用其中的屬性

	//校驗token,如果無誤放行
    Payload payload = jwtService.verifyToken(token);
    ContextUtil.setCurrentplayLoad(payload);

定義一個攔截器配置類InterceptorConfig,實現WebMvcConfigurer接口

首先定義一個Map存放免驗證的url
將不需要jwt驗證的接口放在裏面

 //請求無需驗證登錄的url
    List<String> passingUrl = new ArrayList<String>(){
        {
            //登錄
            add("/login");
            //未登錄跳轉提醒
            add("/toLogin");

            //swagger 相關
            add("/swagger-ui.html");
            add("/webjars/**");
            add("/v2/**");
            add("/swagger-resources/**");
            add("/doc.html/**");
        }
    };

注入一個LoginInterceptor實例

@Autowired
private LoginInterceptor loginInterceptor ;

添加一個攔截器將LoginInterceptor 和passingUrl注入進去

	/**
     * 攔截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).
                addPathPatterns("/**").
                excludePathPatterns(passingUrl);
    }

同時,還有兩個常用的攔截配置需要加入

  • SpringBoot中默認只支持"GET", “POST”, “PUT”, "DELETE"中的Get和Post請求,想要實現Restful的Api,並解決跨域問題,所有需要配置下addCorsMappings
  • 系統中常用到靜態資源映射,映射之後可以通過url訪問服務器上映射的資源,
 /**
     * 配置跨域和支持restoful接口
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "PUT", "DELETE");
    }

    /**
     * 設置圖片映射
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //addResourceHandler是指你想在url請求的路徑
        //addResourceLocations是圖片存放的真實路徑
        registry.addResourceHandler("/image/**").addResourceLocations("file:"+System.getProperty("user.dir")+"/image/");
    }

線程局部變量ThreadLocal使用

還不瞭解ThreadLocal的可以看一看我寫過的一篇關於ThreadLocal的博文

使用時,創建一個工具類ContextUtil操作ThreadLocal

/**
 * ThreadLocal工具類,用來獲取請求用戶的信息
 */
public class ContextUtil {

    //線程局部變量
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static String getCurrentUser() {
        return threadLocal.get();
    }

    public static void setCurrentUser(String openId) {
        threadLocal.set(openId);
    }

    public static void clear() {
        threadLocal.remove();
    }
    
}

當jwt驗證通過後,將Payload對象存入到ThreadLocal中,在一次請求調用到的代碼的任何地方都可以獲取到Payload對象,從而獲取到封裝的業務屬性

熱部署

使用熱部署可以在代碼修改後不用重啓服務器,只需要保存代碼就會自動的重新加載

首先 導入依賴

		<!--熱部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <!--依賴不傳遞-->
            <optional>true</optional>
        </dependency>

在配置文件中添加配置

#熱部署生效
spring.devtools.restart.enabled=true
#設置重啓的目錄,添加那個目錄的文件需要restart
spring.devtools.restart.additional-paths=src/main/java

修改idea配置
File-Settings-Build-Compiler-Build Project automatically
在這裏插入圖片描述
ctrl + shift + alt + / ,選擇Registry,勾上 Compiler autoMake allow when app running
在這裏插入圖片描述

ssl證書配置

(這個需要https時才配)
寫了一篇使用博客

登錄接口實現

從這裏開始就要將之前配置的所有東西都用起來了

定義一個LoginController繼承自BaseController

注入JWTService方便調用

	 @Autowired
    private JWTService jwtService;

編寫登錄的接口,首先校驗是否爲空,然後調用方法校驗密碼,如果不滿足,就直接拋出BusinessException

驗證通過後,將自定義參數加入到map中,調用createToken方法生成token,並存儲到ServletContext 中,並返回給接口調用方token

	@ApiOperation("登錄")
    @PostMapping("/login")
    public CommonreturnType login(@RequestParam(value = "username") String username,
                                  @RequestParam(value = "password") String password,
                                  HttpServletRequest request) throws BusinessException, UnsupportedEncodingException {
        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new BusinessException(EmBusinssError.USER_Login_Fail);
        }
        if (!checked(username,password)){
            throw new BusinessException(EmBusinssError.USER_Login_Fail);
        }

        Map<String, String> userInfo = new HashMap<String, String>() {
            {
                put("username", username);
                //自定義參數
            }
        };

        String token = jwtService.createToken(userInfo, 1);
        ServletContext context = request.getServletContext();
        context.setAttribute(token, token);

        return CommonreturnType.create(token);
    }

退出登錄方法
只需要清除掉ServletContext 中的token,就可以使token邏輯上失效,也可以存入到redis中,這裏就不演示了

	@ApiOperation("退出登錄")
    @GetMapping("/logout")
    public CommonreturnType logout(String token, HttpServletRequest request) {
        ServletContext context = request.getServletContext();
        context.removeAttribute(token);
        return CommonreturnType.create("logout");
    }

token驗證不通過重定向方法

	@ApiIgnore
    @GetMapping("/toLogin")
    public CommonreturnType toLogin() throws BusinessException {
        throw new BusinessException(EmBusinssError.LOGINOUTTIME);
    }

單表CRUD實現

Mybatis Plus的常用操作可以去看官方的文檔

慕課網上也有一個不錯的免費課程,可以跟着熟悉下API,MyBatis-Plus入門

以用戶表爲例,對用戶表進行增刪改查和分頁查詢,代碼如下

@RestController
@RequestMapping("/api")
public class BaseDataController extends BaseController {

    @Autowired
    private TnUserInfoService tnUserInfoService;

    @ApiOperation(value = "insert", notes = "")
    @PostMapping("/user")
    public CommonreturnType insertUser(@RequestBody TnUserInfoPo tnUserInfoPo) throws BusinessException {
        Integer count = tnUserInfoService.lambdaQuery().
                eq(TnUserInfoPo::getUserCode, tnUserInfoPo.getUserCode()).
                count();//判斷主鍵是否已經存在
        if (count > 0) {//主鍵衝突
            throw new BusinessException(EmBusinssError.PRIMARY_KEY_EXIST);
        }
        boolean save = tnUserInfoService.save(tnUserInfoPo);
        return CommonreturnType.create(save);
    }

    @ApiOperation(value = "update", notes = "")
    @PutMapping("/user")
    public CommonreturnType updateUser(@RequestBody TnUserInfoPo tnUserInfoPo) {
        // 通過傳入實體進行修改,默認策略會排除空字段
//        boolean update = tnUserInfoService.updateById(tnUserInfoPo);
        // 通過條件修改指定的字段 lambda表達式
        boolean update = tnUserInfoService.lambdaUpdate().
                eq(TnUserInfoPo::getUserCode, tnUserInfoPo.getUserCode()).
                set(TnUserInfoPo::getUserName, tnUserInfoPo.getUserName()).update();
        return CommonreturnType.create(update);
    }

    @ApiOperation(value = "delete", notes = "")
    @DeleteMapping("/user")
    public CommonreturnType deleteUser(@RequestParam(value = "userCode") String userCode) {
        boolean remove = tnUserInfoService.lambdaUpdate().
                eq(TnUserInfoPo::getUserCode, userCode).
                remove();
        return CommonreturnType.create(remove);
    }

    @ApiOperation(value = "get", notes = "")
    @GetMapping("/user/{userCode}")
    public CommonreturnType getUser(@PathVariable String userCode) {
        TnUserInfoPo tnUserInfoPo = tnUserInfoService.
                getOne(Wrappers.<TnUserInfoPo>lambdaQuery().
                        eq(TnUserInfoPo::getUserCode, userCode));
        return CommonreturnType.create(tnUserInfoPo);
    }

    @ApiOperation(value = "getPage", notes = "")
    @GetMapping(value = "/user/pageNum/{pageNum}/pageSize/{pageSize}")
    public CommonreturnType getUserByPage(@PathVariable Integer pageNum,
                                          @PathVariable Integer pageSize) {
        IPage<TnUserInfoPo> page = new Page<>(pageNum, pageSize);
        IPage<TnUserInfoPo> pageInfo = tnUserInfoService.lambdaQuery().
                select(TnUserInfoPo.class,info->!info.getColumn().equals("PASSWORD")).//排除密碼被查詢
                page(page);
        return CommonreturnType.create(pageInfo);
    }
    
}

多表CRUD實現

用戶表的字段如下:

	 @TableId("USER_CODE")
    private String userCode;

    @TableField("USER_NAME")
    private String userName;

    @TableField("UNIT_CODE")
    private String unitCode;

    @TableField("ChineseName")
    private String ChineseName;

    @TableField("PASSWORD")
    private String password;

    @TableField("CREATE_TIME")
    private Date createTime;

其中的UNIT_CODE對應部門表的部門id,查詢用戶信息時應該把部門名稱也一同查詢出來,所以查詢需要關聯上部門表

聯表查詢的Sql如下,不僅要查出來,我們還需要分頁查詢

SELECT
	u.USER_CODE,
	u.USER_NAME,
	u.ChineseName,
	u.UNIT_CODE,
	unit.NAME unitName,
	u.CREATE_TIME 
FROM
	`tn_user_info` u
	LEFT JOIN tn_unit unit ON unit.UNIT_CODE = u.UNIT_CODE

多表查詢可以使用wrapper輔助使用,習慣了原生的Mybatis使用,所以就採用原生的開發

添加一個實體類封裝返回數據

@Data
@ApiModel(value="User多表聯查結果集映射對象", description="")
public class UserVo implements Serializable {
    private String userCode;

    private String userName;

    private String unitCode;

    private String unitName;

    private String ChineseName;

    private Date createTime;
}

Controller層

	@ApiOperation(value = "get2", notes = "多表關聯查詢")
    @GetMapping("/user2/{userCode}")
    public CommonreturnType getUser2(@PathVariable String userCode) {
        UserVo user = tnUserInfoService.getUser(userCode);
        return CommonreturnType.create(user);
    }
    @ApiOperation(value = "getPage2", notes = "多表關聯分頁查詢")
    @GetMapping(value = "/user2/pageNum/{pageNum}/pageSize/{pageSize}")
    public CommonreturnType getUserByPage2(@PathVariable Integer pageNum,
                                          @PathVariable Integer pageSize) {

        com.github.pagehelper.Page<UserVo> userVos = tnUserInfoService.getUserByPage(pageNum, pageSize);
        PageInfo<UserVo> pageInfo = new PageInfo<>(userVos);
        return CommonreturnType.create(pageInfo);
    }

Server層實現類

	@Autowired
    private TnUserInfoDao tnUserInfoDao;

    @Override
    public UserVo getUser(String userCode) {
        return tnUserInfoDao.getUser(userCode);
    }

    @Override
    public Page<UserVo> getUserByPage(Integer pageNum, Integer pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        return tnUserInfoDao.getUserByPage();
    }

dao層xml文件

<resultMap id="MyResultMap" type="com.example.basedemo.entity.vo.UserVo">
    <id column="USER_CODE" property="userCode" />
    <result column="USER_NAME" property="userName" />
    <result column="UNIT_CODE" property="unitCode" />
    <result column="unitName" property="unitName" />
    <result column="ChineseName" property="ChineseName" />
    <result column="CREATE_TIME" property="createTime" />
</resultMap>

<select id="getUser" resultMap="MyResultMap">
    SELECT
        u.USER_CODE,
        u.USER_NAME,
        u.ChineseName,
        u.UNIT_CODE,
        unit.NAME unitName,
        u.CREATE_TIME
    FROM
      `tn_user_info` u
    LEFT JOIN tn_unit unit ON unit.UNIT_CODE = u.UNIT_CODE
    WHERE u.USER_CODE = #{userCode}
</select>
<select id="getUserByPage" resultMap="MyResultMap">
    SELECT
        u.USER_CODE,
        u.USER_NAME,
        u.ChineseName,
        u.UNIT_CODE,
        unit.NAME unitName,
        u.CREATE_TIME
    FROM
      `tn_user_info` u
    LEFT JOIN tn_unit unit ON unit.UNIT_CODE = u.UNIT_CODE
</select>

Swagger2測試

測試前可以設置是否啓用JWT驗證
在登錄攔截器中注入當前的配置文件環境

	/**
     * 當前激活的配置文件
     */
    @Value("${spring.profiles.active}")
    private String env;

在前置方法裏面加上放行判斷

		//認證驗證
        if ("dev".equals(env)) { //開發環境忽略簽名認證
            return true;
        }

如果是開發環境下不啓動登錄攔截器

沒有攔截器時swagger-ui.htm的測試文件進行測試,針對上傳下載會比較友好,而加上了攔截器需要傳token時可以採用doc.html的接口文檔,可以設置全局的header,方便測試

登錄測試,成功生成token
在這裏插入圖片描述
分頁查詢,查詢多表關聯查詢的
在這裏插入圖片描述
單表的分頁查詢
在這裏插入圖片描述
新增用戶
在這裏插入圖片描述

PostMan測試

相比自動生成的測試接口,使用Postman會更加的靈活
在這裏插入圖片描述
針對一個項目的接口測試,可以創建一個Collection,然後所有的接口都放在裏面
點擊new Collection創建一個Collection
在這裏插入圖片描述
然後再我們剛纔編寫完測試的頁面點擊Save
在這裏插入圖片描述
選中我們新建的Collection
在這裏插入圖片描述
接口就保存好了
在這裏插入圖片描述
點擊右鍵可以修改請求的名稱,方便管理

在這裏插入圖片描述
當啓用jwt驗證時,可以通過給Collection配置全局的header,在裏面攜帶我們的token
點擊
在這裏插入圖片描述
選中Api key
在這裏插入圖片描述
進行一次登錄,然後把token存入header中,保存後再去訪問其他接口就會自動攜帶jwt
在這裏插入圖片描述
一個較爲完善的測試接口文檔如下
在這裏插入圖片描述

項目部署

前後端的開發通常都不是一個人能完成的,爲了方便接口調用者使用,接口寫好後需要部署到服務器
如果是高校學生還沒有云服務器的可以去這裏看看,免費領12個月

最簡單方便的就是打成一個jar包拖服務器上啓動就好了,Spring Boot支持打成Jar包,內置有tomcat,服務器只需要java運行環境就可以了

點擊maven-package打包
在這裏插入圖片描述
在target目錄下找到jar包
在這裏插入圖片描述

將這個jar上傳到服務器上
在這裏插入圖片描述

輸入nohup java -jar basedemo-0.0.1-SNAPSHOT.jar &
將jar包在後臺掛起

爲了方便使用,可以編寫啓動和停止的腳本,或者使用docker進行部署


有時間了再來慢慢完善


更新 2020年5月4日14:57:34

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章