第二课 Spring Cloud分布式微服务实战-开发通行证服务
tags:
- Java
- 慕课网
categories:
- 短信发送
- 注册登录
文章目录
第一节 短信注册配置和环境
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 {
@Autowired
public AliyunResource aliyunResource;
public void sendSMS(String mobile, String code){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", aliyunResource.getAccessKeyID(), aliyunResource.getAccessKeySecret());
/** use STS Token
DefaultProfile 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);
@Autowired
private SMSUtils smsUtils;
@Override
public 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: 0
host: 192.168.242.163
#password: "123456"
port: 6379
- 添加一个测试接口,这里不放到api中了,只是测试使用。
@Autowired
private 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 {
@Autowired
public 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);
@Autowired
private SMSUtils smsUtils;
@Override
public GraceJSONResult getSMSCode(String mobile, HttpServletRequest request) {
// 获取用户IP
String 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 {
@Autowired
public RedisOperator redis;
public static final String MOBILE_SMSCODE = "mobile:smscode";
/**
* 拦截请求,访问controller之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获得用户ip
String 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
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 请求访问到controller之后,渲染视图之后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public 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 {
@Bean
public PassportInterceptor passportInterceptor() {
return new PassportInterceptor();
}
@Override
public 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)
@ResponseBody
public 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;
}
@Override
public 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快捷键
@Override
public 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.StringUtils
String 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注解就可以
@Autowired
public AppUserMapper appUserMapper;
@Autowired
public Sid sid;
@Override
public 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
@Override
public 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导入并判断用户是否已经注册。
// 从容器中导入接口
@Autowired
private 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 {
@Autowired
private UserService userService;
final static Logger logger = LoggerFactory.getLogger(UserController.class);
@Override
public 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;
}
@Override
public GraceJSONResult updateUserInfo(@Valid UpdateUserInfoBO updateUserInfoBO,
BindingResult result) {
// 1. 校验BO
if (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
@Override
public AppUser getUser(String userId) {
return appUserMapper.selectByPrimaryKey(userId);
}
@Override
public 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.copyProperties
UserAccountInfoVO 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
@Override
public 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
*/
@Override
public 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
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 请求访问到controller之后,渲染视图之后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public 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 {
@Autowired
public 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增加配置拦截器。
@Bean
public 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
*/
@Override
public 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
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 请求访问到controller之后,渲染视图之后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public 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的接口实现
@Override
public 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();
}
本文详细介绍了使用SpringCloud进行微服务实战,包括短信验证码发送与限制、用户注册登录接口的实现、接口权限控制(如拦截器限制发送频率、自定义异常返回)、登录接口的实现(如验证码验证、用户信息存储)以及AOP日志监控。同时,还涉及到Redis的使用,如存储会话信息、用户信息以及缓存策略。

被折叠的 条评论
为什么被折叠?



