【项目】小帽学堂(十二)

小帽学堂

十八、首页登录和注册

1. 登录业务介绍(单点登录)

【项目】小帽学堂(十二)_第1张图片

  • 单一服务器模式
    • 早期单一服务器,用户认证。
    • 缺点:单点性能压力,无法扩展
      【项目】小帽学堂(十二)_第2张图片
  • SSO(single sign on)模式(单点登录)
    • 分布式,SSO(single sign on)模式
    • 优点 :
      • 用户身份信息独立管理,更好的分布式管理。
      • 可以自己扩展安全策略。
    • 缺点:
      • 认证服务器访问压力较大。
        【项目】小帽学堂(十二)_第3张图片
  • Token模式
    • 业务流程图{用户访问业务时,必须登录的流程}
    • 优点:
      • 无状态: token无状态,session有状态的
      • 基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT)
    • 缺点:
      • 占用带宽
      • 无法在服务器端销毁
        【项目】小帽学堂(十二)_第4张图片
  • 基于微服务开发,选择token的形式相对较多,因此使用token作为用户认证的标准

2. 使用 JWT 进行跨域身份验证

【项目】小帽学堂(十二)_第5张图片

  • 传统用户身份验证
    【项目】小帽学堂(十二)_第6张图片
  • Internet服务无法与用户身份验证分开。一般过程如下:
    • 用户向服务器发送用户名和密码。
    • 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
    • 服务器向用户返回session_id,session信息都会写入到用户的Cookie。
    • 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
    • 服务器收到session_id并对比之前保存的数据,确认用户的身份。
  • 这种模式最大的问题是,没有分布式架构,无法支持横向扩展。
  • 解决方案
    • session广播
    • 将透明令牌存入cookie,将用户身份信息存入redis
    • 另外一种灵活的解决方案:
      • 使用自包含令牌,通过客户端保存数据,而服务器不保存会话数据。 JWT是这种解决方案的代表。

3. JWT令牌

3.1 访问令牌的类型

【项目】小帽学堂(十二)_第7张图片

3.2 JWT的组成

- 典型的,一个JWT看起来如下图:

【项目】小帽学堂(十二)_第8张图片
- 该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
- 每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

3.2.1 JWT头
  • JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
  "alg": "HS256",
  "typ": "JWT"
}
/* 
在上面的代码中,
alg属性表示签名使用的算法,
默认为HMAC SHA256(写为HS256);
typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
*/
3.2.2 有效载荷
  • 有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
  • 除以上默认字段外,我们还可以自定义私有字段,如下例:
{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}
  • 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
  • JSON对象也使用Base64 URL算法转换为字符串保存。
3.2.3 签名哈希
  • 签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
  • 首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) 
 + "." 
 + base64UrlEncode(claims), secret)
  • 在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
3.2.4 Base64URL算法
  • 如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
  • 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/“和”=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。

3.3 JWT的原则

  • JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}
  • 之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。
  • 服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

3.4 JWT的用法

  • 客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
  • 此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。

3.5 JWT问题和趋势

  • JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
  • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库。
  • 存储在客户端,不占用服务端的内存资源。
  • JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。
  • 当JWT未加密时,一些私密数据无法通过JWT传输。
  • JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
  • JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
  • 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

4. 整合JWT令牌

4.1 在common_utils模块中添加jwt工具依赖

  • 在pom中添加
<dependencies>
        
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
        dependency>
dependencies>

4.2 创建JWT工具类

package com.alex.commonutils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

public class JwtUtils {

    // 常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24;  // token过期时间
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";   // 秘钥

