本文主要是介绍第二课 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 短信登录注册涉及内容
-
短信登录注册
-
短信验证码发送与限制
-
分布式会话 redis实现
-
用户信息完善,OSS/FastDFS文件上传
-
AOP日志监控。通过AOP把常用日志输出如:MyBaits日志
-
短信发送验证码流程
-
短信一键登录注册流程
1.2 配置密钥和资源文件(阿里云)
- 阿里云官网直接搜索
短信服务
imooc-news-dev-common
下创建aliyun.properties文件。
aliyun.accessKeyID=XXXXXXXXXXXXXXXXX
aliyun.accessKeySecret=XXXXXXXXXXXXXX
- 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>
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 整合发送短信
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 发送短信测试一下
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();
}
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环境配置操作类
- 安装redis。找到封装redis的操作类复制到
imooc-news-dev-common
中的com.imooc.utils.RedisOperator
。 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>
imooc-news-dev-service-user
中添加redis相关配置。
spring:redis:database: 0host: 192.168.242.163#password: "123456"port: 6379
- 添加一个测试接口,这里不放到api中了,只是测试使用。
@Autowiredprivate RedisOperator redis;@GetMapping("/redis")public Object redis() {redis.set("age", "18");return GraceJSONResult.ok(redis.get("age"));}
- 运行访问。http://127.0.0.1:8003/doc.html
第二节 短信注册接口完善
2.1 获取客户端ip和验证码存储
- 写一个获取客户端IP的工具类
com.imooc.utils.IPUtil
。放到imooc-news-dev-common
中。 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";}
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);
}
- 修改
imooc-news-dev-service-user
的com.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);}
}
- 运行和前端联调。
imooc-news-service-api
中添加com.imooc.api.config.CorsConfig
工具类解决跨站访问请求的问题。 - 服务在本人电脑上运行,前端在服务器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 拦截器限制发送频率
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 {}
}
- 把拦截器配置到容器中。
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");}
}
- 重新编译测试。看下点击两此发送验证码,会不会输出频率过高的提示。
2.3 自定义异常返回错误信息
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);}}
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;}
}
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());}
}
- 改写
imooc-news-service-api
中com.imooc.api.interceptors.PassportInterceptor
的代码端。
if (keyIsExist) {GraceException.display(ResponseStatusEnum.SMS_NEED_WAIT_ERROR);//System.out.println("短信发送频率太大!");return false;}
- 运行测试。
第三节 短信登录接口
3.1 验证BO信息
- BO是从视图层传过来,又称Bussiness Object针对业务方面进行处理的。
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 + '\'' +'}';}
}
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);
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();}
imooc-news-service-api
的com.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;}
- 运行测试。http://127.0.0.1:8003/doc.html
3.2 查询老用户和新用户添加
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);
}
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 一些枚举类
- org.n3r.idworker.Sid 用来生成全局唯一的id的,这里需要注册到容器中。
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;}
}
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信息
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);}
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);
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);
- 运行测试。
3.4 资源属性与常量绑定
- 将第一步中的"imoocnews.com"提出到配置文件中。
imooc-news-dev-service-user
中的配置文件application-dev.yml 添加配置。
# 设置域名,在java代码中获取,这里是资源配置
website:domain-name: imoocnews.com
- 常量绑定:
com.imooc.api.BaseController
中用@value获取。
@Value("${website.domain-name}")public String DOMAIN_NAME;cookie.setDomain(DOMAIN_NAME);// cookie.setDomain("imoocnews.com");
第四节 用户信息完善接口
4.1 展示和更新用户账户信息
- 第一步:api中创建用户相关的路由api接口
com.imooc.api.controller.user.UserControllerApi#getAccountInfo和updateUserInfo
。创建一个UpdateUserInfoBO。 - 第二步:实现这个路由api
com.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();}
}
- 第三步:用户UserService接口中添加函数
com.imooc.user.service.UserService#getUser和updateUserInfo
- 第四步:实现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);}}
- 第五步:上面直接把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 展示和缓储用户基本信息
- 上面用户账户信息字段比较多,而一些常用的字段我们需要经常读取。那么在创建一个视图层的VO
com.imooc.pojo.vo.AppUserVO
用来获取用户基本信息。
public class AppUserVO {private String id;private String nickname;private String face;private String realname;private Integer activeStatus;
}
- 创建接口
com.imooc.api.controller.user.UserControllerApi#getUserInfo
和它的实现和上面获取用户账户信息类似。 - 思考一下,用户基本信息接口几乎每个页面都会访问。它的压力还是比较大的,怎么分摊一些压力呢。 因为用户的基本信息基本上不会频繁变化,可以把它存储在浏览器上。
- 浏览器存储介质(这里我们前端使用的就是sessionStorage)
- cookie用于存放用户信息也不太好,而且cookie的大小限制为4k
- 保存用户信息在 sessionStorage(保存数据的时间有效周期:从打开页面到关闭页面)。5M
- localStorage是永久存在,对于用户信息不适合存放,5M
- 后端把这个用户基本信息存储到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;}
- 别忘记个人信息更新时也要更新我们的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));
- 断点调试测试一下缓存是否成功。
4.3 缓储数据双写一致
- 双写数据不一致问题:假设接口修改user信息,由于网络故障导致redis中信息和mysql中不一致。
- 怎么保证mysql和redis数据中的双写一致呢。缓存双删
- 第一步:保证双写一致,先删除redis中的数据,后更新数据库,如果用户请求量较大,已经删除redis中旧数据来没来的急更新mysql,就又被写到redis中呢?
- 第二步:那就启动一个线程,等mysql更新过100毫秒之后再删一次redis。
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();}}
- CAP理论,只能同时满足其中两个(可以搜一下),不能同时满足CAP
- C 一致性
- A 可用性
- P 分区容错性
4.4 用户会话拦截器
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 {}
}
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;}
}
imooc-news-dev-service-api
中com.imooc.api.config.InterceptorConfig
增加配置拦截器。
@Beanpublic UserTokenInterceptor userTokenInterceptor() {return new UserTokenInterceptor();}registry.addInterceptor(userTokenInterceptor()).addPathPatterns("/user/getAccountInfo").addPathPatterns("/user/updateUserInfo");
4.5 用户状态拦截器
imooc-news-dev-service-api
中创建com.imooc.api.interceptors.UserActiveInterceptor
。- 然后配置到
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切面完成统计实现类中函数执行的时间
- 在
imooc-news-dev-common
引入aop依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
- 创建一个切面计算指定的包下的实现类中函数执行的时间。
imooc-news-dev-service-api
下创建切面com.imooc.api.aspect.ServiceLogAspect
。- @Aspect 说明这个类是一个切面
- @Component 注入到容器中
* 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的日志打印
imooc-news-dev-service-user
中配置
# dev环境开启mybatis的日志打印
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
5.3 退出会话和注销会话
- api中添加接口。
@ApiOperation(value = "用户退出登录", notes = "用户退出登录", httpMethod = "POST")@PostMapping("/logout")public GraceJSONResult logout(@RequestParam String userId,HttpServletRequest request,HttpServletResponse response);
- 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分布式微服务实战-开发通行证服务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!