第二课 Spring Cloud分布式微服务实战-开发通行证服务

本文主要是介绍第二课 Spring Cloud分布式微服务实战-开发通行证服务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第二课 Spring Cloud分布式微服务实战-开发通行证服务

tags:

  • Java
  • 慕课网

categories:

  • 短信发送
  • 注册登录

文章目录

  • 第二课 Spring Cloud分布式微服务实战-开发通行证服务
    • 第一节 短信注册配置和环境
      • 1.1 短信登录注册涉及内容
      • 1.2 配置密钥和资源文件(阿里云)
      • 1.3 整合发送短信
      • 1.4 发送短信测试一下
      • 1.5 redis环境配置操作类
    • 第二节 短信注册接口完善
      • 2.1 获取客户端ip和验证码存储
      • 2.2 拦截器限制发送频率
      • 2.3 自定义异常返回错误信息
    • 第三节 短信登录接口
      • 3.1 验证BO信息
      • 3.2 查询老用户和新用户添加
      • 3.3 设置会话和cookie信息
      • 3.4 资源属性与常量绑定
    • 第四节 用户信息完善接口
      • 4.1 展示和更新用户账户信息
      • 4.2 展示和缓储用户基本信息
      • 4.3 缓储数据双写一致
      • 4.4 用户会话拦截器
      • 4.5 用户状态拦截器
    • 第五节 AOP警告日志监控和sql打印
      • 5.1 AOP切面完成统计实现类中函数执行的时间
      • 5.2 开启mybatis的日志打印
      • 5.3 退出会话和注销会话

第一节 短信注册配置和环境

1.1 短信登录注册涉及内容

  1. 短信登录注册

  2. 短信验证码发送与限制

  3. 分布式会话 redis实现

  4. 用户信息完善,OSS/FastDFS文件上传

  5. AOP日志监控。通过AOP把常用日志输出如:MyBaits日志

  6. 短信发送验证码流程
    在这里插入图片描述

  7. 短信一键登录注册流程
    在这里插入图片描述

1.2 配置密钥和资源文件(阿里云)

  1. 阿里云官网直接搜索短信服务
  2. imooc-news-dev-common下创建aliyun.properties文件。
aliyun.accessKeyID=XXXXXXXXXXXXXXXXX
aliyun.accessKeySecret=XXXXXXXXXXXXXX
  1. spring boot相关的依赖从imooc-news-service-api移动到imooc-news-dev-common中去。让这个模块可以使用springboot的容器。添加阿里云短信接口的依赖
    • 这里注意:一般第三方的库(不怎么变动),直接放到common模块中管理或者顶级工程中管理都是可以的。
        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency><!-- 添加阿里云短信第三方云厂商相关依赖--><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.16</version></dependency>
  1. imooc-news-dev-common写一个类读取配置文件中的内容。java文件夹下创建com.imooc.utils.extend.AliyunResource
package com.imooc.utils.extend;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;@Component
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun")
public class AliyunResource {private String accessKeyID;private String accessKeySecret;public String getAccessKeyID() {return accessKeyID;}public void setAccessKeyID(String accessKeyID) {this.accessKeyID = accessKeyID;}public String getAccessKeySecret() {return accessKeySecret;}public void setAccessKeySecret(String accessKeySecret) {this.accessKeySecret = accessKeySecret;}
}

1.3 整合发送短信

  1. imooc-news-dev-common中新建文件com.imooc.utils.SMSUtils。根据阿里云的模板引入发送接口。
package com.imooc.utils;import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.imooc.utils.extend.AliyunResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class SMSUtils {@Autowiredpublic AliyunResource aliyunResource;public void sendSMS(String mobile, String code){DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", aliyunResource.getAccessKeyID(), aliyunResource.getAccessKeySecret());/** use STS TokenDefaultProfile profile = DefaultProfile.getProfile("<your-region-id>",           // The region ID"<your-access-key-id>",       // The AccessKey ID of the RAM account"<your-access-key-secret>",   // The AccessKey Secret of the RAM account"<your-sts-token>");          // STS Token**/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("PhoneNumbers", mobile);request.putQueryParameter("SignName", "XXX");request.putQueryParameter("TemplateCode", "XXX");request.putQueryParameter("TemplateParam", "{\"code\": \"" + code + "\"}");try {CommonResponse response = client.getCommonResponse(request);System.out.println(response.getData());} catch (ServerException e) {e.printStackTrace();} catch (ClientException e) {e.printStackTrace();}}
}

1.4 发送短信测试一下

  1. imooc-news-service-api中创建接口类com.imooc.api.controller.user.PassportControllerApi
