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

本文详细介绍了使用SpringCloud进行微服务实战,包括短信验证码发送与限制、用户注册登录接口的实现、接口权限控制(如拦截器限制发送频率、自定义异常返回)、登录接口的实现(如验证码验证、用户信息存储)以及AOP日志监控。同时,还涉及到Redis的使用,如存储会话信息、用户信息以及缓存策略。
摘要由CSDN通过智能技术生成

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

tags:

  • Java
  • 慕课网

categories:

  • 短信发送
  • 注册登录

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

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 {

    @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 发送短信测试一下

  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);

    @Autowired
    private SMSUtils smsUtils;

    @Override
    public 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: 0
    host: 192.168.242.163
    #password: "123456"
    port: 6379
  1. 添加一个测试接口,这里不放到api中了,只是测试使用。
	@Autowired
    private 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 {

    @Autowired
    public 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);

    @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);
    }
}
  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 {

    @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 {

    }
}
  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 {

    @Bean
    public PassportInterceptor passportInterceptor() {
        return new PassportInterceptor();
    }


    @Override
    public 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)
    @ResponseBody
    public 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;
    }

    @Override
    public 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快捷键
    @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();
    }
  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注解就可以
    @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;
    }
}
  1. 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信息

  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 {

    @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();
    }
}
  1. 第三步:用户UserService接口中添加函数com.imooc.user.service.UserService#getUser和updateUserInfo
  2. 第四步:实现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);
        }
    }
  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.copyProperties
        UserAccountInfoVO 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
    @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();
        }
    }
  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
     */
    @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 {

    }
}
  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 {

    @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;
    }
}
  1. imooc-news-dev-service-apicom.imooc.api.config.InterceptorConfig增加配置拦截器。
    @Bean
    public 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
     */
    @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切面完成统计实现类中函数执行的时间

  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的接口实现
    @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();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值