SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 后端篇(四): 整合阿里雲 短信服務、整合 JWT 單點登錄


(1) 相關博文地址:

SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(一):搭建基本環境:https://www.cnblogs.com/l-y-h/p/12930895.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(二):引入 element-ui 定義基本頁面顯示:https://www.cnblogs.com/l-y-h/p/12935300.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(三):引入 js-cookie、axios、mock 封裝請求處理以及返回結果:https://www.cnblogs.com/l-y-h/p/12955001.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(四):引入 vuex 進行狀態管理、引入 vue-i18n 進行國際化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(五):引入 vue-router 進行路由管理、模塊化封裝 axios 請求、使用 iframe 標簽嵌套頁面:https://www.cnblogs.com/l-y-h/p/12973364.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 前端篇(六):使用 vue-router 進行動態加載菜單:https://www.cnblogs.com/l-y-h/p/13052196.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 后端篇(一): 搭建基本環境、整合 Swagger、MyBatisPlus、JSR303 以及國際化操作:https://www.cnblogs.com/l-y-h/p/13083375.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 后端篇(二): 整合 Redis(常用工具類、緩存)、整合郵件發送功能:https://www.cnblogs.com/l-y-h/p/13163653.html
SpringBoot + Vue + ElementUI 實現后台管理系統模板 -- 后端篇(三): 整合阿里雲 OSS 服務 -- 上傳、下載文件、圖片:https://www.cnblogs.com/l-y-h/p/13202746.html

(2)代碼地址:

https://github.com/lyh-man/admin-vue-template.git

 

一、SpringBoot 整合阿里雲服務 -- 短信服務

1、簡介

  短信服務(Short Message Service)是指通過調用短信發送API,將指定短信內容發送給指定手機用戶。短信的內容多用於企業向用戶傳遞驗證碼、系統通知、會員服務等信息。

2、開通短信服務

(1)進入阿里雲官網,找到 短信服務

【官網地址:】
    https://www.aliyun.com/

【官方文檔:】
    https://help.aliyun.com/product/44282.html
    
【使用流程參考文檔:】
    https://help.aliyun.com/document_detail/59210.html

 

 

 

(2)進入 短信服務 控制台

 

 

 

(3)開通短信服務(發短信要收費的)

 

 

 

 

 

 

(4)添加短信 模板管理
  用於定義短信主體內容。
Step1:
  選擇國內消息 --》模板管理 --》添加模板

 

 

Step2:
  填寫模板相關信息,並等待審核(審核通過后即可使用)。

 

 

 

(5)添加短信 簽名管理
  用於定義短信簽名。
Step1:
  選擇國內消息 --》簽名管理 --》添加簽名

 

 

 

Step2:
  填寫簽名相關信息,並等待審核(審核通過后即可使用)。

 

 

 

(6)簡單測試一下:
  輸出格式如下圖所示:
    【簽名】 + 模板

【后台管理系統】您的驗證碼758644,該驗證碼5分鍾內有效,請勿泄漏於他人!

點擊發送短信,該手機號即可接收到短信。

 

 

 

3、SpringBoot 整合短信服務

【參考文檔:】
    https://help.aliyun.com/document_detail/112148.html

(1)引入依賴
  引入 短信服務 所需的 jar 包。

<!-- aliyun sms -->
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.5.1</version>
</dependency>

 

 

 

(2)編寫配置信息

# 阿里雲配置信息
aliyun:
  # common 配置信息
  accessKeyId: LTAI4GEWZbLZocBzXKYEfmmq
  accessKeySecret: rZLsruKxWex2qGYVA3UsuBgW5B3uJQ
  # SMS 短信服務
  regionId: cn-hangzhou
  signName: 后台管理系統
  templateCode: SMS_194050461

 

 

 

(3)編寫一個工具類 SmsUtil.java 用來操作短信發送。
短信發送參數:
  需要使用 AccessKey,可參考:https://www.cnblogs.com/l-y-h/p/13202746.html#_label0_1
  需要使用 SignName,在網站中申請的簽名模板(比如:后台管理系統)。
  需要使用 TemplateCode,在網站中申請的模板 CODE(比如:SMS_194050461)。
  需要使用 PhoneNumbers,用來接收驗證碼的手機號。
  需要使用 TemplateParam,json 形式,用於保存驗證碼。

注:
  使用 AccessKey 時需要給其開通 發送短信的權限。

 

 

 