    // 生成token字符串的方法
    public static String getJwtToken(String id, String nickname){

        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                
                .setSubject("lemon-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                
                .claim("id", id)    // 设置token主体部分,存储用户信息
                .claim("nickname", nickname)
                
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

5. 阿里云短信服务介绍

【项目】小帽学堂(十二)_第9张图片

5.1 新建短信微服务

  • 在service模块下创建子模块service-msm
  • 创建controller和service代码
  • 配置application.properties
# 服务端口

server.port=8005

# 服务名

spring.application.name=service-msm


# mysql数据库连接

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/school?serverTimezone=GMT%2B8

spring.datasource.username=root

spring.datasource.password=root


spring.redis.host=127.0.0.1

spring.redis.port=6379

spring.redis.database= 0

spring.redis.timeout=1800000


spring.redis.lettuce.pool.max-active=20

spring.redis.lettuce.pool.max-wait=-1

#最大阻塞等待时间(负数表示没限制)

spring.redis.lettuce.pool.max-idle=5

spring.redis.lettuce.pool.min-idle=0

#最小空闲



#返回json的全局时间格式

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss

spring.jackson.time-zone=GMT+8




#mybatis日志

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
  • 创建启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源自动配置
@ComponentScan("com.alex")
public class MsmApplication {
    public static void main(String[] args) {
        SpringApplication.run(MsmApplication.class, args);
    }
}

5.2 阿里云短信服务

  • 开通阿里云短信服务
  • 添加签名管理与模板管理
    • 添加模板管理
      • 选择 国内消息 - 模板管理 - 添加模板
        【项目】小帽学堂(十二)_第10张图片
      • 点击 添加模板,进入到添加页面,输入模板信息
        【项目】小帽学堂(十二)_第11张图片
      • 点击提交,等待审核,审核通过后可以使用
    • 添加签名管理
      • 选择 国内消息 - 签名管理 - 添加签名
        【项目】小帽学堂(十二)_第12张图片
      • 点击添加签名,进入添加页面,填入相关信息
        【项目】小帽学堂(十二)_第13张图片
      • 点击提交,等待审核,审核通过后可以使用

5.3 编写发送短信接口

【项目】小帽学堂(十二)_第14张图片

  • 在service-msm的pom中引入依赖
<dependencies>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
        dependency>
        <dependency>
            <groupId>com.aliyungroupId>
            <artifactId>aliyun-java-sdk-coreartifactId>
        dependency>
dependencies>
  • 编写controller,根据手机号发送短信
public class MsmController {
    @Autowired
    private MsmService msmService;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 发送短信的方法
    @GetMapping("send/{phone}")
    public R sendMsm(@PathVariable String phone) {
        // 1. 从redis获取验证码,如果获取到直接返回
        String code = redisTemplate.opsForValue().get(phone);
        if(!StringUtils.isEmpty(code)) {
            return R.ok();
        }
        // 生成随机值,传递阿里云进行发送
        code = RandomUtil.getFourBitRandom();
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        // 调用service发送短信的方法
        boolean isSend = msmService.send(param,phone);
        if(isSend) {
            // 发送成功,把发送成功验证码放到redis里面
            // 设置有效时间
            redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
            return R.ok();
        } else {
            return R.error().message("短信发送失败");
        }
    }
}
  • 编写 Service
@Service
public class MsmServiceImpl implements MsmService {
    // 发送短信的方法
    @Override
    public boolean send(Map<String, Object> param, String phone) {
        if(StringUtils.isEmpty(phone)) return false;
        DefaultProfile profile =
        DefaultProfile.getProfile("default", "LTAIq6nIPY09VROj", "FQ7UcixT9wEqMv9F35nORPqKr8XkTF");
        IAcsClient client = new DefaultAcsClient(profile);

        // 设置相关参数
        CommonRequest request = new CommonRequest();

        //request.setProtocol(ProtocolType.HTTPS);

        request.setMethod(MethodType.POST);

        request.setDomain("dysmsapi.aliyuncs.com");

        request.setVersion("2017-05-25");

        request.setAction("SendSms");


        // 设置发送相关的参数
        request.putQueryParameter("PhoneNumbers", phone);   // 手机号

        request.putQueryParameter("SignName", "小帽学堂");  // 申请阿里云 签名名称

        request.putQueryParameter("TemplateCode", "SMS_180051135");    // 申请阿里云 模板code

        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));


        try {
            // 最终发送
            CommonResponse response = client.getCommonResponse(request);

            System.out.println(response.getData());

            return response.getHttpResponse().isSuccess();

        } catch (ClientException e) {

            e.printStackTrace();

        }

        return false;
    }
}

6. 登录功能(接口)

【项目】小帽学堂(十二)_第15张图片

6.1 新建用户微服务

  • 在service模块下创建子模块service-ucenter

  • 使用代码生成器生成代码