package com.imooc.api.controller.user;import com.imooc.grace.result.GraceJSONResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;@Api(value = "用户注册登录", tags = {"用户注册登陆的controller"})
public interface PassportControllerApi {@ApiOperation(value = "获得短信验证码", notes = "获得短信验证码", httpMethod = "GET")@GetMapping("/getSMSCode")public GraceJSONResult getSMSCode();
}
  1. imooc-news-dev-service-user实现接口调用短信发送函数。com.imooc.user.controller.PassportController
package com.imooc.user.controller;import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.grace.result.GraceJSONResult;
import com.imooc.utils.SMSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;@RestController
public class PassportController implements PassportControllerApi {final static Logger logger = LoggerFactory.getLogger(PassportController.class);@Autowiredprivate SMSUtils smsUtils;@Overridepublic GraceJSONResult getSMSCode() {String random = "123456";smsUtils.sendSMS("XXXXXXXXXX", random);return GraceJSONResult.ok();}
}

1.5 redis环境配置操作类

  1. 安装redis。找到封装redis的操作类复制到imooc-news-dev-common中的com.imooc.utils.RedisOperator
  2. imooc-news-dev-common中引入redis依赖和上面操作类需要的相关依赖进来。
        <!-- 引入 redis 依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><!--<version>2.1.5.RELEASE</version>--></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId></dependency><!-- jackson --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency><!-- apache 工具类 --><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId></dependency><!-- google 工具类 --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId></dependency><!-- joda-time 时间工具 --><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency>
  1. imooc-news-dev-service-user中添加redis相关配置。
spring:redis:database: 0host: 192.168.242.163#password: "123456"port: 6379
  1. 添加一个测试接口,这里不放到api中了,只是测试使用。
	@Autowiredprivate RedisOperator redis;@GetMapping("/redis")public Object redis() {redis.set("age", "18");return GraceJSONResult.ok(redis.get("age"));}
  1. 运行访问。http://127.0.0.1:8003/doc.html

第二节 短信注册接口完善

2.1 获取客户端ip和验证码存储

  1. 写一个获取客户端IP的工具类com.imooc.utils.IPUtil。放到imooc-news-dev-common中。
  2. imooc-news-service-api中定义一个com.imooc.api.BaseController装载redis,把一些redis相关的定义写道其中。减少代码冗余。
package com.imooc.api;import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;public class BaseController {@Autowiredpublic RedisOperator redis;public static final String MOBILE_SMSCODE = "mobile:smscode";}
  1. imooc-news-service-api修改PassportControllerApi加上参数电话和客户端地址和二级路径passport.
@Api(value = "用户注册登录", tags = {"用户注册登陆的controller"})
@RequestMapping("passport")
public interface PassportControllerApi {@ApiOperation(value = "获得短信验证码", notes = "获得短信验证码", httpMethod = "GET")@GetMapping("/getSMSCode")public GraceJSONResult getSMSCode(@RequestParam String mobile, HttpServletRequest request);
}
  1. 修改imooc-news-dev-service-usercom.imooc.user.controller.PassportController。继承BaseController, 生成随机验证码, 把验证码存入redis.
