• 22-08-12 西安 尚医通(05) JWT令牌、阿里云发送短信验证码


    JWT令牌

    1、传统的用户身份验证

    1、tomcat为客户端创建session.

    2、客户端存储jsessionId的cookie

    3、登录后把userinfo存储到session中,以后就能根据这个来判断当前用户有没有登录

    缺点:分布式场景下,session不一致问题

    2种解决方式

    解决:引入redis,redis存储session,tomcat不再存储session。缺点使用了额外的技术

    解决:使用jwt令牌机制,户端自己存储身份信息,服务器不再保存会话数据。

    jwt令牌使用原理

    客户端每次发请求,都需要携带该令牌,服务端就会校验令牌,从中提取想要的用户信息(用户名、用户id...)


    2、JWT介绍

    JWT:Json Web Token,是基于Json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

    使用场景:认证

    使用起来就是,由服务端根据规范生成一个jwt令牌(token),并且发放给客户端。此时客户端请求服务端的时候就可以携带着令牌,以令牌来证明自己的身份信息

    JWT令牌的组成部分:

    (1)头部 header

    一个json字符串,存储当前令牌名称,以及加密算法,

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

    经过Base64编码,得到如下值

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

    (2)载荷 payload

    一个json字符串,存储一些自定义的信息

    {"sub":"1234567890","name":"John Doe","admin":true}

    经过Base64编码,得到如下值 

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

    (3)签名

    这个部分需要base64编码后的header和base64编码后的payload使用.(点)连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

    来自老师的灵魂画作:

    将这三部分用.(点)连接成一个完整的字符串,构成了最终的jwt令牌。

    老师的灵魂画作:

    注:base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息


    3、JWT的入门使用

    1.引入依赖

    1. <dependency>
    2. <groupId>io.jsonwebtokengroupId>
    3. <artifactId>jjwtartifactId>
    4. dependency>

    2.在util包下添加一个JWT工具类

    1. public class JwtHelper {
    2. //令牌过期时间
    3. private static long tokenExpiration = 24*60*60*1000;
    4. private static String tokenSignKey = "123456";
    5. public static String createToken(Long userId, String userName) {
    6. String token = Jwts.builder()
    7. .setSubject("YYGH-USER")
    8. .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
    9. .claim("userId", userId)
    10. .claim("userName", userName)
    11. .signWith(SignatureAlgorithm.HS512, tokenSignKey)
    12. .compressWith(CompressionCodecs.GZIP)
    13. .compact();
    14. return token;
    15. }
    16. public static Long getUserId(String token) {
    17. if(StringUtils.isEmpty(token)) return null;
    18. Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
    19. Claims claims = claimsJws.getBody();
    20. Integer userId = (Integer)claims.get("userId");
    21. return userId.longValue();
    22. }
    23. public static String getUserName(String token) {
    24. if(StringUtils.isEmpty(token)) return "";
    25. Jws claimsJws
    26. = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
    27. Claims claims = claimsJws.getBody();
    28. return (String)claims.get("userName");
    29. }
    30. public static void main(String[] args) {
    31. String token = JwtHelper.createToken(1L, "55");
    32. System.out.println(token);
    33. System.out.println(JwtHelper.getUserId(token));
    34. System.out.println(JwtHelper.getUserName(token));
    35. }
    36. }

    你可以用我写在工具类里的main方法,也可以自己去自定义测试

    运行该main方法,主要就是使用这三个方法。

     控制台打印结果如下:


    阿里云短信验证码

    【官方106三网短信】短信平台/短信免费试用/短信验证码/短信通知/短信群发推广—短信API接口对接【最新版】_数据API_最新活动_API-云市场-阿里云

    1、阿里云购买短信服务和API调试

    1、扫码登录(用支付宝)、亏快速的实名认证

    2、在在阿里云云市场搜索''短信API'', 

    使用官方推荐的短信平台:鼎信,第一次可以免费领取5条短信

    3、在云市场中可以查看已购买的短信服务

    4、网页调试

    这部分我没找到,,服了。不管是官方文档还是老师笔记,怎么都写的不清不楚的,这也是我最痛恨的地方了

     在加载出来的页面中的中部,选择去调试

    只需要在这里配置一下红色框圈住的这俩个参数

     接着在手机上就能收到这样的短信了


    2、用java代码实现发送短信验证码

    1、在网站是能看到示例代码的。直接复制走,稍微改一下就能用了

    2、引入依赖

    1. <dependencies>
    2. <dependency>
    3. <groupId>org.apache.httpcomponentsgroupId>
    4. <artifactId>httpclientartifactId>
    5. dependency>
    6. <dependency>
    7. <groupId>org.apache.httpcomponentsgroupId>
    8. <artifactId>httpcoreartifactId>
    9. dependency>
    10. <dependency>
    11. <groupId>org.springframework.bootgroupId>
    12. <artifactId>spring-boot-starter-data-redisartifactId>
    13. dependency>
    14. dependencies>

    3、util包下添加工具类 HttpUtils

    1. package com.atguigu.yygh.msm.util;
    2. import org.apache.commons.lang.StringUtils;
    3. import org.apache.http.HttpResponse;
    4. import org.apache.http.NameValuePair;
    5. import org.apache.http.client.HttpClient;
    6. import org.apache.http.client.entity.UrlEncodedFormEntity;
    7. import org.apache.http.client.methods.HttpDelete;
    8. import org.apache.http.client.methods.HttpGet;
    9. import org.apache.http.client.methods.HttpPost;
    10. import org.apache.http.client.methods.HttpPut;
    11. import org.apache.http.conn.ClientConnectionManager;
    12. import org.apache.http.conn.scheme.Scheme;
    13. import org.apache.http.conn.scheme.SchemeRegistry;
    14. import org.apache.http.conn.ssl.SSLSocketFactory;
    15. import org.apache.http.entity.ByteArrayEntity;
    16. import org.apache.http.entity.StringEntity;
    17. import org.apache.http.impl.client.DefaultHttpClient;
    18. import org.apache.http.message.BasicNameValuePair;
    19. import javax.net.ssl.SSLContext;
    20. import javax.net.ssl.TrustManager;
    21. import javax.net.ssl.X509TrustManager;
    22. import java.io.UnsupportedEncodingException;
    23. import java.net.URLEncoder;
    24. import java.security.KeyManagementException;
    25. import java.security.NoSuchAlgorithmException;
    26. import java.security.cert.X509Certificate;
    27. import java.util.ArrayList;
    28. import java.util.List;
    29. import java.util.Map;
    30. public class HttpUtils {
    31. /**
    32. * get
    33. *
    34. * @param host
    35. * @param path
    36. * @param method
    37. * @param headers
    38. * @param querys
    39. * @return
    40. * @throws Exception
    41. */
    42. public static HttpResponse doGet(String host, String path, String method,
    43. Map headers,
    44. Map querys)
    45. throws Exception {
    46. HttpClient httpClient = wrapClient(host);
    47. HttpGet request = new HttpGet(buildUrl(host, path, querys));
    48. for (Map.Entry e : headers.entrySet()) {
    49. request.addHeader(e.getKey(), e.getValue());
    50. }
    51. return httpClient.execute(request);
    52. }
    53. /**
    54. * post form
    55. *
    56. * @param host
    57. * @param path
    58. * @param method
    59. * @param headers
    60. * @param querys
    61. * @param bodys
    62. * @return
    63. * @throws Exception
    64. */
    65. public static HttpResponse doPost(String host, String path, String method,
    66. Map headers,
    67. Map querys,
    68. Map bodys)
    69. throws Exception {
    70. HttpClient httpClient = wrapClient(host);
    71. HttpPost request = new HttpPost(buildUrl(host, path, querys));
    72. for (Map.Entry e : headers.entrySet()) {
    73. request.addHeader(e.getKey(), e.getValue());
    74. }
    75. if (bodys != null) {
    76. List nameValuePairList = new ArrayList();
    77. for (String key : bodys.keySet()) {
    78. nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
    79. }
    80. UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
    81. formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
    82. request.setEntity(formEntity);
    83. }
    84. return httpClient.execute(request);
    85. }
    86. /**
    87. * Post String
    88. *
    89. * @param host
    90. * @param path
    91. * @param method
    92. * @param headers
    93. * @param querys
    94. * @param body
    95. * @return
    96. * @throws Exception
    97. */
    98. public static HttpResponse doPost(String host, String path, String method,
    99. Map headers,
    100. Map querys,
    101. String body)
    102. throws Exception {
    103. HttpClient httpClient = wrapClient(host);
    104. HttpPost request = new HttpPost(buildUrl(host, path, querys));
    105. for (Map.Entry e : headers.entrySet()) {
    106. request.addHeader(e.getKey(), e.getValue());
    107. }
    108. if (StringUtils.isNotBlank(body)) {
    109. request.setEntity(new StringEntity(body, "utf-8"));
    110. }
    111. return httpClient.execute(request);
    112. }
    113. /**
    114. * Post stream
    115. *
    116. * @param host
    117. * @param path
    118. * @param method
    119. * @param headers
    120. * @param querys
    121. * @param body
    122. * @return
    123. * @throws Exception
    124. */
    125. public static HttpResponse doPost(String host, String path, String method,
    126. Map headers,
    127. Map querys,
    128. byte[] body)
    129. throws Exception {
    130. HttpClient httpClient = wrapClient(host);
    131. HttpPost request = new HttpPost(buildUrl(host, path, querys));
    132. for (Map.Entry e : headers.entrySet()) {
    133. request.addHeader(e.getKey(), e.getValue());
    134. }
    135. if (body != null) {
    136. request.setEntity(new ByteArrayEntity(body));
    137. }
    138. return httpClient.execute(request);
    139. }
    140. /**
    141. * Put String
    142. * @param host
    143. * @param path
    144. * @param method
    145. * @param headers
    146. * @param querys
    147. * @param body
    148. * @return
    149. * @throws Exception
    150. */
    151. public static HttpResponse doPut(String host, String path, String method,
    152. Map headers,
    153. Map querys,
    154. String body)
    155. throws Exception {
    156. HttpClient httpClient = wrapClient(host);
    157. HttpPut request = new HttpPut(buildUrl(host, path, querys));
    158. for (Map.Entry e : headers.entrySet()) {
    159. request.addHeader(e.getKey(), e.getValue());
    160. }
    161. if (StringUtils.isNotBlank(body)) {
    162. request.setEntity(new StringEntity(body, "utf-8"));
    163. }
    164. return httpClient.execute(request);
    165. }
    166. /**
    167. * Put stream
    168. * @param host
    169. * @param path
    170. * @param method
    171. * @param headers
    172. * @param querys
    173. * @param body
    174. * @return
    175. * @throws Exception
    176. */
    177. public static HttpResponse doPut(String host, String path, String method,
    178. Map headers,
    179. Map querys,
    180. byte[] body)
    181. throws Exception {
    182. HttpClient httpClient = wrapClient(host);
    183. HttpPut request = new HttpPut(buildUrl(host, path, querys));
    184. for (Map.Entry e : headers.entrySet()) {
    185. request.addHeader(e.getKey(), e.getValue());
    186. }
    187. if (body != null) {
    188. request.setEntity(new ByteArrayEntity(body));
    189. }
    190. return httpClient.execute(request);
    191. }
    192. /**
    193. * Delete
    194. *
    195. * @param host
    196. * @param path
    197. * @param method
    198. * @param headers
    199. * @param querys
    200. * @return
    201. * @throws Exception
    202. */
    203. public static HttpResponse doDelete(String host, String path, String method,
    204. Map headers,
    205. Map querys)
    206. throws Exception {
    207. HttpClient httpClient = wrapClient(host);
    208. HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
    209. for (Map.Entry e : headers.entrySet()) {
    210. request.addHeader(e.getKey(), e.getValue());
    211. }
    212. return httpClient.execute(request);
    213. }
    214. private static String buildUrl(String host, String path, Map querys) throws UnsupportedEncodingException {
    215. StringBuilder sbUrl = new StringBuilder();
    216. sbUrl.append(host);
    217. if (!StringUtils.isBlank(path)) {
    218. sbUrl.append(path);
    219. }
    220. if (null != querys) {
    221. StringBuilder sbQuery = new StringBuilder();
    222. for (Map.Entry query : querys.entrySet()) {
    223. if (0 < sbQuery.length()) {
    224. sbQuery.append("&");
    225. }
    226. if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
    227. sbQuery.append(query.getValue());
    228. }
    229. if (!StringUtils.isBlank(query.getKey())) {
    230. sbQuery.append(query.getKey());
    231. if (!StringUtils.isBlank(query.getValue())) {
    232. sbQuery.append("=");
    233. sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
    234. }
    235. }
    236. }
    237. if (0 < sbQuery.length()) {
    238. sbUrl.append("?").append(sbQuery);
    239. }
    240. }
    241. return sbUrl.toString();
    242. }
    243. private static HttpClient wrapClient(String host) {
    244. HttpClient httpClient = new DefaultHttpClient();
    245. if (host.startsWith("https://")) {
    246. sslClient(httpClient);
    247. }
    248. return httpClient;
    249. }
    250. private static void sslClient(HttpClient httpClient) {
    251. try {
    252. SSLContext ctx = SSLContext.getInstance("TLS");
    253. X509TrustManager tm = new X509TrustManager() {
    254. public X509Certificate[] getAcceptedIssuers() {
    255. return null;
    256. }
    257. public void checkClientTrusted(X509Certificate[] xcs, String str) {
    258. }
    259. public void checkServerTrusted(X509Certificate[] xcs, String str) {
    260. }
    261. };
    262. ctx.init(null, new TrustManager[] { tm }, null);
    263. SSLSocketFactory ssf = new SSLSocketFactory(ctx);
    264. ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
    265. ClientConnectionManager ccm = httpClient.getConnectionManager();
    266. SchemeRegistry registry = ccm.getSchemeRegistry();
    267. registry.register(new Scheme("https", 443, ssf));
    268. } catch (KeyManagementException ex) {
    269. throw new RuntimeException(ex);
    270. } catch (NoSuchAlgorithmException ex) {
    271. throw new RuntimeException(ex);
    272. }
    273. }
    274. }

    4、写一个main方法测试一下短信服务

    1. public static void main(String[] args) {
    2. String host = "http://dingxin.market.alicloudapi.com";
    3. String path = "/dx/sendSms";
    4. String method = "POST";
    5. //只需要改你自己的appcode,我这里也随便写了
    6. String appcode = "f346ae38f2b84f6498XXXXXXXXXXX";
    7. Map headers = new HashMap();
    8. //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
    9. headers.put("Authorization", "APPCODE " + appcode);
    10. Map querys = new HashMap();
    11. //写你自己的手机号,我这里就随便写了
    12. querys.put("mobile", "1839XXXXXX");
    13. querys.put("param", "code:1234");
    14. querys.put("tpl_id", "TP1711063");
    15. Map bodys = new HashMap();
    16. try {
    17. /**
    18. * 重要提示如下:
    19. * HttpUtils请从
    20. * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
    21. * 下载
    22. *
    23. * 相应的依赖请参照
    24. * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
    25. */
    26. HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
    27. System.out.println(response.toString());
    28. //获取response的body
    29. //System.out.println(EntityUtils.toString(response.getEntity()));
    30. } catch (Exception e) {
    31. e.printStackTrace();
    32. }
    33. }

    3、实现完整的登录功能

    该部分,我们将使用JWT和阿里云短信验证完整登录过程的短信验证和返给前端token,实现用户登录认证

    关于redis的配置文件啥的就不多讲了

    整体思路:

    我们调用阿里云服务发送短信验证码,发送成功的话把验证码保存到redis中没设置5分钟的有效期。

    在这个页面中:点击获取验证码

    点击“获取验证码”完,按钮文字会变为“马上登录” ,输入框的提示也变为“请输入验证码” 

    “获取验证码”过程会调用如下接口:  @GetMapping(value = "/send/{phone}")

    把验证码存储到redis中一份,供一会做登录时的“验证码校验”。这里有个细节,RedisTemplate是有泛型的,当然也可以直接用StringRedisTemplate

    1. @Autowired
    2. private MsmService msmService;
    3. @Autowired
    4. private RedisTemplate redisTemplate;
    5. @GetMapping(value = "/send/{phone}")
    6. public R code(@PathVariable String phone) {
    7. //在有效期内,验证码不能重复发送
    8. String code = redisTemplate.opsForValue().get(phone);
    9. if(!StringUtils.isEmpty(code)) return R.ok();
    10. //生成验证码
    11. code = (long) (new Random().nextDouble() * 10000) +"";
    12. //调用方法发送
    13. boolean isSend = msmService.send(phone, code);
    14. if(isSend) {
    15. //发送成功,验证码放到redis,设置有效时间
    16. redisTemplate.opsForValue().set(phone, code,5, TimeUnit.MINUTES);
    17. return R.ok();
    18. } else {
    19. return R.error().message("发送短信失败");
    20. }
    21. }

    在service中实现了阿里云短信发送,实际上跟我们刚刚测试的短信发送一毛一样

    1. //发送验证码
    2. @Override
    3. public boolean send(String phone, String code) {
    4. String host = "http://dingxin.market.alicloudapi.com";
    5. String path = "/dx/sendSms";
    6. String method = "POST";
    7. String appcode = "f346ae38f2b84f649858630e32a79280";
    8. Map headers = new HashMap();
    9. //Authorization:APPCODE 83359fd73fe94948385f570e3c139105
    10. headers.put("Authorization", "APPCODE " + appcode);
    11. Map querys = new HashMap();
    12. querys.put("mobile", phone);
    13. querys.put("param", "code:" + code);
    14. querys.put("tpl_id", "TP1711063");
    15. Map bodys = new HashMap();
    16. try {
    17. HttpResponse response = HttpUtils.doPost(host, path, method, headers,
    18. querys, bodys);
    19. System.out.println(response.toString());
    20. return true;
    21. } catch (Exception e) {
    22. e.printStackTrace();
    23. return false;
    24. }
    25. }

    2、接着输入完验证码,点击马上登录,按钮文字会变为“正在提交”,

    “马上登录”调用的接口是   @PostMapping("login")

    controller层

    1. @ApiOperation(value = "会员登录")
    2. @PostMapping("login")
    3. public R login(@RequestBody LoginVo loginVo) {
    4. Map info = userInfoService.login(loginVo);
    5. return R.ok().data(info);
    6. }

    service实现类层,校验验证码以及生成JWT令牌返回给前端

    1. @Service
    2. public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService {
    3. @Autowired
    4. private RedisTemplate redisTemplate;
    5. @Override
    6. public Map login(LoginVo loginVo) {
    7. //1.校验手机号和验证码不能为空
    8. String phone = loginVo.getPhone();
    9. String code = loginVo.getCode();
    10. //校验参数
    11. if(StringUtils.isEmpty(phone) ||
    12. StringUtils.isEmpty(code)) {
    13. throw new YyghException("数据为空",20001);
    14. }
    15. //2.从redis中获取验证码,校验验证码
    16. String mobileCode = redisTemplate.opsForValue().get(phone);
    17. if(!code.equals(mobileCode)) {
    18. throw new YyghException("验证码失败",20001);
    19. }
    20. //3.用手机号去查询这个用户
    21. QueryWrapper queryWrapper = new QueryWrapper<>();
    22. queryWrapper.eq("phone",phone);
    23. UserInfo userInfo = baseMapper.selectOne(queryWrapper);
    24. //4.判断是注册还是登录
    25. if (userInfo==null) {
    26. //注册
    27. userInfo = new UserInfo();
    28. userInfo.setPhone(loginVo.getPhone());
    29. userInfo.setName("");//用户真实姓名
    30. userInfo.setStatus(1);//这行很有必要,只有主键才自动回填
    31. this.save(userInfo);
    32. }
    33. //5.校验是否被禁用
    34. if(userInfo.getStatus() == 0) {
    35. throw new YyghException("用户已经禁用",20001);
    36. }
    37. //6.给前端返回一个map,包含name和token令牌
    38. Map map = new HashMap<>();
    39. String name=userInfo.getName();
    40. if (StringUtils.isEmpty(name)) {
    41. name=userInfo.getNickName();
    42. if(StringUtils.isEmpty(name)){
    43. name=userInfo.getPhone();
    44. }
    45. }
    46. String token = JwtHelper.createToken(userInfo.getId(), userInfo.getName());
    47. // JwtHelper.createToken(userInfo.getId(),name);
    48. map.put("name",name);
    49. map.put("token",token);
    50. return map;
    51. }
    52. }

     一会就会重新加载页面,并把后端返回的name和token放在cookie中。 

    如果使用postman测试,参考如下

    1.先调用发送验证码的服务,此时就会在redis中存储键为手机号码,值为验证码的k-v键值对

    2.再调用登录的空接口

    1.发送错误的验证码,返回验证码失败

      2、发送正确的验证码,拿到令牌

  • 相关阅读:
    JavaScript(一)
    静电消除自动清洁离子风机
    云起云落:揭秘云计算基础内功心法
    自助报表和 BI 能解决多少事?
    Nginx代理websocket配置(解决websocket异常断开连接tcp连接不断问题)
    哪个品种能够叫板白银现货t+d?
    HashMap源码分析(二)
    【MySQL】(七)SQL约束——主键约束、非空约束、唯一约束、默认值约束、外键约束
    数据结构--------排序方法
    C++ 并发编程指南(8)线程间通信
  • 原文地址:https://blog.csdn.net/m0_56799642/article/details/126309118