    • 创建ucenter_member表
    CREATE TABLE `ucenter_member` (
      `id` char(19) NOT NULL COMMENT '会员id',
      `openid` varchar(128) DEFAULT NULL COMMENT '微信openid',
      `mobile` varchar(11) DEFAULT '' COMMENT '手机号',
      `password` varchar(255) DEFAULT NULL COMMENT '密码',
      `nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
      `sex` tinyint(2) unsigned DEFAULT NULL COMMENT '性别 1 女,2 男',
      `age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄',
      `avatar` varchar(255) DEFAULT NULL COMMENT '用户头像',
      `sign` varchar(100) DEFAULT NULL COMMENT '用户签名',
      `is_disabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否禁用 1(true)已禁用,  0(false)未禁用',
      `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
      `gmt_create` datetime NOT NULL COMMENT '创建时间',
      `gmt_modified` datetime NOT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表';
    
    #
    # Data for table "ucenter_member"
    #
    
    INSERT INTO `ucenter_member` VALUES ('1',NULL,'13700000001','96e79218965eb72c92a549dd5a330112','小三123',1,5,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-01 12:11:33','2019-11-08 11:56:01'),('1080736474267144193',NULL,'13700000011','96e79218965eb72c92a549dd5a330112','用户XJtDfaYeKk',1,19,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-02 12:12:45','2019-01-02 12:12:56'),('1080736474355224577',NULL,'13700000002','96e79218965eb72c92a549dd5a330112','用户wUrNkzAPrc',1,27,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-02 12:13:56','2019-01-02 12:14:07'),('1086387099449442306',NULL,'13520191388','96e79218965eb72c92a549dd5a330112','用户XTMUeHDAoj',2,20,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1086387099520745473',NULL,'13520191389','96e79218965eb72c92a549dd5a330112','用户vSdKeDlimn',1,21,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1086387099608825858',NULL,'13520191381','96e79218965eb72c92a549dd5a330112','用户EoyWUVXQoP',1,18,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1086387099701100545',NULL,'13520191382','96e79218965eb72c92a549dd5a330112','用户LcAYbxLNdN',2,24,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1086387099776598018',NULL,'13520191383','96e79218965eb72c92a549dd5a330112','用户dZdjcgltnk',2,25,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1086387099852095490',NULL,'13520191384','96e79218965eb72c92a549dd5a330112','用户wNHGHlxUwX',2,23,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-01-19 06:17:23','2019-01-19 06:17:23'),('1106746895272849410','o1R-t5u2TfEVeVjO9CPGdHPNw-to',NULL,NULL,'檀梵\'',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/zZfLXcetf2Rpsibq6HbPUWKgWSJHtha9y1XBeaqluPUs6BYicW1FJaVqj7U3ozHd3iaodGKJOvY2PvqYTuCKwpyfQ/132',NULL,0,0,'2019-03-16 10:39:57','2019-03-16 10:39:57'),('1106822699956654081',NULL,NULL,NULL,NULL,NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-03-16 15:41:10','2019-03-16 15:41:10'),('1106823035660357634','o1R-t5i4gENwHYRb5lVFy98Z0bdk',NULL,NULL,'GaoSir',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJI53RcCuc1no02os6ZrattWGiazlPnicoZQ59zkS7phNdLEWUPDk8fzoxibAnXV1Sbx0trqXEsGhXPw/132',NULL,0,0,'2019-03-16 15:42:30','2019-03-16 15:42:30'),('1106823041599492098',NULL,NULL,NULL,NULL,NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-03-16 15:42:32','2019-03-16 15:42:32'),('1106823115788341250','o1R-t5l_3rnbZbn4jWwFdy6Gk6cg',NULL,NULL,'换个网名哇、',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/jJHyeM0EN2jhB70LntI3k8fEKe7W6CwykrKMgDJM4VZqCpcxibVibX397p0vmbKURGkLS4jxjGB0GpZfxCicgt07w/132',NULL,0,0,'2019-03-16 15:42:49','2019-03-16 15:42:49'),('1106826046730227714','o1R-t5gyxumyBqt0CWcnh0S6Ya1g',NULL,NULL,'我是Helen',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKDRfib8wy7A2ltERKh4VygxdjVC1x5OaOb1t9hot4JNt5agwaVLdJLcD9vJCNcxkvQnlvLYIPfrZw/132',NULL,0,0,'2019-03-16 15:54:28','2019-03-16 15:54:28'),('1106828185829490690','o1R-t5nNlou5lRwBVgGNJFm4rbc4',NULL,NULL,' 虎头',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKxCqRzuYWQmpwiaqQEjNxbC7WicebicXQusU306jgmfoOzUcFg1qaDq5BStiblwBjw5dUOblQ2gUicQOQ/132',NULL,0,0,'2019-03-16 16:02:58','2019-03-16 16:02:58'),('1106830599651442689','o1R-t5hZHQB1cbX7HZJsiM727_SA',NULL,NULL,'是吴啊',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJ9CsqApybcs7f3Dyib9IxIh0sBqJb7LicbjU4WticJFF0PVwFvHgtbFdBwfmk3H2t3NyqmEmVx17tRA/132',NULL,0,0,'2019-03-16 16:12:34','2019-03-16 16:12:34'),('1106830976199278593','o1R-t5meKOoyEJ3-IhWRCBKFcvzU',NULL,NULL,'我才是Helen',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83epMicP9UT6mVjYWdno0OJZkOXiajG0sllJTbGJ9DYiceej2XvbDSGCK8LCF7jv1PuG2uoYlePWic9XO8A/132',NULL,0,0,'2019-03-16 16:14:03','2019-03-16 16:14:03'),('1106831936900415490','o1R-t5jXYSWakGtnUBnKbfVT5Iok',NULL,NULL,'文若姬',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/3HEmJwpSzguqqAyzmBwqT6aicIanswZibEOicQInQJI3ZY1qmu59icJC6N7SahKqWYv24GvX5KH2fibwt0mPWcTJ3fg/132',NULL,0,0,'2019-03-16 16:17:52','2019-03-16 16:17:52'),('1106832491064442882','o1R-t5sud081Qsa2Vb2xSKgGnf_g',NULL,NULL,'Peanut',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-03-16 16:20:04','2019-03-16 16:20:04'),('1106833021442510849','o1R-t5lsGc3I8P5bDpHj7m_AIRvQ',NULL,NULL,'食物链终结者',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/MQ7qUmCprK9am16M1Ia1Cs3RK0qiarRrl9y8gsssBjIZeS2GwKSrnq7ZYhmrzuzDwBxSMMAofrXeLic9IBlW4M3Q/132',NULL,0,0,'2019-03-16 16:22:11','2019-03-16 16:22:11'),('1191600824445046786',NULL,'15210078344','96e79218965eb72c92a549dd5a330112','IT妖姬',1,5,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-11-05 14:19:10','2019-11-08 18:04:43'),('1191616288114163713',NULL,'17866603606','96e79218965eb72c92a549dd5a330112','xiaowu',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-11-05 15:20:37','2019-11-05 15:20:37'),('1195187659054329857',NULL,'15010546384','96e79218965eb72c92a549dd5a330112','qy',NULL,NULL,'http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132',NULL,0,0,'2019-11-15 11:51:58','2019-11-15 11:51:58');
    
    • 生成代码
    • 配置application.properties
    # 服务端口
    
    server.port=8006
    
    # 服务名
    
    spring.application.name=service-ucenter
    
    
    # mysql数据库连接
    
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    
    spring.datasource.url=jdbc:mysql://localhost:3306/school?serverTimezone=GMT%2B8
    
    spring.datasource.username=root
    
    spring.datasource.password=alex
    
    
    spring.redis.host=127.0.0.1
    
    spring.redis.port=6379
    
    spring.redis.database= 0
    
    spring.redis.timeout=1800000
    
    spring.redis.lettuce.pool.max-active=20
    
    spring.redis.lettuce.pool.max-wait=-1
    
    #最大阻塞等待时间(负数表示没限制)
    
    spring.redis.lettuce.pool.max-idle=5
    
    spring.redis.lettuce.pool.min-idle=0
    
    #最小空闲
    
    
    #返回json的全局时间格式
    
    spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
    
    spring.jackson.time-zone=GMT+8
    
    
    #配置mapper xml文件的路径
    
    mybatis-plus.mapper-locations=classpath:com/alex/educenter/mapper/xml/*.xml
    
    
    #mybatis日志
    
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    
    • 创建启动类
@ComponentScan({"com.alex"})
@SpringBootApplication
@MapperScan("com.alex.educenter.mapper")
public class UcenterApplication {
    public static void main(String[] args) {
        SpringApplication.run(UcenterApplication.class, args);
    }
}

6.2 创建登录接口

@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
    @Autowired
    private UcenterMemberService memberService;

    // 登录
    @PostMapping("login")
    public R loginUser(@RequestBody UcenterMember member) {
        // 调用Service方法实现登录
        // 返回token值,使用jwt生成
        String token = memberService.login(member);
        return R.ok().data("token", token);
    }
    // 注册
}
public interface UcenterMemberService extends IService<UcenterMember> {
    // 登录的方法
    String login(UcenterMember member);
}
@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    // 登录的方法
    @Override
    public String login(UcenterMember member) {
        // 获取登录手机号和密码
        String mobile = member.getMobile();
        String password = member.getPassword();

        // 手机号和密码非空判断
        if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
            throw new LemonException(20001, "登录失败");
        }

        // 判断手机号是否正确
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile", mobile);
        UcenterMember mobileMember = baseMapper.selectOne(wrapper);
        // 判断查询对象是否为空
        if(mobileMember == null) {  // 没有这个手机号
            throw new LemonException(20001, "登录失败");
        }
        // 判断密码
        // 因为存储到数据库密码肯定加密
        // 把输入的密码进行加密,再和数据库密码进行比较
        // 加密方式 MD5
        if(!MD5.encrypt(password).equals(mobileMember.getPassword())) {
            throw new LemonException(20001, "登录失败");
        }
        // 判断用户是否禁用
        if(mobileMember.getIsDisabled()) {
            throw new LemonException(20001, "登录失败");
        }
        // 登录成功
        // 生成token字符串,使用jwt工具类
        String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());

        return jwtToken;
    }
}

6.3 创建注册接口

  • 创建RegisterVo用于数据封装
@Data
@ApiModel(value="注册对象", description="注册对象")
public class RegisterVo {
    @ApiModelProperty(value = "昵称")
    private String nickname;

    @ApiModelProperty(value = "手机号")
    private String mobile;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "验证码")
    private String code;
}
@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
    @Autowired
    private UcenterMemberService memberService;

    // 注册
    @PostMapping("register")
    public R registerUser(@RequestBody RegisterVo registerVo) {
        memberService.register(registerVo);
        return R.ok();
    }
}
public interface UcenterMemberService extends IService<UcenterMember> {
    // 注册的方法
    void register(RegisterVo registerVo);
}
@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 注册的方法
    @Override
    public void register(RegisterVo registerVo) {
        // 获取注册的数据
        String code = registerVo.getCode(); // 验证码
        String mobile = registerVo.getMobile(); // 手机号
        String nickname = registerVo.getNickname(); // 昵称
        String password = registerVo.getPassword(); // 密码

        // 非空判断
        if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)
            || StringUtils.isEmpty(code) || StringUtils.isEmpty(nickname)) {
            throw new LemonException(20001, "注册失败");
        }

        // 判断验证码
        // 获取redis验证码
        String redisCode = redisTemplate.opsForValue().get(mobile);
        if(!code.equals(redisCode)) {
            throw new LemonException(20001, "注册失败");
        }

        // 判断手机号是否重复,表里面存在相同手机号不尽兴添加
        QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile", mobile);
        Integer count = baseMapper.selectCount(wrapper);
        if(count > 0) {
            throw new LemonException(20001, "注册失败");
        }

        // 数据添加数据库中
        UcenterMember member = new UcenterMember();
        member.setMobile(mobile);
        member.setNickname(nickname);
        member.setPassword(MD5.encrypt(password));
        member.setIsDeleted(false); // 用户不禁用
        member.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132");
        baseMapper.insert(member);
    }
}

6.4 创建接口根据 token 获取用户信息

@RestController
@RequestMapping("/educenter/member")
@CrossOrigin
public class UcenterMemberController {
    @Autowired
    private UcenterMemberService memberService;
    
    // 根据token获取用户信息
    @GetMapping("getMemberInfo")
    public R getMemberInfo(HttpServletRequest request) {
        // 调用jwt工具类的方法。根据request对象获取头信息,返回用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        // 调用数据库根据用户id获取用户信息
        UcenterMember member = memberService.getById(memberId);
        return R.ok().data("userInfo", member);
    }
}

7. 整合登录和注册页面

【项目】小帽学堂(十二)_第16张图片

7.1 在nuxt环境中安装插件

7.1.1 安装element-ui 和 vue-qriously
  • 执行命令安装
    • npm install element-ui
    • npm install vue-qriously
7.1.2 修改配置文件 nuxt-swiper-plugin.js,使用插件
  • nuxt-swiper-plugin.js
// plugins\nuxt-swiper-plugin.js
import Vue from 'vue'
import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr'
import VueQriously from 'vue-qriously'
import ElementUI from 'element-ui' //element-ui的全部组件
import 'element-ui/lib/theme-chalk/index.css'//element-ui的css

Vue.use(ElementUI) //使用elementUI
Vue.use(VueQriously)
Vue.use(VueAwesomeSwiper)

7.2 用户注册功能前端整合

【项目】小帽学堂(十二)_第17张图片

7.2.1 在api文件夹中创建注册的js文件,定义接口
// api\register.js
import request from '@/utils/request'

export default {
  //根据手机号码发送短信
  getMobile(phone) {

    return request({

      url: `/edumsm/msm/send/${phone}`,

      method: 'get'

    })

  },

  //用户注册
  registerMember(formItem) {

    return request({

      url: `/educenter/member/register`,

      method: 'post',

      data: formItem

    })

  }

}
7.2.2 在pages文件夹中创建注册页面,调用方法
  • 在layouts创建布局页面
    • sign.vue
    <template>
      <div class="sign">
        
        <div class="logo">
          <img src="~/assets/img/logo.png" alt="logo">
        div>
        
        <nuxt/>
      div>
    template>
    
  • 创建注册页面
    • 修改layouts文件夹里面default.vue页面,修改登录和注册超链接地址
      【项目】小帽学堂(十二)_第18张图片
    • 在pages文件夹下,创建注册和登录页面
      【项目】小帽学堂(十二)_第19张图片

<template>

  <div class="main">

    <div class="title">

      <a href="/login">登录a>

      <span>·span>

      <a class="active" href="/register">注册a>

    div>


    <div class="sign-up-container">

      <el-form ref="userForm" :model="params">


        <el-form-item class="input-prepend restyle" prop="nickname" :rules="[{ required: true, message: '请输入你的昵称', trigger: 'blur' }]">

          <div>

            <el-input type="text" placeholder="你的昵称" v-model="params.nickname"/>

            <i class="iconfont icon-user"/>

          div>

        el-form-item>


        <el-form-item class="input-prepend restyle no-radius" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">

          <div>

            <el-input type="text" placeholder="手机号" v-model="params.mobile"/>

            <i class="iconfont icon-phone"/>

          div>

        el-form-item>


        <el-form-item class="input-prepend restyle no-radius" prop="code" :rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]">

          <div style="width: 100%;display: block;float: left;position: relative">

            <el-input type="text" placeholder="验证码" v-model="params.code"/>

            <i class="iconfont icon-phone"/>

          div>

          <div class="btn" style="position:absolute;right: 0;top: 6px;width: 40%;">

            <a href="javascript:" type="button" @click="getCodeFun()" :value="codeTest" style="border: none;background-color: none">{{codeTest}}a>

          div>

        el-form-item>


        <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">

          <div>

            <el-input type="password" placeholder="设置密码" v-model="params.password"/>

            <i class="iconfont icon-password"/>

          div>

        el-form-item>


        <div class="btn">

          <input type="button" class="sign-up-button" value="注册" @click="submitRegister()">

        div>

        <p class="sign-up-msg">

          点击 “注册” 即表示您同意并愿意遵守简书

          <br>

          <a target="_blank" href="http://www.jianshu.com/p/c44d171298ce">用户协议a><a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策a>p>

      el-form>

      

      <div class="more-sign">

        <h6>社交帐号直接注册h6>

        <ul>

          <li><a id="weixin" class="weixin" target="_blank" href="http://huaan.free.idcfengye.com/api/ucenter/wx/login"><i

            class="iconfont icon-weixin"/>a>li>

          <li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/>a>li>

        ul>

      div>

    div>

  div>

template>


<script>

  import '~/assets/css/sign.css'

  import '~/assets/css/iconfont.css'

  import registerApi from '@/api/register'


  export default {

    layout: 'sign',

    data() {

      return {

        params: {

          mobile: '',

          code: '',

          nickname: '',

          password: ''

        },

        sending: true,      //是否发送验证码

        second: 60,        //倒计时间

        codeTest: '获取验证码'

      }

    },

    methods: {
      // 注册提交的方法
      submitRegister() {
          registerApi.registerMember(this.params)
            .then(response => {
                // 提示注册成功
              this.$message({
                type: 'success',
                message: "注册成功"
              })
                // 跳转登录页面
                this.$router.push({path:'/login'})
            })
      },
      // 通过输入手机号发送验证码
      getCodeFun() {
          registerApi.getMobile(this.params.mobile)
            .then(response => {
                this.sending = false
                // 调用倒计时的方法
                this.timeDown()
            })
      },
    
      timeDown() {

        let result = setInterval(() => {

          --this.second;

          this.codeTest = this.second

          if (this.second < 1) {

            clearInterval(result);

            this.sending = true;

            //this.disabled = false;

            this.second = 60;

            this.codeTest = "获取验证码"

          }

        }, 1000);
      },
      checkPhone (rule, value, callback) {

        //debugger

        if (!(/^1[34578]\d{9}$/.test(value))) {

          return callback(new Error('手机号码格式不正确'))

        }

        return callback()

      }

    }

  }

script>

7.3 登录前端整合

【项目】小帽学堂(十二)_第20张图片

7.3.1 在api文件夹中创建登录的js文件,定义接口
// api\login.js
import request from '@/utils/request'


export default {

  //登录的方法

  submitLoginUser(userInfo) {

    return request({

      url: `/educenter/member/login`,

      method: 'post',

      data: userInfo

    })

  },

  //根据token获取用户信息

  getLoginUserInfo() {

    return request({

      url: `/educenter/member/getMemberInfo`,

      method: 'get'

    })

  }

}
7.3.2 在pages文件夹中创建登录页面,调用方法
  • 安装js-cookie插件
<template>

  <div class="main">

    <div class="title">

      <a class="active" href="/login">登录a>

      <span>·span>

      <a href="/register">注册a>

    div>


    <div class="sign-up-container">

      <el-form ref="userForm" :model="user">


        <el-form-item class="input-prepend restyle" prop="mobile" :rules="[{ required: true, message: '请输入手机号码', trigger: 'blur' },{validator: checkPhone, trigger: 'blur'}]">

          <div >

            <el-input type="text" placeholder="手机号" v-model="user.mobile"/>

            <i class="iconfont icon-phone" />

          div>

        el-form-item>


        <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">

          <div>

            <el-input type="password" placeholder="密码" v-model="user.password"/>

            <i class="iconfont icon-password"/>

          div>

        el-form-item>


        <div class="btn">

          <input type="button" class="sign-in-button" value="登录" @click="submitLogin()">

        div>

      el-form>

      

      <div class="more-sign">

        <h6>社交帐号登录h6>

        <ul>

          <li><a id="weixin" class="weixin" target="_blank" href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login"><i class="iconfont icon-weixin"/>a>li>

          <li><a id="qq" class="qq" target="_blank" href="#"><i class="iconfont icon-qq"/>a>li>

        ul>

      div>

    div>


  div>

template>


<script>

  import '~/assets/css/sign.css'

  import '~/assets/css/iconfont.css'

  import cookie from 'js-cookie'


  import loginApi from '@/api/login'

  export default {

    layout: 'sign',


    data () {

      return {

        user:{

          mobile:'',

          password:''

        },

        loginInfo:{}

      }

    },


    methods: {

     submitLogin() {
        // 第一步 调用接口进行登录,返回token字符串
        loginApi.submitLoginUser(this.user)
          .then(response => {
            // 第二步 获取token字符串放到cookie里面
            // 第一个参数 cookie名称,第二个参数 值,第三个参数 作用范围
            cookie.set('lemon_token', response.data.data.token, {domain:'localhost'})

            // 第四步 调用接口 根据token获取用户信息,为了首页面显示
            loginApi.getLoginUserInfo()
              .then(response => {
                this.loginInfo = response.data.data.userInfo
                // 获取返回的用户信息,放到cookie里面
                cookie.set('lemon_ucenter', JSON.stringify(this.loginInfo), {domain:'localhost'})
                // 跳转页面
                window.location.href = "/";
              })
          })
      },


      checkPhone (rule, value, callback) {

        //debugger

        if (!(/^1[34578]\d{9}$/.test(value))) {

          return callback(new Error('手机号码格式不正确'))

        }

        return callback()

      }

    }

  }

script>

<style>

   .el-form-item__error{

    z-index: 9999999;

  }

style>
7.3.3 在request.js添加拦截器,用于传递token信息
// utils\request.js
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import cookie from 'js-cookie'

// 创建axios实例
const service = axios.create({
  baseURL: 'http://localhost:9001', // api的base_url
  timeout: 20000 // 请求超时时间
})

// 第三步 创建拦截器
service.interceptors.request.use(
  config => {

    //debugger
    // 判断cookie里面是否有名称是lemon_token数据
    if (cookie.get('lemon_token')) {
      // 把获取cookie值放到header里面
      config.headers['token'] = cookie.get('lemon_token');
  
    }
  
      return config
  
    },
  
    err => {
  
    return Promise.reject(err);
    }
)

// http response 拦截器

service.interceptors.response.use(

  response => {

    //debugger

    if (response.data.code == 28004) {

        console.log("response.data.resultCode是28004")

        // 返回 错误代码-1 清除ticket信息并跳转到登录页面

        //debugger

        window.location.href="/login"

        return

    }else{

      if (response.data.code !== 20000) {

        //25000:订单支付中,不做任何提示

        if(response.data.code != 25000) {

          Message({

            message: response.data.message || 'error',

            type: 'error',

            duration: 5 * 1000

          })

        }

      } else {

        return response;

      }

    }

  },

  error => {

    return Promise.reject(error.response)   // 返回接口返回的错误信息

});
export default service
7.3.4 修改layouts中的default.vue页面
  • 显示登录之后的用户信息
<script>
import "~/assets/css/reset.css";
import "~/assets/css/theme.css";
import "~/assets/css/global.css";
import "~/assets/css/web.css";

import cookie from 'js-cookie'
import userApi from '@/api/login'

export default {
  data() {
    return {
      token: '',
      loginInfo: {
        id: '',
        age: '',
        avatar: '',
        mobile: '',
        nickname: '',
        sex: ''
      }
    }
  },

  created() {
    this.showInfo()
  },

  methods: {
    showInfo() {
      //debugger
      var jsonStr = cookie.get("lemon_ucenter");
      //alert(jsonStr)
      if (jsonStr) {
        this.loginInfo = JSON.parse(jsonStr)
      }
    },

    logout() {
      //debugger
      cookie.set('lemon_token', '', {domain:'localhost'})
      cookie.set('lemon_ucenter', '', {domain:'localhost'})

      //跳转页面
      window.location.href = "/"
    }
  }
}
script>
  • default.vue页面显示登录之后的用户信息
<ul class="h-r-login">

    <li v-if="!loginInfo.id" id="no-login">

        <a href="/login" title="登录">

            <em class="icon18 login-icon"> em>

            <span class="vam ml5">登录span>

        a>

        |

        <a href="/register" title="注册">

            <span class="vam ml5">注册span>

        a>

    li>

    <li v-if="loginInfo.id" id="is-login-one" class="mr10">

        <a id="headerMsgCountId" href="#" title="消息">

            <em class="icon18 news-icon"> em>

        a>

        <q class="red-point" style="display: none"> q>

    li>

    <li v-if="loginInfo.id" id="is-login-two" class="h-r-user">

        <a href="/ucenter" title>

            <img

                 :src="loginInfo.avatar"

                 width="30"

                 height="30"

                 class="vam picImg"

                 alt

                 >

            <span id="userName" class="vam disIb">{{ loginInfo.nickname }}span>

        a>

        <a href="javascript:void(0);" title="退出" @click="logout()" class="ml5">退出a>

    li>

    

ul>

你可能感兴趣的:(项目,java)