package com.lyh.admin_template.back.common.utils;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.lyh.admin_template.back.modules.sms.entity.SmsResponse;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * sms 短信發送工具類
 */
@Data
@Component
public class SmsUtil {
    @Value("${aliyun.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.signName}")
    private String signName;
    @Value("${aliyun.templateCode}")
    private String templateCode;
    @Value("${aliyun.regionId}")
    private String regionId;
    private final static String OK = "OK";

    /**
     * 發送短信
     */
    public boolean sendSms(String phoneNumbers) {
        if (StringUtils.isEmpty(phoneNumbers)) {
            return false;
        }
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        // 固定參數,無需修改
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", regionId);

        // 設置手機號
        request.putQueryParameter("PhoneNumbers", phoneNumbers);
        // 設置簽名模板
        request.putQueryParameter("SignName", signName);
        // 設置短信模板
        request.putQueryParameter("TemplateCode", templateCode);
        // 設置短信驗證碼
        request.putQueryParameter("TemplateParam", "{\"code\":" + getCode() +"}");
        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            // 轉換返回的數據(需引入 Gson 依賴)
            SmsResponse smsResponse = GsonUtil.fromJson(response.getData(), SmsResponse.class);
            // 當 message 與 code 均為 ok 時,短信發送成功、否則失敗
            if (SmsUtil.OK.equals(smsResponse.getMessage()) && SmsUtil.OK.equals(smsResponse.getCode())) {
                return true;
            }
            return false;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 獲取 6 位驗證碼
     */
    public String getCode() {
        return String.valueOf((int)((Math.random()*9+1)*100000));
    }
}

 

 

 

在上面代碼中,為了更好地獲取到返回數據,使用 Gson 對其數據進行轉換(之前博客中已有介紹,此處直接使用,可參考:https://www.cnblogs.com/l-y-h/p/13163653.html#_label1_1),生成 SmsResponse 實例,再根據 SmsResponse 實例數據進行相關處理。

package com.lyh.admin_template.back.modules.sms.entity;

import lombok.Data;

/**
 * 用於接收並轉換 sms 返回的數據
 */
@Data
public class SmsResponse {
    private String Message;
    private String RequestId;
    private String Code;
    private String BizId;
}

 

 

 

(4)編寫一個代碼 TestSMSController.java 測試一下。

package com.lyh.admin_template.back.modules.sms.controller;

import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.common.utils.SmsUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/sms")
@Api(tags = "短信發送")
public class TestSMSController {

    @Autowired
    private SmsUtil smsUtil;

    @ApiOperation(value = "測試短信發送功能")
    @ApiImplicitParam(name = "phoneNumber", required = true, value = "手機號", paramType = "query", dataType = "String")
    @PostMapping("/testSend")
    public Result testSend(@RequestParam String phoneNumber) {
        if (smsUtil.sendSms(phoneNumber)) {
            return Result.ok().message("短信發送成功");
        }
        return Result.error().message("短信發送失敗");
    }
}

 

 

 

二、SpringBoot 整合 JWT 單點登錄

1、簡單了解下概念(session、SSO、token)

  開發過程中,前后端通過 http 協議進行數據交互。而 http 是一種無狀態的協議,也即客戶端(瀏覽器)每次請求都會被服務器獨立處理,每個請求間沒有任何關系。
  這就導致了一個問題:服務器如何知道某個請求是哪個客戶端(用戶)發送的?

(1)解決方法一:
  既然 http 無狀態,那自己主動讓服務器與客戶端同時維護某個用戶狀態即可。
  最常用的就是 session。

session 使用流程:
  Step1:用戶登錄,客戶端向服務器發送用戶名、密碼等用戶信息。
  Step2:服務器驗證數據,並將通過驗證的用戶信息保存在 session 中。
  Step3:服務器響應請求,並將 session_id 返回給客戶端。
  Step4:客戶端接收返回數據,並將 session_id 保存在 cookie 中。
  Step5:下一次客戶端發送請求時,會從 cookie 中取出 session_id 並發給服務器。
  Step6:服務器通過 session_id 找到相應的 session 數據,解析出用戶信息,從而知道是哪個客戶端(用戶)發送的請求。

 

 

 

session 分析:
  由於需要在服務器中進行存儲,若用戶數量過多,會消耗很多存儲空間。且通常一個復雜的業務中一個請求會進行多次轉發操作,每次都需要經過 session_id 查詢 session 數據的操作,無異於增加服務器壓力。
  具有局限性,適用於傳統的單一服務器模式。如果服務器是使用集群的方式部署,那么就需要對 session 進行共享處理。

  

對於多服務器模式,有什么好的登錄方案嗎?
可以使用 單點登錄 方式解決。

(2)解決方法二:
  單點登錄(Single Sign On),簡稱 SSO,指的是在多個系統中,用戶只需要登錄一次,就可以訪問所有相互信任的系統。也即 對於多個系統不用重復進行登錄操作。

 

SSO 使用流程:
  Step1:用戶第一次訪問系統時,由於還未登錄,會被轉向登錄界面用於用戶登錄。
  Step2:用戶信息發送到認證服務器,並對其進行校驗,通過后返回一個認證信息(令牌)。
  Step3:用戶再次訪問系統時,帶上這個令牌,作為認證依據。
  Step4:系統服務器接收請求后,將令牌發給認證服務器進行校驗,若通過校驗則可以訪問系統服務器。

 

 

sso 分析:
  用戶信息可以在認證服務器獨立保存,便於分布式部署,也可以自定義安全策略。
  但同時增加了認證服務器的壓力。

常見 sso 實現機制即為 token。

 

(3)解決方法三:
  基於 token 實現單點登錄。簡單理解:令牌,是由服務端生成的一串字符串,作為客戶端進行請求的一個標識。

 

token 使用流程:
  Step1:用戶登錄,客戶端向服務器發送用戶名、密碼等用戶信息。
  Step2;服務器驗證數據,並將驗證通過的數據生成一個 token(加密字符串)。
  Step3:服務器響應請求,並將 token 返回給客戶端。
  Step4:客戶端接收返回數據,將 token 保存在 cookie 或者 localStorage、sessionStorage 中。
  Step5:下一次客戶端發送請求時,會帶上這個 token。
  Step6:服務器驗證 token,從而獲取用戶數據。

 

 

注:
  通過 token 與 session 的使用流程比較,實現邏輯看起來是類似的。
  但是還是有區別的。使用 token 時不需在服務器存儲用戶信息,直接從 token 中就可以解析出用戶信息。session 需要在服務器存儲用戶信息(多個服務器時需要實現 session 共享,否則多個系統需要進行多次登錄)。所以使用 token 便於拓展業務(不需要知道在哪個服務器進行登錄操作)。

 

token 分析:
  token 無狀態、且不需要將信息存儲在 session 中,便於擴展。但是由於 token 存儲在客戶端,服務端無法對其進行銷毀(可以設置過期時間)。

 

采用 token,可以自定義 加密、解密字符串的 規則,但是為了標准化,就得引入 JWT。

 

(4)解決方法四:
  使用 JWT 實現 token。
  服務器驗證數據通過后,將數據封裝成 json 對象並發送給用戶(token)。
  客戶端接收 JWT 后將其存儲 cookie 或者 localstorage、sessionstorage 中。
  下次請求時,可以將 cookie 作為 HTTP Header 數據發送或者 POST 請求主體數據發送。

 

2、簡單了解一下 JWT

(1)什么是 JWT?
  JWT(JSON Web Token),即使用 Json 數據作為 web 網絡層的令牌機制。是 Java 實現 token 的一種具體解決方案。
  JWT 可以使用 HMAC 算法或者是 RSA 的公私秘鑰對進行簽名,防止數據偽造。

(2)透明令牌 與 自包含令牌的區別:
  引用(透明)令牌(reference token):指令牌存儲的是數據標識符,數據內容存儲在其他地方。也即隨機生成一個 字符串(uuid 等)作為令牌,不清楚該令牌的具體含義,只有通過 字符串 訪問數據內容才能得到具體信息。可以類比為 session_id 的使用。
  自包含令牌(Self-contained token):指令牌存儲的是數據(必要且不隱私的數據),通過解析令牌即可得到相關數據。此處使用的 JWT 即為一種自包含令牌。

(3)JWT 優點:
  數據量小、簡潔,可以通過URL、 POST 參數、HTTP Header 發送,傳輸速度快。
  自包含了數據,解析字符串即可獲取想要的數據,避免與數據庫進行交互。
  token 以加密形式保存在客戶端,不需要保存在服務端,易於擴展。

(4)JWT 組成
  JWT 是一個很長的字符串,由三部分組成,並使用 點(.) 隔開。
  Header.Payload.Signature,即 JWT 頭.有效載荷.簽名。
Header:
  用於存儲 JWT 元數據,是一個 JSON 對象。
  其中 alg 表示加密算法(HS256、RS256)。typ 表示 token 類型。

{
  "alg": "HS256",
  "typ": "JWT"
}

【注:】
    HS256 指的是 HMAC SHA256(默認),一種對稱算法,采用同一個密鑰生成、驗證簽名。
    RS256 指的是 RSA SHA256,一種非對稱算法,采用私鑰生成簽名,用公鑰驗證簽名。

 

Payload:
  用於存放需要傳遞的數據(用戶信息)。
  其包含一些默認字段,也可以自定義字段(不建議存儲私密數據,易泄露)。

【默認字段:】
iss:發行人(JWT 生成的一方)
exp:過期時間(要大於 iat)
sub:主題
aud:用戶(接收 JWT 的一方)
nbf:在此時間之前 JWT 不可用
iat:JWT 發布時間
jti:JWT ID用於標識該JWT

 

Signature:
  用於存放簽名信息。
  指定一個 密碼(secret,不能公開給用戶,保存在服務端),按如下公式生成。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)

【理解:】
    對 header、payload 分別進行 Base64URL 加密,使用 點(.)連接。
    並根據 header 中指定的 加密算法,使用 secret 對數據再次加密。
注:
    由於 JWT 可以放在 URL 中(比如:/home?token=xxx),
    由於 Base64 中 =、+、/ 在 url 中有特殊含義,使用 base64 生成的 token 會出現問題。
    Base64url 對這些符號進行了轉換,(去掉 =,用 - 替換 +, 用 _ 替換 /).

 

3、SpringBoot 整合 JWT

(1)添加依賴

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

 

 

(2)創建一個工具類(JwtUtil.java)用於操作 JWT。

package com.lyh.admin_template.back.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * JWT 操作工具類
 */
public class JwtUtil {

