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 数据。

 


作者:累成一条狗,发布于:2020/06/30
原文:https://www.cnblogs.com/l-y-h/p/13214493.html