@RestController
public class PassportController extends BaseController implements PassportControllerApi {final static Logger logger = LoggerFactory.getLogger(PassportController.class);@Autowiredprivate SMSUtils smsUtils;@Overridepublic GraceJSONResult getSMSCode(String mobile, HttpServletRequest request) {// 获取用户IPString userIP = IPUtil.getRequestIp(request);// 根据用户的ip进行限制,限制用户在60秒内只能获得一次验证码redis.setnx60s(MOBILE_SMSCODE + ":" + userIP, userIP);// 生成随机验证码并且发送短信String random = (int)((Math.random() * 9 + 1) * 100000) + "" ;smsUtils.sendSMS(mobile, random);// 把验证码存入redis,用于后续进行验证 30分钟redis.set(MOBILE_SMSCODE + ":" + mobile, random, 30 * 60);return GraceJSONResult.ok(random);}
}
  1. 运行和前端联调。imooc-news-service-api中添加com.imooc.api.config.CorsConfig工具类解决跨站访问请求的问题。
  2. 服务在本人电脑上运行,前端在服务器nginx上运行。使用nginx方法代理本地8003到虚拟机的8003。
upstream user.imooc.com.cn{server 192.168.0.111:8003;}server {listen       9090;server_name  localhost;location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}server {listen       8003;server_name  localhost;location / {proxy_redirect off;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;#proxy_set_header X-Forwarded-For $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_pass http://user.imooc.com.cn/;}}

2.2 拦截器限制发送频率

  1. imooc-news-service-api中添加拦截器com.imooc.api.interceptors.PassportInterceptor
package com.imooc.api.interceptors;import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class PassportInterceptor implements HandlerInterceptor {@Autowiredpublic RedisOperator redis;public static final String MOBILE_SMSCODE = "mobile:smscode";/*** 拦截请求,访问controller之前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获得用户ipString userIp = IPUtil.getRequestIp(request);boolean keyIsExist = redis.keyIsExist(MOBILE_SMSCODE + ":" + userIp);if (keyIsExist) {System.out.println("短信发送频率太大!");return false;}/*** false:请求被拦截* true:请求通过验证,放行*/return true;}/*** 请求访问到controller之后,渲染视图之前* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}/*** 请求访问到controller之后,渲染视图之后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
  1. 把拦截器配置到容器中。imooc-news-service-api中创建om.imooc.api.config.InterceptorConfig
package com.imooc.api.config;import com.imooc.api.controller.user.PassportControllerApi;
import com.imooc.api.interceptors.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Beanpublic PassportInterceptor passportInterceptor() {return new PassportInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(passportInterceptor()).addPathPatterns("/passport/getSMSCode");}
}
  1. 重新编译测试。看下点击两此发送验证码,会不会输出频率过高的提示。

2.3 自定义异常返回错误信息

  1. imooc-news-dev-common中创建文件夹exception然后创建类com.imooc.exception.GraceException
package com.imooc.exception;import com.imooc.grace.result.ResponseStatusEnum;/*** 优雅的处理异常,统一封装*/
public class GraceException {public static void display(ResponseStatusEnum responseStatusEnum) {throw new MyCustomException(responseStatusEnum);}}
  1. imooc-news-dev-common中创建com.imooc.exception.MyCustomException.
package com.imooc.exception;import com.imooc.grace.result.ResponseStatusEnum;/*** 自定义异常* 目的:统一处理异常信息*      便于解耦,service与controller错误的解耦,不会被service返回的类型而限制*/
public class MyCustomException extends RuntimeException {private ResponseStatusEnum responseStatusEnum;public MyCustomException(ResponseStatusEnum responseStatusEnum) {super("异常状态码为:" + responseStatusEnum.status()+ ";具体异常信息为:" + responseStatusEnum.msg());this.responseStatusEnum = responseStatusEnum;}public ResponseStatusEnum getResponseStatusEnum() {return responseStatusEnum;}public void setResponseStatusEnum(ResponseStatusEnum responseStatusEnum) {this.responseStatusEnum = responseStatusEnum;}
}
  1. imooc-news-dev-common中创建com.imooc.exception.GraceExceptionHandler用来拦截MyCustomException抛出的异常信息,返回json数据给前端。@ControllerAdvice也是一种AOP,一种切面的类型。
package com.imooc.exception;import com.imooc.grace.result.GraceJSONResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;/*** 统一异常拦截处理* 可以针对异常的类型进行捕获,然后返回json信息到前端*/
@ControllerAdvice
public class GraceExceptionHandler {@ExceptionHandler(MyCustomException.class)@ResponseBodypublic GraceJSONResult returnMyException(MyCustomException e) {e.printStackTrace();return GraceJSONResult.exception(e.getResponseStatusEnum());}
}
  1. 改写imooc-news-service-apicom.imooc.api.interceptors.PassportInterceptor的代码端。
        if (keyIsExist) {GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);//System.out.println("短信发送频率太大!");return false;}
  1. 运行测试。

第三节 短信登录接口

3.1 验证BO信息

  1. BO是从视图层传过来,又称Bussiness Object针对业务方面进行处理的。
  2. imooc-news-dev-model中创建BO对象。先创建文件夹com.imooc.pojo.bo,创建类com.imooc.pojo.bo.RegistLoginBO.
    • 这里有的公司用Lombok插件。简洁。
    • 有的公司不用,因为和一些第三方库结合使用时候可能有一些小bug.
package com.imooc.pojo.bo;import javax.validation.constraints.NotNull;public class RegistLoginBO {// @NotNull不为空的校验// @NotBlank 同时校验null 和 空是这种""@NotBlank(message = "手机号不能为空")private String mobile;@NotBlank(message = "短信验证码不能为空")private String smsCode;public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}public String getSmsCode() {return smsCode;}public void setSmsCode(String smsCode) {this.smsCode = smsCode;}@Overridepublic String toString() {return "RegistLoginBO{" +"mobile='" + mobile + '\'' +", smsCode='" + smsCode + '\'' +'}';}
}
  1. com.imooc.api.controller.user.PassportControllerApi接口中创建一个登陆方法。
    // BindingResult result 是验证的结果// @Valid 用于做验证的// @RequestBody 表示后端对象和前端json是对应的 **如果不加 数据是获得不了的**@ApiOperation(value = "一键注册登录接口", notes = "一键注册登录接口", httpMethod = "POST")@PostMapping("/doLogin")public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO, BindingResult result);
  1. com.imooc.user.controller.PassportController中去实现上面方法。CTRL + I快捷键
    @Overridepublic GraceJSONResult doLogin(@Valid RegistLoginBO registLoginBO, BindingResult result) {// 1. 判断 BindingResult中是否保存了错误的验证信息,如果有 需要返回if (result.hasErrors()){Map<String, String> map = getErrors(result);return GraceJSONResult.errorMap(map);}// 2. 校验验证码是否匹配 StringUtils是org.apache.commons.lang3.StringUtilsString mobile = registLoginBO.getMobile();String smsCode = registLoginBO.getSmsCode();String redisSMSCode = redis.get(MOBILE_SMSCODE + ":" + mobile);if (StringUtils.isBlank(redisSMSCode) || !redisSMSCode.equalsIgnoreCase(smsCode)) {return GraceJSONResult.errorCustom(ResponseStatusEnum.SMS_CODE_ERROR);}return GraceJSONResult.ok();}
  1. imooc-news-service-apicom.imooc.api.BaseController添加一个常用的函数,返回登录中的错误信息。
    /*** BO中的错误信息* @param result*/public Map<String, String> getErrors(BindingResult result){Map<String, String> map = new HashMap<>();List<FieldError> errorList = result.getFieldErrors();for (FieldError error : errorList){String field = error.getField(); // 验证错误时 对应的属性String msg = error.getDefaultMessage(); // 验证错误时 对应的信息map.put(field, msg);}return map;}
  1. 运行测试。http://127.0.0.1:8003/doc.html

3.2 查询老用户和新用户添加

  1. imooc-news-dev-service-user创建包com.imooc.user.service,然后创建com.imooc.user.service.UserService接口。
package com.imooc.user.service;import com.imooc.pojo.AppUser;public interface UserService {/***  判断用户是否存在, 如果存在返回user信息*/public AppUser queryMobileIsExist(String mobile);/***  创建用户新增用户到数据库*/public AppUser createUser(String mobile);
}
  1. imooc-news-dev-service-user创建包com.imooc.user.service.impl,然后创建com.imooc.user.service.impl.UserServiceImpl实现类。这里common中导入了一些工具类
    • org.n3r.idworker.Sid 用来生成全局唯一的id的,这里需要注册到容器中。@ComponentScan(basePackages = {"com.imooc", "org.n3r.idworker"})
    • DesensitizationUtil 给一些敏感信息做一些修改, 让别人看不到
    • com.imooc.utils.DateUtil 把传入字符串时间变成时间的类
    • Sex和UserStatus 一些枚举类
package com.imooc.user.service.impl;import com.imooc.enums.Sex;
import com.imooc.enums.UserStatus;
import com.imooc.pojo.AppUser;
import com.imooc.user.mapper.AppUserMapper;
import com.imooc.user.service.UserService;
import com.imooc.utils.DateUtil;
import com.imooc.utils.DesensitizationUtil;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;import java.util.Date;@Service
public class UserServiceImpl implements UserService {// 如果appUserMapper有红线报错,去AppUserMapper接口加一个@Repository注解就可以@Autowiredpublic AppUserMapper appUserMapper;@Autowiredpublic Sid sid;@Overridepublic AppUser queryMobileIsExist(String mobile) {Example userExample = new Example(AppUser.class);Example.Criteria userCriteria = userExample.createCriteria();userCriteria.andEqualTo(mobile, mobile);AppUser user = appUserMapper.selectOneByExample(userExample);return user;}private static final String USER_FACE0 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxFw_8_qAIlFXAAAcIhVPdSg994.png";private static final String USER_FACE1 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF6ZUySASMbOAABBAXhjY0Y649.png";private static final String USER_FACE2 = "http://122.152.205.72:88/group1/M00/00/05/CpoxxF6ZUx6ANoEMAABTntpyjOo395.png";@Transactional@Overridepublic AppUser createUser(String mobile) {/*** 互联网项目都要考虑可扩展性* 如果未来的业务激增,那么就需要分库分表* 那么数据库表主键id必须保证全局(全库)唯一,不得重复*/String userId = sid.nextShort();AppUser user = new AppUser();user.setId(userId);user.setMobile(mobile);user.setNickname("用户:" + DesensitizationUtil.commonDisplay(mobile));user.setFace(USER_FACE0);user.setBirthday(DateUtil.stringToDate("1900-01-01"));user.setSex(Sex.secret.type);user.setActiveStatus(UserStatus.INACTIVE.type);user.setTotalIncome(0);user.setCreatedTime(new Date());user.setUpdatedTime(new Date());appUserMapper.insert(user);return user;}
}
  1. com.imooc.user.controller.PassportController#doLogin导入并判断用户是否已经注册。
// 从容器中导入接口@Autowiredprivate UserService userService;// 3. 查询数据库,判断该用户是否注册AppUser user = userService.queryMobileIsExist(mobile);if (user != null && user.getActiveStatus() == UserStatus.FROZEN.type){// 如果用户不为空,并且状态为冻结直接抛出异常 禁止登陆return GraceJSONResult.errorCustom(ResponseStatusEnum.USER_FROZEN);} else if (user == null) {// 如果用户没有注册过, 则为null 需要注册信息入库user = userService.createUser(mobile);}return GraceJSONResult.ok(user);

3.3 设置会话和cookie信息

  1. com.imooc.api.BaseController中加一个变量作为保存到redis的token的键值。和设置cookie的方法。
    public static final String REDIS_USER_TOKEN = "redis_user_token";public static final Integer COOKIE_MONTH = 30 * 24 * 60 * 60;public void setCookie(HttpServletRequest request,HttpServletResponse response,String cookieName,String cookieValue,Integer maxAge){try {cookieValue = URLEncoder.encode(cookieValue, "utf-8");setCookieValue(request, response, cookieName, cookieValue, maxAge);} catch (UnsupportedEncodingException e) {e.printStackTrace();}}public void setCookieValue(HttpServletRequest request,HttpServletResponse response,String cookieName,String cookieValue,Integer maxAge){Cookie cookie = new Cookie(cookieName, cookieValue);cookie.setMaxAge(maxAge);cookie.setDomain("imoocnews.com");cookie.setPath("/");response.addCookie(cookie);}
  1. com.imooc.api.controller.user.PassportControllerApi中修改接口添加参数。
    @ApiOperation(value = "一键注册登录接口", notes = "一键注册登录接口", httpMethod = "POST")@PostMapping("/doLogin")public GraceJSONResult doLogin(@RequestBody @Valid RegistLoginBO registLoginBO,BindingResult result,HttpServletRequest request,HttpServletResponse response);
  1. com.imooc.user.controller.PassportController#doLogin保存用户分布式会话的相关操作。
        // 4. 保存用户分布式会话的相关操作int userActiveStatus = user.getActiveStatus();if (userActiveStatus != UserStatus.FROZEN.type){// 保存token 到redis中String uToken = UUID.randomUUID().toString();redis.set(REDIS_USER_TOKEN + ":" + user.getId(), uToken);// 保存用户id和token到cookie中setCookie(request, response, "utoken", utoken, COOKIE_MONTH);setCookie(request, response, "uid", user.getId(), COOKIE_MONTH);}// 5. 用户登录或注册成功后需要删除redis中的短信验证码, 验证码只能使用一次 用过作废redis.del(MOBILE_SMSCODE + ":" + mobile);// 6. 返回用户状态return GraceJSONResult.ok(userActiveStatus);
  1. 运行测试。

3.4 资源属性与常量绑定

  1. 将第一步中的"imoocnews.com"提出到配置文件中
  2. imooc-news-dev-service-user中的配置文件application-dev.yml 添加配置。
# 设置域名,在java代码中获取,这里是资源配置
website:domain-name: imoocnews.com
  1. 常量绑定:com.imooc.api.BaseController中用@value获取。
    @Value("${website.domain-name}")public String DOMAIN_NAME;cookie.setDomain(DOMAIN_NAME);// cookie.setDomain("imoocnews.com");

第四节 用户信息完善接口

4.1 展示和更新用户账户信息

  1. 第一步:api中创建用户相关的路由api接口com.imooc.api.controller.user.UserControllerApi#getAccountInfo和updateUserInfo。创建一个UpdateUserInfoBO。
  2. 第二步:实现这个路由apicom.imooc.user.controller.UserController#getAccountInfo和updateUserInfo
@RestController
public class UserController extends BaseController implements UserControllerApi {@Autowiredprivate UserService userService;final static Logger logger = LoggerFactory.getLogger(UserController.class);@Overridepublic GraceJSONResult getAccountInfo(String userId) {// 1. 判断参数不能为空if (StringUtils.isBlank(userId)){return GraceJSONResult.errorCustom(ResponseStatusEnum.UN_LOGIN);}// 2. 根据userId查询用户信息AppUser user = getUser(userId);// 3. 返回用户信息return GraceJSONResult.ok(user);}private AppUser getUser(String userId){// TODO 本方法后续公用 并且拓展AppUser user = userService.getUser(userId);return user;}@Overridepublic GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO,BindingResult result) {// 1. 校验BOif (result.hasErrors()){Map<String, String> map = getErrors(result);return GraceJSONResult.errorMap(map);}// 2. 执行更新操作userService.updateUserInfo(updateUserInfoBO);return GraceJSONResult.ok();}
}
  1. 第三步:用户UserService接口中添加函数com.imooc.user.service.UserService#getUser和updateUserInfo
  2. 第四步:实现service中getUser从数据库中获取com.imooc.user.service.impl.UserServiceImpl#getUser和updateUserInfo
    @Overridepublic AppUser getUser(String userId) {return appUserMapper.selectByPrimaryKey(userId);}@Overridepublic void updateUserInfo(UpdateUserInfoBO updateUserInfoBO) {// String userId = updateUserInfoBO.getId();AppUser userInfo = new AppUser();BeanUtils.copyProperties(updateUserInfoBO, userInfo);userInfo.setUpdatedTime(new Date());userInfo.setActiveStatus(UserStatus.ACTIVE.type);// updateByPrimaryKey会把数据库中的所有数据覆盖 没穿过来的覆盖为空// updateByPrimaryKeySelective只会针对对象中现有的数据进行覆盖int result = appUserMapper.updateByPrimaryKeySelective(userInfo);if (result != 1){GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);}}
  1. 第五步:上面直接把user返回是不好的,因为有些字段用不到而且比较隐私。所以可以选择创建一个视图层的对象VO, 用来发挥。com.imooc.pojo.vo.UserAccountInfoVO。字段从com.imooc.pojo.AppUser获取, 重新生成getter和setter。
public class UserAccountInfoVO {private String id;private String mobile;private String nickname;private String face;private String realname;private String email;private Integer sex;private Date birthday;private String province;private String city;private String district;
}
        // 3. 返回用户信息  BeanUtils.copyPropertiesUserAccountInfoVO userAccountInfoVO = new UserAccountInfoVO();BeanUtils.copyProperties(user, userAccountInfoVO);return GraceJSONResult.ok(user);

4.2 展示和缓储用户基本信息

  1. 上面用户账户信息字段比较多,而一些常用的字段我们需要经常读取。那么在创建一个视图层的VOcom.imooc.pojo.vo.AppUserVO用来获取用户基本信息。
public class AppUserVO {private String id;private String nickname;private String face;private String realname;private Integer activeStatus;
}
  1. 创建接口com.imooc.api.controller.user.UserControllerApi#getUserInfo和它的实现和上面获取用户账户信息类似。
  2. 思考一下,用户基本信息接口几乎每个页面都会访问。它的压力还是比较大的,怎么分摊一些压力呢。 因为用户的基本信息基本上不会频繁变化,可以把它存储在浏览器上。
  3. 浏览器存储介质(这里我们前端使用的就是sessionStorage)
    • cookie用于存放用户信息也不太好,而且cookie的大小限制为4k
    • 保存用户信息在 sessionStorage(保存数据的时间有效周期:从打开页面到关闭页面)。5M
    • localStorage是永久存在,对于用户信息不适合存放,5M
  4. 后端把这个用户基本信息存储到redis中。改写com.imooc.user.controller.UserController#getUser, Common中添加工具类JsonUtils。用来处理字符串和对象的转换。
    private AppUser getUser(String userId){// 查询判断redis中是否包含用户信息,如果包含,则查询后直接返回,就不去查询数据库了String userJson = redis.get(REDIS_USER_INFO + ":" + userId);AppUser user = null;if (StringUtils.isNotBlank(userJson)) {user = JsonUtils.jsonToPojo(userJson, AppUser.class);} else {user = userService.getUser(userId);// 由于用户信息不怎么会变动,对于一些千万级别的网站来说,这类信息不会直接去查询数据库// 那么完全可以依靠redis,直接把查询后的数据存入到redis中redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));}return user;}
  1. 别忘记个人信息更新时也要更新我们的redis。防止redis残留脏数据。com.imooc.user.service.impl.UserServiceImpl#updateUserInfo
        String userId = updateUserInfoBO.getId();// 再次查询用户的最新信息,放到redis中AppUser user = getUser(userId);redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));
  1. 断点调试测试一下缓存是否成功。

4.3 缓储数据双写一致

  1. 双写数据不一致问题:假设接口修改user信息,由于网络故障导致redis中信息和mysql中不一致。
  2. 怎么保证mysql和redis数据中的双写一致呢。缓存双删
    • 第一步:保证双写一致,先删除redis中的数据,后更新数据库,如果用户请求量较大,已经删除redis中旧数据来没来的急更新mysql,就又被写到redis中呢?
    • 第二步:那就启动一个线程,等mysql更新过100毫秒之后再删一次redis。
  3. com.imooc.user.service.impl.UserServiceImpl#updateUserInfo
    @Overridepublic void updateUserInfo(UpdateUserInfoBO updateUserInfoBO) {String userId = updateUserInfoBO.getId();// 保证双写一致,先删除redis中的数据,后更新数据库redis.del(REDIS_USER_INFO + ":" + userId);AppUser userInfo = new AppUser();BeanUtils.copyProperties(updateUserInfoBO, userInfo);userInfo.setUpdatedTime(new Date());userInfo.setActiveStatus(UserStatus.ACTIVE.type);// updateByPrimaryKey会把数据库中的所有数据覆盖 没穿过来的覆盖为空// updateByPrimaryKeySelective只会针对对象中现有的数据进行覆盖int result = appUserMapper.updateByPrimaryKeySelective(userInfo);if (result != 1){GraceException.display(ResponseStatusEnum.USER_UPDATE_ERROR);}// 再次查询用户的最新信息,放到redis中AppUser user = getUser(userId);redis.set(REDIS_USER_INFO + ":" + userId, JsonUtils.objectToJson(user));// 缓存双删策略try {Thread.sleep(100);redis.del(REDIS_USER_INFO + ":" + userId);} catch (InterruptedException e) {e.printStackTrace();}}
  1. CAP理论,只能同时满足其中两个(可以搜一下),不能同时满足CAP
    • C 一致性
    • A 可用性
    • P 分区容错性

4.4 用户会话拦截器

  1. imooc-news-dev-service-api中创建com.imooc.api.interceptors.UserTokenInterceptor
package com.imooc.api.interceptors;import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.IPUtil;
import com.imooc.utils.RedisOperator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class UserTokenInterceptor extends BaseInterceptor implements HandlerInterceptor {/*** 拦截请求,访问controller之前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String userId = request.getHeader("headerUserId");String userToken = request.getHeader("headerUserToken");// 判断是否放行boolean run = verifyUserIdToken(userId, userToken, REDIS_USER_TOKEN);System.out.println(run);/*** false:请求被拦截* true:请求通过验证,放行*/return true;}/*** 请求访问到controller之后,渲染视图之前* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}/*** 请求访问到controller之后,渲染视图之后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
  1. imooc-news-dev-service-api中创建com.imooc.api.interceptors.BaseInterceptor
package com.imooc.api.interceptors;import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;public class BaseInterceptor {@Autowiredpublic RedisOperator redis;public static final String REDIS_USER_TOKEN = "redis_user_token";public boolean verifyUserIdToken(String id,String token,String redisKeyPrefix) {if (StringUtils.isNotBlank(id) && StringUtils.isNotBlank(token)) {String redisToken = redis.get(redisKeyPrefix + ":" + id);if (StringUtils.isBlank(id)) {GraceException.display(ResponseStatusEnum.UN_LOGIN);return false;} else {if (!redisToken.equalsIgnoreCase(token)) {GraceException.display(ResponseStatusEnum.TICKET_INVALID);return false;}}} else {GraceException.display(ResponseStatusEnum.UN_LOGIN);return false;}return true;}
}
  1. imooc-news-dev-service-apicom.imooc.api.config.InterceptorConfig增加配置拦截器。
    @Beanpublic UserTokenInterceptor userTokenInterceptor() {return new UserTokenInterceptor();}registry.addInterceptor(userTokenInterceptor()).addPathPatterns("/user/getAccountInfo").addPathPatterns("/user/updateUserInfo");

4.5 用户状态拦截器

  1. imooc-news-dev-service-api中创建com.imooc.api.interceptors.UserActiveInterceptor
  2. 然后配置到com.imooc.api.config.InterceptorConfig
package com.imooc.api.interceptors;import com.imooc.enums.UserStatus;
import com.imooc.exception.GraceException;
import com.imooc.grace.result.ResponseStatusEnum;
import com.imooc.pojo.AppUser;
import com.imooc.utils.JsonUtils;
import com.imooc.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 用户激活状态检查拦截器* 发文章,修改文章,删除文章,* 发表评论,查看评论等等* 这些接口都是需要在用户激活以后,才能进行* 否则需要提示用户前往[账号设置]去修改信息*/
public class UserActiveInterceptor extends BaseInterceptor implements HandlerInterceptor {/*** 拦截请求,访问controller之前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String userId = request.getHeader("headerUserId");String userJson = redis.get(REDIS_USER_INFO + ":" + userId);AppUser user = null;if (StringUtils.isNotBlank(userJson)) {user = JsonUtils.jsonToPojo(userJson, AppUser.class);} else {GraceException.display(ResponseStatusEnum.UN_LOGIN);return false;}if (user.getActiveStatus() == null|| user.getActiveStatus() != UserStatus.ACTIVE.type) {GraceException.display(ResponseStatusEnum.USER_INACTIVE_ERROR);return false;}/*** false:请求被拦截* true:请求通过验证,放行*/return true;}/*** 请求访问到controller之后,渲染视图之前* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}/*** 请求访问到controller之后,渲染视图之后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

第五节 AOP警告日志监控和sql打印

5.1 AOP切面完成统计实现类中函数执行的时间

  1. imooc-news-dev-common引入aop依赖。
        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
  1. 创建一个切面计算指定的包下的实现类中函数执行的时间imooc-news-dev-service-api下创建切面com.imooc.api.aspect.ServiceLogAspect
    • @Aspect 说明这个类是一个切面
    • @Component 注入到容器中
  2. * com.imooc.*.service.impl..*.*(..)任意项目下imooc下的任意包的service的实现包下的任意文件夹或者子文件夹..下任意类下的任意方法*.*下有参数或无参数(..)
package com.imooc.api.aspect;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;@Aspect
@Component
public class ServiceLogAspect {final static Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);/*** AOP通知:* 1. 前置通知* 2. 后置通知* 3. 环绕通知* 4. 异常通知* 5. 最终通知*/// 环绕通知@Around("execution(* com.imooc.*.service.impl..*.*(..))")public Object recordTimeOfService(ProceedingJoinPoint joinPoint)throws Throwable {logger.info("==== 开始执行 {}.{}====",joinPoint.getTarget().getClass(),joinPoint.getSignature().getName());long start = System.currentTimeMillis();Object result = joinPoint.proceed();long end = System.currentTimeMillis();long takeTime = end - start;if (takeTime > 3000) {logger.error("当前执行耗时:{}", takeTime);} else if (takeTime > 2000) {logger.warn("当前执行耗时:{}", takeTime);} else {logger.info("当前执行耗时:{}", takeTime);}return result;}
}

5.2 开启mybatis的日志打印

  1. imooc-news-dev-service-user中配置
# dev环境开启mybatis的日志打印
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

5.3 退出会话和注销会话

  1. api中添加接口。
    @ApiOperation(value = "用户退出登录", notes = "用户退出登录", httpMethod = "POST")@PostMapping("/logout")public GraceJSONResult logout(@RequestParam String userId,HttpServletRequest request,HttpServletResponse response);
  1. api的接口实现
    @Overridepublic GraceJSONResult logout(String userId, HttpServletRequest request, HttpServletResponse response) {redis.del(REDIS_USER_TOKEN + ":" + userId);setCookie(request, response, "utoken", "", COOKIE_DELETE);setCookie(request, response, "uid", "", COOKIE_DELETE);return GraceJSONResult.ok();}

这篇关于第二课 Spring Cloud分布式微服务实战-开发通行证服务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/824696

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

基于Qt开发一个简单的OFD阅读器

《基于Qt开发一个简单的OFD阅读器》这篇文章主要为大家详细介绍了如何使用Qt框架开发一个功能强大且性能优异的OFD阅读器,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 目录摘要引言一、OFD文件格式解析二、文档结构解析三、页面渲染四、用户交互五、性能优化六、示例代码七、未来发展方向八、结论摘要

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

springboot整合 xxl-job及使用步骤

《springboot整合xxl-job及使用步骤》XXL-JOB是一个分布式任务调度平台,用于解决分布式系统中的任务调度和管理问题,文章详细介绍了XXL-JOB的架构,包括调度中心、执行器和Web... 目录一、xxl-job是什么二、使用步骤1. 下载并运行管理端代码2. 访问管理页面,确认是否启动成功

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(