    // 設置過期時間(15 分鍾)
    public static final long EXPIRE = 1000 * 60 * 15;
    // 設置 jwt 生成 secret(隨意指定)
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 生成 jwt token
     */
    public static String getJwtToken(String userId, String userName) {
        String JwtToken = Jwts.builder()
                // 設置 jwt 類型
                .setHeaderParam("typ", "JWT")
                // 設置 jwt 加密方法
                .setHeaderParam("alg", "HS256")
                // 設置 jwt 主題
                .setSubject("admin-user")
                // 設置 jwt 發布時間
                .setIssuedAt(new Date())
                // 設置 jwt 過期時間
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                // 設置自定義數據
                .claim("userId", userId)
                .claim("userName", userName)
                // 設置密鑰與算法
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                // 生成 token
                .compact();
        return JwtToken;
    }

    /**
     * 判斷token是否存在與有效,true 表示未過期,false 表示過期或不存在
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return false;
        }
        try {
            // 獲取 token 數據
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            // 判斷是否過期
            return claimsJws.getBody().getExpiration().after(new Date());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 判斷token是否存在與有效
     */
    public static boolean checkToken(HttpServletRequest request) {
        return checkToken(request.getHeader("token"));
    }

    /**
     * 根據 token 獲取數據
     */
    public static Claims getTokenBody(HttpServletRequest request) {
        return getTokenBody(request.getHeader("token"));
    }

    /**
     * 根據 token 獲取數據
     */
    public static Claims getTokenBody(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) {
            return null;
        }
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        return claimsJws.getBody();
    }
}

 

 

(3)編寫一個測試類(TestJWTController.java) ,用於測試

package com.lyh.admin_template.back.controller.test;

import com.lyh.admin_template.back.common.utils.JwtUtil;
import com.lyh.admin_template.back.common.utils.Result;
import io.jsonwebtoken.Claims;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test/jwt")
@RestController
@Api(tags = "測試 JWT")
public class TestJWTController {

    @ApiOperation(value = "獲取 token")
    @PostMapping("/getToken")
    public Result testJwt() {
        return Result.ok().data("token", JwtUtil.getJwtToken("1", "tom"));
    }

    @ApiOperation(value = "測試是否過期")
    @PostMapping("/testExpire")
    public Result testJwtExpire(String jwtToken) {
        if (JwtUtil.checkToken(jwtToken)) {
            Claims claims = JwtUtil.getTokenBody(jwtToken);
            return Result.ok().message("token 未過期").data("claims", claims);
        }
        return Result.ok().message("token 已過期");
    }
}

測試結果如下:
  定義過期時間為 30s,未過期時,返回 json 數據。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM