3.6.2、SSO短信接口服务
3.6.2.1、mock接口
地址:https://mock-java.itheima.net/project/35/interface/api/581
3.6.2.2、编写接口服务
编写ErrorResult,ErrorResult对象是与前端约定好的结构,如果发生错误需要返回该对象,如果未发生错误响应200即可。
1. package com.tanhua.sso.vo; 2. 3. import lombok.Builder; 4. import lombok.Data; 5. 6. @Data 7. @Builder 8. public class ErrorResult { 9. 10. private String errCode; 11. private String errMessage; 12. }
SmsController:
1. package com.tanhua.sso.controller; 2. 3. import com.tanhua.sso.service.SmsService; 4. import com.tanhua.sso.vo.ErrorResult; 5. import lombok.extern.slf4j.Slf4j; 6. import org.springframework.beans.factory.annotation.Autowired; 7. import org.springframework.http.HttpStatus; 8. import org.springframework.http.ResponseEntity; 9. import org.springframework.web.bind.annotation.PostMapping; 10. import org.springframework.web.bind.annotation.RequestBody; 11. import org.springframework.web.bind.annotation.RequestMapping; 12. import org.springframework.web.bind.annotation.RestController; 13. 14. import java.util.Map; 15. 16. @RestController 17. @RequestMapping("user") 18. @Slf4j 19. public class SmsController { 20. 21. @Autowired 22. private SmsService smsService; 23. 24. /** 25. * 发送短信验证码接口 26. * 27. * @param param 28. * @return 29. */ 30. @PostMapping("login") 31. public ResponseEntity<ErrorResult> sendCheckCode(@RequestBody Map<String, String> param) { 32. ErrorResult errorResult = null; 33. String phone = param.get("phone"); 34. try { 35. errorResult = this.smsService.sendCheckCode(phone); 36. if (null == errorResult) { 37. return ResponseEntity.ok(null); 38. } 39. } catch (Exception e) { 40. log.error("发送短信验证码失败~ phone = " + phone, e); 41. errorResult = ErrorResult.builder().errCode("000002").errMessage("短信验证码发送失败!").build(); 42. } 43. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult); 44. } 45. 46. }
SmsService:
1. package com.tanhua.sso.service; 2. 3. import cn.hutool.core.util.RandomUtil; 4. import cn.hutool.core.util.StrUtil; 5. import com.aliyun.dysmsapi20170525.Client; 6. import com.aliyun.dysmsapi20170525.models.SendSmsRequest; 7. import com.aliyun.dysmsapi20170525.models.SendSmsResponse; 8. import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody; 9. import com.aliyun.teaopenapi.models.Config; 10. import com.tanhua.sso.config.AliyunSMSConfig; 11. import com.tanhua.sso.vo.ErrorResult; 12. import lombok.extern.slf4j.Slf4j; 13. import org.springframework.beans.factory.annotation.Autowired; 14. import org.springframework.data.redis.core.RedisTemplate; 15. import org.springframework.stereotype.Service; 16. 17. import java.time.Duration; 18. 19. @Service 20. @Slf4j 21. public class SmsService { 22. 23. @Autowired 24. private AliyunSMSConfig aliyunSMSConfig; 25. 26. @Autowired 27. private RedisTemplate<String, String> redisTemplate; 28. 29. /** 30. * 发送短信验证码 31. * 32. * @param mobile 33. * @return 34. */ 35. public String sendSms(String mobile) { 36. //随机生成6位数字验证码 37. String code = RandomUtil.randomNumbers(6); 38. try { 39. Config config = new Config() 40. .setAccessKeyId(this.aliyunSMSConfig.getAccessKeyId()) 41. .setAccessKeySecret(this.aliyunSMSConfig.getAccessKeySecret()) 42. .setEndpoint(this.aliyunSMSConfig.getDomain()); 43. 44. Client client = new Client(config); 45. SendSmsRequest sendSmsRequest = new SendSmsRequest() 46. .setPhoneNumbers(mobile)//目标手机号 47. .setSignName(this.aliyunSMSConfig.getSignName()) //签名名称 48. .setTemplateCode(this.aliyunSMSConfig.getTemplateCode()) //短信模板code 49. .setTemplateParam("{\"code\":\"" + code + "\"}"); //模板中变量替换 50. SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest); 51. SendSmsResponseBody body = sendSmsResponse.getBody(); 52. if (StrUtil.equals("OK", body.getCode())) { 53. return code; 54. } 55. } catch (Exception e) { 56. log.error("发送短信验证码失败!" + mobile, e); 57. } 58. return null; 59. } 60. 61. /** 62. * 发送短信验证码 63. * 实现:发送完成短信验证码后,需要将验证码保存到redis中 64. * @param phone 65. * @return 66. */ 67. public ErrorResult sendCheckCode(String phone) { 68. String redisKey = "CHECK_CODE_" + phone; 69. 70. //先判断该手机号发送的验证码是否还未失效 71. if(this.redisTemplate.hasKey(redisKey)){ 72. String msg = "上一次发送的验证码还未失效!"; 73. return ErrorResult.builder().errCode("000001").errMessage(msg).build(); 74. } 75. 76. String code = this.sendSms(phone); 77. if(StrUtil.isEmpty(code)){ 78. String msg = "发送短信验证码失败!"; 79. return ErrorResult.builder().errCode("000000").errMessage(msg).build(); 80. } 81. 82. //短信发送成功,将验证码保存到redis中,有效期为5分钟 83. this.redisTemplate.opsForValue().set(redisKey, code, Duration.ofMinutes(5)); 84. 85. return null; 86. } 87. }
7、用户登录
7.1、登录验证码
7.1.1、接口说明
参见YAPI接口地址:http://192.168.136.160:3000/project/19/interface/api/94
7.1.2、流程分析
客户端发送请求
服务端调用第三方组件发送验证码
验证码发送成功,存入redis
响应客户端,客户端跳转到输入验证码页面
7.1.3、代码实现
LoginController
1. @RestController 2. @RequestMapping("/user") 3. public class LoginController { 4. 5. @Autowired 6. private UserService userService; 7. 8. /** 9. * 获取登录验证码 10. * 请求参数:phone (Map) 11. * 响应:void 12. */ 13. @PostMapping("/login") 14. public ResponseEntity login(@RequestBody Map map){ 15. String phone =(String) map.get("phone"); 16. userService.sendMsg(phone); 17. return ResponseEntity.ok(null); //正常返回状态码200 18. } 19. }
UserService
1. @Service 2. public class UserService { 3. 4. @Autowired 5. private SmsTemplate template; 6. 7. @Autowired 8. private RedisTemplate<String,String> redisTemplate; 9. 10. /** 11. * 发送短信验证码 12. * @param phone 13. */ 14. public void sendMsg(String phone) { 15. //1、随机生成6位数字 16. //String code = RandomStringUtils.randomNumeric(6); 17. String code = "123456"; 18. //2、调用template对象,发送手机短信 19. //template.sendSms(phone,code); 20. //3、将验证码存入到redis 21. redisTemplate.opsForValue().set("CHECK_CODE_"+phone,code, Duration.ofMinutes(5)); 22. } 23. }
7.2、JWT
7.2.1、简介
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全
7.2.2、格式
- JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
- A由JWT头部信息header加密得到
- B由JWT用到的身份验证信息json数据加密得到
- C由A和B加密得到,是校验部分
7.2.3、流程
7.2.4、示例
导入依赖:
1. <dependency> 2. <groupId>io.jsonwebtoken</groupId> 3. <artifactId>jjwt</artifactId> 4. <version>0.9.1</version> 5. </dependency>
编写测试用例:
1. @Test 2. public void testCreateToken() { 3. //生成token 4. //1、准备数据 5. Map map = new HashMap(); 6. map.put("id",1); 7. map.put("mobile","13800138000"); 8. //2、使用JWT的工具类生成token 9. long now = System.currentTimeMillis(); 10. String token = Jwts.builder() 11. .signWith(SignatureAlgorithm.HS512, "itcast") //指定加密算法 12. .setClaims(map) //写入数据 13. .setExpiration(new Date(now + 30000)) //失效时间 14. .compact(); 15. System.out.println(token); 16. } 17. 18. //解析token 19. 20. /** 21. * SignatureException : token不合法 22. * ExpiredJwtException:token已过期 23. */ 24. @Test 25. public void testParseToken() { 26. String token = "eyJhbGciOiJIUzUxMiJ9.eyJtb2JpbGUiOiIxMzgwMDEzODAwMCIsImlkIjoxLCJleHAiOjE2MTgzOTcxOTV9.2lQiovogL5tJa0px4NC-DW7zwHFqZuwhnL0HPAZunieGphqnMPduMZ5TtH_mxDrgfiskyAP63d8wzfwAj-MIVw"; 27. try { 28. Claims claims = Jwts.parser() 29. .setSigningKey("itcast") 30. .parseClaimsJws(token) 31. .getBody(); 32. Object id = claims.get("id"); 33. Object mobile = claims.get("mobile"); 34. System.out.println(id + "--" + mobile); 35. }catch (ExpiredJwtException e) { 36. System.out.println("token已过期"); 37. }catch (SignatureException e) { 38. System.out.println("token不合法"); 39. } 40. 41. }
通过解析Token得知,如果抛出SignatureException异常表示token不合法,如果抛出ExpiredJwtException异常表示token已过期
7.2.5 JWT工具类
1. public class JwtUtils { 2. 3. // TOKEN的有效期1小时(S) 4. private static final int TOKEN_TIME_OUT = 1 * 3600; 5. 6. // 加密KEY 7. private static final String TOKEN_SECRET = "itcast"; 8. 9. 10. // 生成Token 11. public static String getToken(Map params){ 12. long currentTime = System.currentTimeMillis(); 13. return Jwts.builder() 14. .signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) //加密方式 15. .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳 16. .addClaims(params) 17. .compact(); 18. } 19. 20. 21. /** 22. * 获取Token中的claims信息 23. */ 24. public static Claims getClaims(String token) { 25. return Jwts.parser() 26. .setSigningKey(TOKEN_SECRET) 27. .parseClaimsJws(token).getBody(); 28. } 29. 30. 31. /** 32. * 是否有效 true-有效,false-失效 33. */ 34. public static boolean verifyToken(String token) { 35. 36. if(StringUtils.isEmpty(token)) { 37. return false; 38. } 39. 40. try { 41. Claims claims = Jwts.parser() 42. .setSigningKey("itcast") 43. .parseClaimsJws(token) 44. .getBody(); 45. }catch (Exception e) { 46. return false; 47. } 48. 49. return true; 50. } 51. }
7.3、用户登录
用户接收到验证码后,进行输入验证码,点击登录,前端系统将手机号以及验证码提交到服务端进行校验。
7.3.1、接口文档
YAPI接口地址:https://mock-java.itheima.net/project/164/interface/api/12593
7.3.2、LoginController
1. /** 2. * 检验登录 3. */ 4. @PostMapping("/loginVerification") 5. public ResponseEntity loginVerification(@RequestBody Map map) { 6. //1、调用map集合获取请求参数 7. String phone = (String) map.get("phone"); 8. String code = (String) map.get("verificationCode"); 9. //2、调用userService完成用户登录 10. Map retMap = userService.loginVerification(phone,code); 11. //3、构造返回 12. return ResponseEntity.ok(retMap); 13. }
7.3.3、UserService
1. /** 2. * 验证登录 3. * @param phone 4. * @param code 5. */ 6. public Map loginVerification(String phone, String code) { 7. //1、从redis中获取下发的验证码 8. String redisCode = redisTemplate.opsForValue().get("CHECK_CODE_" + phone); 9. //2、对验证码进行校验(验证码是否存在,是否和输入的验证码一致) 10. if(StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) { 11. //验证码无效 12. throw new RuntimeException(); 13. } 14. //3、删除redis中的验证码 15. redisTemplate.delete("CHECK_CODE_" + phone); 16. //4、通过手机号码查询用户 17. User user = userApi.findByMobile(phone); 18. boolean isNew = false; 19. //5、如果用户不存在,创建用户保存到数据库中 20. if(user == null) { 21. user = new User(); 22. user.setMobile(phone); 23. user.setPassword(DigestUtils.md5Hex("123456")); 24. Long userId = userApi.save(user); 25. user.setId(userId); 26. isNew = true; 27. } 28. //6、通过JWT生成token(存入id和手机号码) 29. Map tokenMap = new HashMap(); 30. tokenMap.put("id",user.getId()); 31. tokenMap.put("mobile",phone); 32. String token = JwtUtils.getToken(tokenMap); 33. //7、构造返回值 34. Map retMap = new HashMap(); 35. retMap.put("token",token); 36. retMap.put("isNew",isNew); 37. 38. return retMap; 39. }
7.3.4、测试
8、代码优化
8.1 抽取BasePojo
为了简化实体类中created和updated字段,抽取BasePojo
1. @Data 2. public abstract class BasePojo implements Serializable { 3. 4. @TableField(fill = FieldFill.INSERT) //自动填充 5. private Date created; 6. @TableField(fill = FieldFill.INSERT_UPDATE) 7. private Date updated; 8. 9. }
8.2 自动填充
对于created和updated字段,每次操作都需要手动设置。为了解决这个问题,mybatis-plus支持自定义处理器的形式实现保存更新的自动填充
1. ```java 2. package com.tanhua.dubbo.server.handler; 3. 4. import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; 5. import org.apache.ibatis.reflection.MetaObject; 6. import org.springframework.stereotype.Component; 7. 8. import java.util.Date; 9. 10. @Component 11. public class MyMetaObjectHandler implements MetaObjectHandler { 12. 13. @Override 14. public void insertFill(MetaObject metaObject) { 15. Object created = getFieldValByName("created", metaObject); 16. if (null == created) { 17. //字段为空,可以进行填充 18. setFieldValByName("created", new Date(), metaObject); 19. } 20. 21. Object updated = getFieldValByName("updated", metaObject); 22. if (null == updated) { 23. //字段为空,可以进行填充 24. setFieldValByName("updated", new Date(), metaObject); 25. } 26. } 27. 28. @Override 29. public void updateFill(MetaObject metaObject) { 30. //更新数据时,直接更新字段 31. setFieldValByName("updated", new Date(), metaObject); 32. } 33. } 34.