[TOC]

前台的登录和注册

使用腾讯云短信服务

注册服务

  • 先开通微信公众号
  • 登录腾讯云,注册账号后,开通短信服务
  • 创建签名,注意此时选择公众号,并在微信公众号申请开通
    image.png
  • 创建模板
    image.png
  • 创建子AK(也可以使用主AK)
    image.png
    image.png
  • 新建用户,授权短信权限,保存好secretId和secretKey
    image.png
  • 在线测试短信,可以直接生成代码
    image.png
    可以参考自动生成的代码。
    SmsSdkAppId查找方式
    image.png

项目集成短信

  • 创建项目
    image.png
  • 配置文件

    server:
    port: 8005             # 服务端口
    spring:
    application:
     name: service-msm    # 微服务名称
    profiles:
     active: dev          # 设置为开发环境
    jackson:                            # 配置json全局时间
     date-format: yyyy-MM-dd HH:mm:ss  # 配置返回json的时间格式
     time-zone: GMT+8                  # json是格林尼治时间,和我们相差8小时,需要加上8
    redis:
     host: 192.168.241.130             # ip地址
     port: 6379                        # 端口号
     database: 0
     timeout: 1800000                  # 超时时间
     lettuce:
       pool:
         max-active: 20
         max-wait: -1                  # 最大阻塞等待时间(负数表示没限制)
         max-idle: 5
         min-idle: 0                   # 最小空闲
    mybatis-plus:                         # mybatis-plus日志
    configuration:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    mapper-locations: classpath:com/atguigu/eduservice/mapper/xml/*.xml
    tencent:
    msm:
     secretID:   # keyId
     secretKey:  # secreKey
     endPoint: sms.tencentcloudapi.com
     appId:      # 短信的应用号
     signName:   # 签名
     templateId: # 模板id
    
  • 主启动类

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @ComponentScan({"com.atguigu"})
    public class MsmApplication {
     public static void main(String[] args) {
         SpringApplication.run(MsmApplication.class, args);
     }
    }
    
  • 创建生成验证码工具类

    /**
    * 获取随机数
    * 
    * @author qianyi
    *
    */
    public class RandomUtil {
    
    private static final Random random = new Random();
    
    private static final DecimalFormat fourdf = new DecimalFormat("0000");
    
    private static final DecimalFormat sixdf = new DecimalFormat("000000");
    
    public static String getFourBitRandom() {
       return fourdf.format(random.nextInt(10000));
    }
    
    public static String getSixBitRandom() {
       return sixdf.format(random.nextInt(1000000));
    }
    
    /**
     * 给定数组,抽取n个数据
     * @param list
     * @param n
     * @return
     */
    public static ArrayList getRandom(List list, int n) {
    
       Random random = new Random();
    
       HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
    
       // 生成随机数字并存入HashMap
       for (int i = 0; i < list.size(); i++) {
    
          int number = random.nextInt(100) + 1;
    
          hashMap.put(number, i);
       }
    
       // 从HashMap导入数组
       Object[] robjs = hashMap.values().toArray();
    
       ArrayList r = new ArrayList();
    
       // 遍历数组并打印数据
       for (int i = 0; i < n; i++) {
          r.add(list.get((int) robjs[i]));
          System.out.print(list.get((int) robjs[i]) + "\t");
       }
       System.out.print("\n");
       return r;
    }
    }
    
  • 创建常量类,在配置文件中绑定值
    ```java @Component public class ConstantMsgUtil implements InitializingBean { //我已经再 @Value(“${tencent.msm.secretID}”) private String secretID ;

    @Value(“${tencent.msm.secretKey}”) private String secretKey ;

    @Value(“${tencent.msm.endPoint}”) private String endPoint;

    @Value(“${tencent.msm.appId}”) private String appId;

    @Value(“${tencent.msm.signName}”) private String signName;

    @Value(“${tencent.msm.templateId}”) private String templateId; //六个相关的参数 public static String SECRET_ID; public static String SECRET_KEY; public static String END_POINT; public static String APP_ID; public static String SIGN_NAME; public static String TEMPLATE_ID;

@Override
public void afterPropertiesSet() throws Exception {
    SECRET_ID = secretID;
    SECRET_KEY = secretKey;
    END_POINT = endPoint;
    APP_ID = appId;
    SIGN_NAME = signName;
    TEMPLATE_ID = templateId;
}

}


-  controller  
```java
@CrossOrigin
@RestController
@RequestMapping("/msmservice/msm-msgservice")
public class MessageController {

    @Autowired
    private MsgService msgService;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("send/{phone}")
    public ResultEntity sendCode(@PathVariable String phone) {
        // 先在redis中获取验证码
        String code = redisTemplate.opsForValue().get(phone);

        // 判断验证码是否存在
        if (!StringUtils.isEmpty(code)) {
            return ResultEntity.ok();
        }

        // 工具类获取6位的验证码
        code = RandomUtil.getSixBitRandom();
        // 调用service发送验证码
        boolean ifSend = msgService.sendMessage(phone, code);
        if (ifSend) {
            // 将验证码存入redis,并设置5分钟过期
            redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
            return ResultEntity.ok();
        }else {
            return ResultEntity.error().message("发送短信失败");
        }
    }

}
  • service实现发送短信,参考腾讯云生成的代码进行修改

    @Service
    public class MsgServiceImpl implements MsgService {
     @Override
     public boolean sendMessage(String phone, String code) {
         try{
             // 实例化一个认证对象,入参需要传入腾讯云账户secretId,secretKey,此处还需注意密钥对的保密
             // 密钥可前往https://console.cloud.tencent.com/cam/capi网站进行获取
             Credential cred = new Credential(ConstantMsgUtil.SECRET_ID, ConstantMsgUtil.SECRET_KEY);
             // 实例化一个http选项,可选的,没有特殊需求可以跳过
             HttpProfile httpProfile = new HttpProfile();
             httpProfile.setEndpoint(ConstantMsgUtil.END_POINT);
             // 实例化一个client选项,可选的,没有特殊需求可以跳过
             ClientProfile clientProfile = new ClientProfile();
             clientProfile.setHttpProfile(httpProfile);
             // 实例化要请求产品的client对象,clientProfile是可选的
             SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile);
             // 实例化一个请求对象,每个接口都会对应一个request对象
             SendSmsRequest req = new SendSmsRequest();
             // 国内短信需要加上86
             String[] phoneNumberSet1 = {"86" + phone};
             req.setPhoneNumberSet(phoneNumberSet1);
    
             req.setSmsSdkAppId(ConstantMsgUtil.APP_ID);
             req.setSignName(ConstantMsgUtil.SIGN_NAME);
             req.setTemplateId(ConstantMsgUtil.TEMPLATE_ID);
    
             String[] templateParamSet1 = {code};
             req.setTemplateParamSet(templateParamSet1);
    
             // 返回的resp是一个SendSmsResponse的实例,与请求对象对应
             SendSmsResponse resp = client.SendSms(req);
             // 输出json格式的字符串回包
             System.out.println(SendSmsResponse.toJsonString(resp));
    
             return true;
         } catch (TencentCloudSDKException e) {
             System.out.println(e.toString());
             return false;
         }
     }
    }
    

登录业务介绍

单一服务器模式

早期单一服务器,用户认证。

前台的登录和注册 - 图9

缺点:单点性能压力,无法扩展

SSO(single sign on)模式

分布式,SSO(single sign on)模式

前台的登录和注册 - 图10

优点 :

用户身份信息独立管理,更好的分布式管理。

可以自己扩展安全策略

缺点:

认证服务器访问压力较大。

Token模式

业务流程图{用户访问业务时,必须登录的流程}

前台的登录和注册 - 图11

优点:

无状态: token无状态,session有状态的

基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT)

缺点:

占用带宽

无法在服务器端销毁

注:基于微服务开发,选择token的形式相对较多,因此我使用token作为用户认证的标准

单点登录

单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。

单点登录的三种常见方式:

  • session广播机制:通过session赋值可以在各模块间传输用户的信息,但模块过多会造成性能下降。
  • cookie+redis:用户在任意模块登录之后,会在生成随机的key,再将用户的信息作为value保存在redis中,将key保存在cookie中,在用户访问其他模块时,会带着cookie访问,此时会通过cookie带的值作为key查找redis,如果查出有数据则无需再次登录。
  • 使用token:用户在任意模块登录之后,会按照一定规则将用户信息生成token字符串,可以将token通过cookie返回,也可以通过地址栏返回,此项目通过第二种方式,每次在地址栏带上用户信息,如果能按规则解析到用户信息,则不用登录,若不能解析则需要登录。

JWT

使用JWT进行跨域身份验证

传统用户身份验证

image.png

Internet服务无法与用户身份验证分开。一般过程如下:

  1. 用户向服务器发送用户名和密码。
  2. 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
  3. 服务器向用户返回session_id,session信息都会写入到用户的Cookie。
  4. 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
  5. 服务器收到session_id并对比之前保存的数据,确认用户的身份。

这种模式最大的问题是,没有分布式架构,无法支持横向扩展。

解决方案

  1. session广播
  2. 将透明令牌存入cookie,将用户身份信息存入redis

另外一种灵活的解决方案:

使用自包含令牌,通过客户端保存数据,而服务器不保存会话数据。 JWT是这种解决方案的代表。

JWT令牌

访问令牌的类型

image.png

JWT的组成

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

image.png

该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。

每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

JWT头

JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。

{
  "alg": "HS256",
  "typ": "JWT"
}

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用”.”分隔,就构成整个JWT对象。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法。

JWT的原则

JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。

服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。

JWT的用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。

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

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

整合JWT令牌

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

在pom中添加

<dependencies>
    <!-- JWT-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

创建JWT工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
 * @author
 */
public class JwtUtils {
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
    public static String getJwtToken(String id, String nickname){
        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("guli-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .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");
    }
}

用户登录和注册

后端

  • 创建模块service-ucenter,application文件,端口号修改后,注意与nginx配置对应

    server:
    port: 8160              # 服务端口(原本为8006,为了使用微信登录的回调,需要修改为8160)
    spring:
    application:
     name: service-ucenter # 微服务名称
    profiles:
     active: dev           # 设置为开发环境
    datasource:             # 配置数据源
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://localhost:3306/guli_edu?characterEncoding=utf-8&serverTimezone=GMT%2B8
     username: root
     password: root
    jackson:                            # 配置json全局时间
     date-format: yyyy-MM-dd HH:mm:ss  # 配置返回json的时间格式
     time-zone: GMT+8                  # json是格林尼治时间,和我们相差8小时,需要加上8
    redis:
     host: 192.168.241.130             # ip地址
     port: 6379                        # 端口号
     database: 0
     timeout: 1800000                  # 超时时间
     lettuce:
       pool:
         max-active: 20
         max-wait: -1                  # 最大阻塞等待时间(负数表示没限制)
         max-idle: 5
         min-idle: 0
    mybatis-plus:                         # mybatis-plus日志
    configuration:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    mapper-locations: classpath:com/atguigu/eduservice/mapper/xml/*.xml
    wx:
    open:
     app_id:                                   # 微信开放平台 appid
     app_secret:                                 # 微信开放平台 appsecret
     redirect_url: http://localhost:8160/ucenterservice/ucenter-wxlogin/callback # 微信开放平台 重定向url
    
  • controller
    ```java @RestController @RequestMapping(“/ucenterservice/ucenter-member”) @CrossOrigin public class UcenterMemberController {

    @Autowired private UcenterMemberService ucenterMemberService;

    @ApiOperation(value = “登录账号,返回token”) @PostMapping(“login”) public ResultEntity userLogin(@RequestBody LoginVO loginVO) {

     String token = ucenterMemberService.login(loginVO);
     if (!StringUtils.isEmpty(token)) {
         return ResultEntity.ok().data("token", token);
     } else {
         return ResultEntity.error();
     }
    

    }

    @ApiOperation(value = “注册账号”) @PostMapping(“register”) public ResultEntity userRegister(@RequestBody RegisterVO registerVO) {

     ucenterMemberService.register(registerVO);
     return ResultEntity.ok();
    

    }

    @ApiOperation(value = “根据token获取登录信息”) @GetMapping(“getLoginInfo”) public ResultEntity getLoginInfo(HttpServletRequest request) {

     try {
         String memberIdByJwtToken = JwtUtils.getMemberIdByJwtToken(request);
         LoginInfoVO loginInfoVO = ucenterMemberService.getLoginInfo(memberIdByJwtToken);
         return ResultEntity.ok().data("loginInfoVO", loginInfoVO);
     } catch (Exception exception) {
         exception.printStackTrace();
         throw new GuliException(20001, "信息不存在!");
     }
    

    }

}


-  service实现  
```java
@Service
@Transactional(readOnly = true)
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public String login(LoginVO loginVO) {
        // 判断获取的loginVO的手机号和密码是否为空
        String mobile = loginVO.getMobile();
        String password = loginVO.getPassword();

        if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
            throw new GuliException(20001, "登录失败!");
        }

        // 从数据库中查询phone对应的对象
        QueryWrapper<UcenterMember> ucenterMemberQueryWrapper = new QueryWrapper<>();
        ucenterMemberQueryWrapper.eq("mobile", mobile);
        UcenterMember ucenterMember = baseMapper.selectOne(ucenterMemberQueryWrapper);

        // 无对象,则抛出异常
        if (ucenterMember == null) {
            throw new GuliException(20001, "登陆失败!");
        }

        // 校验密码
        if (!MD5.encrypt(password).equals(ucenterMember.getPassword())) {
            throw new GuliException(20001, "登陆失败!");
        }

        // 校验用户是否被禁用
        if (ucenterMember.getIsDisabled()) {
            throw new GuliException(20001, "登陆失败!");
        }

        String jwtToken = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());
        return jwtToken;
    }

    @Override
    @Transactional(readOnly = false, rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void register(RegisterVO registerVO) {
        // 获取注册信息
        //获取注册信息,进行校验
        String nickname = registerVO.getNickname();
        String mobile = registerVO.getMobile();
        String password = registerVO.getPassword();
        String code = registerVO.getCode();

        // 判断VO的属性值是否为空
        if (StringUtils.isEmpty(mobile)
                || StringUtils.isEmpty(nickname)
                || StringUtils.isEmpty(code)
                || StringUtils.isEmpty(password)) {
            throw new GuliException(20001, "注册失败!");
        }

        // 从redis中取出验证码,注意autowired时写明泛型
        String redisCode = redisTemplate.opsForValue().get(mobile);
        // 校验验证码是否为空或相等
        if (StringUtils.isEmpty(redisCode) || !code.equals(redisCode)) {
            throw new GuliException(20001, "注册失败!");
        }

        // 查询数据库中是否有相同手机号的对象
        QueryWrapper<UcenterMember> ucenterMemberQueryWrapper = new QueryWrapper<>();
        ucenterMemberQueryWrapper.eq("mobile", mobile);
        Integer count = baseMapper.selectCount(ucenterMemberQueryWrapper);
        if (count == 0) {
            throw new GuliException(20001, "注册失败!");
        }

        // 将数据保存到数据库
        UcenterMember ucenterMember = new UcenterMember();
        BeanUtils.copyProperties(registerVO, ucenterMember);
        // 将密码加密后存储
        ucenterMember.setPassword(MD5.encrypt(password));
        // 禁用默认为false
        ucenterMember.setIsDisabled(false);
        // 设置默认头像
        ucenterMember.setAvatar("https://project-guli-education.oss-cn-chengdu.aliyuncs.com/2022/02/17/1c592678eb374548a316df1e4eb92960file.png");
        baseMapper.insert(ucenterMember);
        // 插入数据完成后将redis中的验证码清除
        redisTemplate.delete(mobile);
    }

    @Override
    public LoginInfoVO getLoginInfo(String memberIdByJwtToken) {
        if (StringUtils.isEmpty(memberIdByJwtToken)) {
            throw new GuliException(20001, "信息为空!");
        }

        // 根据id查询数据库
        LoginInfoVO loginInfoVO = new LoginInfoVO();
        UcenterMember ucenterMember = baseMapper.selectById(memberIdByJwtToken);
        BeanUtils.copyProperties(ucenterMember , loginInfoVO);

        return loginInfoVO;
    }

    // 查询是否有vx号相同的用户
    @Override
    public UcenterMember getByOpenId(String openId) {
        QueryWrapper<UcenterMember> ucenterMemberQueryWrapper = new QueryWrapper<>();
        ucenterMemberQueryWrapper.eq("openid" , openId);
        UcenterMember ucenterMember = baseMapper.selectOne(ucenterMemberQueryWrapper);
        return ucenterMember;
    }
}

前端

  • 安装element-ui 和 vue-qriously
    执行命令安装
    npm install element-ui
    npm install vue-qriously
    安装js-cookie插件
    npm install js-cookie
  • 修改配置文件 nuxt-swiper-plugin.js,使用插件
    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)
  • 在api文件夹中创建注册的js文件,定义接口
    register.js
    login.js

    import request from '@/utils/request'
    export default {
     // 注册账号
     register(registerVO) {
         return request({
             url: `/ucenterservice/ucenter-member/register`,
             method: 'post',
             data: registerVO
         })
     },
     // 发送验证码
     sendMessage(phone) {
         return request({
             url: `/msmservice/msm-msgservice/send/${phone}`,
             method: 'get'
         })
     }
    }
    
    import request from '@/utils/request'
    export default {
     // 登录账号
     login(loginVO) {
         return request({
             url: `/ucenterservice/ucenter-member/login`,
             method: 'post',
             data: loginVO
         })
     },
     // 通过token获取用户的信息
     getLoginInfo() {
         return request({
             url: `/ucenterservice/ucenter-member/getLoginInfo`,
             method: 'get'
         })
     }
    }
    
  • 在pages文件夹中创建注册和登录页面,调用方法

    • 在layouts创建布局页面
      sign.vue

      <template>
      <div class="sign">
      <!--标题-->
      <div class="logo">
      <img src="~/assets/img/logo.png" alt="logo">
      </div>
      <!--表单-->
      <nuxt/>
      </div>
      </template>
      
    • 创建注册页面
      ```vue


   -  创建登录页面  
```vue
<template>
  <div class="main">
    <div class="title">
      <a class="active" href="login/login">登录</a>
      <span>·</span>
      <a href="/register/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/login.js";

export default {
  layout: "sign",

  data() {
    return {
      user: {
        mobile: "",
        password: "",
      },
      loginInfo: {},
    };
  },

  methods: {
    submitLogin() {
      loginApi
        .login(this.user)
        .then((response) => {
          if (response.data.success) {
            // 把token保存在cookie中、也可以放在localStorage中
            // 第一个参数为cookie名称,第二个参数为cookie的数据,第三个参数cookie的作用范围
            cookie.set("guli_token", response.data.data.token, {
              domain: "localhost",
            });
            // 登录成功后根据token获取他用户信息
            loginApi.getLoginInfo().then((response) => {
              // 将用户信息保存在loginInfo对象中
              this.loginInfo = response.data.data.loginInfoVO;
              // 再将用户信息通过token传到其他页面
              cookie.set("guli_ucenter", JSON.stringify(this.loginInfo), {
                domain: "localhost",
              });
              // 路由跳转主页面
              this.$router.push({ path: "/" });
            });
          }
        })
        .catch((error) => {
          // 提示登录成功
          this.$message({
            type: "error",
            message: "登录失败",
          });
        });
    },
    // 校验手机号格式
    checkPhone(rule, value, callback) {
      if (!/^1[34578]\d{9}$/.test(value)) {
        return callback(new Error("手机号码格式不正确"));
      }
      return callback();
    },
  },
};
</script>
<style>
.el-form-item__error {
  z-index: 9999999;
}
</style>
  • 在request.js添加拦截器,用于传递token信息
    ```javascript import axios from ‘axios’ import { MessageBox, Message } from ‘element-ui’ import cookie from ‘js-cookie’ // 创建axios实例 const service = axios.create({ //baseURL: ‘http://qy.free.idcfengye.com/api‘, // api 的 base_url //baseURL: ‘http://localhost:8210‘, // api 的 base_url baseURL: ‘http://localhost:9001‘, timeout: 15000 // 请求超时时间

}) // http request 拦截器 service.interceptors.request.use( config => { //debugger if (cookie.get(‘guli_token’)) { config.headers[‘token’] = cookie.get(‘guli_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


-  修改layouts中的default.vue页面,显示登录之后的用户信息  
```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() {
    // 路径中无token值
    this.showLoginInfo();
  },
  methods: {
    // 显示登录信息
    showLoginInfo() {
      // 获取cookie中的数据
      var jsonStr = cookie.get("guli_ucenter");
      if (jsonStr) {
        // 将字符传转化为JSON对象
        this.loginInfo = JSON.parse(jsonStr);
      }
    },
    logout() {
      // 清空cookie即可,将cookie赋值为空字符串
      cookie.set("guli_token", "", { domain: "localhost" });
      cookie.set("guli_ucenter", "", { domain: "localhost" });
      // 路由跳转到主页面
      this.$router.push({ path: "/" });
    },
  },
}
</script>
  • default.vue页面**显示登录之后的用户信息
    <!-- / nav -->
    <ul class="h-r-login">
     <li v-if="!loginInfo.id" id="no-login">
         <a href="/login" title="登录">
             <em class="icon18 login-icon">&nbsp;</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">&nbsp;</em>
         </a>
         <q class="red-point" style="display: none">&nbsp;</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>
     <!-- /未登录显示第1 li;登录后显示第2,3 li -->
    </ul>
    

微信登录

调用微信接口流程图

image.png

后端

  • 导入依赖

    <!--httpclient-->
    <dependency>
     <groupId>org.apache.httpcomponents</groupId>
     <artifactId>httpclient</artifactId>
    </dependency>
    <!--commons-io-->
    <dependency>
     <groupId>commons-io</groupId>
     <artifactId>commons-io</artifactId>
    </dependency>
    <!--gson-->
    <dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
    </dependency>
    
  • 创建常量类绑定配置
    yml配置

    @Component
    public class ConstantWechatUtil implements InitializingBean {
     @Value("${wx.open.app_id}")
     private String appId;
    
     @Value("${wx.open.app_secret}")
     private String appSecret;
    
     @Value("${wx.open.redirect_url}")
     private String redirectUrl;
    
     public static String WX_OPEN_APP_ID;
     public static String WX_OPEN_APP_SECRET;
     public static String WX_OPEN_REDIRECT_URL;
    
     @Override
     public void afterPropertiesSet() throws Exception {
         WX_OPEN_APP_ID = appId;
         WX_OPEN_APP_SECRET = appSecret;
         WX_OPEN_REDIRECT_URL = redirectUrl;
     }
    }
    
    wx:
    open:
     app_id:                     # 微信开放平台 appid
     app_secret:                 # 微信开放平台 appsecret
     redirect_url: http://localhost:8160/ucenterservice/ucenter-wxlogin/callback # 微信开放平台 重定向url
    
  • controller,wechatLogin为调用微信的扫码地址,callback为微信的回调,回调后将微信生成的令牌带去访问微信提供的地址,即可得到微信扫描用户的信息(其中使用了httpclient的机制),保存在数据库中之后,通过jwt生成用户id的token,返回至前端页面
    ```java @CrossOrigin @Controller // 跳转页面 @RequestMapping(“/ucenterservice/ucenter-wxlogin”) public class WechatLoginController {

    @Autowired private RedisTemplate redisTemplate;

    @Autowired private UcenterMemberService ucenterMemberService;

    // 扫码之后的回调 @GetMapping(“callback”) public String callback(String code, String state) {

     // 获取redis中的state和传过来的state进行判断
     String redisState = redisTemplate.opsForValue().get("wechar-open-state");
    
     // 不相等抛出异常
     if (!state.equals(redisState)) {
         throw new GuliException(20001, "非法访问!");
     }
    
     // 使用httpclient,向认证服务器发送请求换取access_token
     String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
             "?appid=%s" +
             "&secret=%s" +
             "&code=%s" +
             "&grant_type=authorization_code";
    
     String accessTokenUrl = String.format(baseAccessTokenUrl,
             ConstantWechatUtil.WX_OPEN_APP_ID,
             ConstantWechatUtil.WX_OPEN_APP_SECRET,
             code);
    
     String result = null;
     try {
         // 获取访问后返回的值
         result = HttpClientUtils.get(accessTokenUrl);
     } catch (Exception exception) {
         throw new GuliException(20001, "登录失败!");
     }
    
     // 使用gson解析返回的结果
     Gson gson = new Gson();
     HashMap map = gson.fromJson(result, HashMap.class);
     String accessToken = (String) map.get("access_token");
     String openId = (String) map.get("openid");
    
     // 查询数据库中是否存在登录过的用户
     UcenterMember ucenterMember = ucenterMemberService.getByOpenId(openId);
     String jwtToken = null;
    
     // 数据库中不存在,则可以使用微信登录
     if (ucenterMember == null) {
         //访问微信的资源服务器,获取用户信息
         String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                 "?access_token=%s" +
                 "&openid=%s";
         // 拼接访问地址
         String userInfoUrl = String.format(baseUserInfoUrl, accessToken, openId);
    
         String userInfoResult = null;
         // 获取返回的用户信息
         try {
             userInfoResult = HttpClientUtils.get(userInfoUrl);
         } catch (Exception exception) {
             throw new GuliException(20001, "登录失败!");
         }
    
         // 转化为字符串
         HashMap hashMap = gson.fromJson(userInfoResult, HashMap.class);
         // 获取用户名和头像
         String nickName = (String) hashMap.get("nickname");
         String headImgUrl = (String) hashMap.get("headimgurl");
    
         // 保存用户信息在数据库中
         UcenterMember member = new UcenterMember();
         member.setOpenid(openId);
         member.setAvatar(headImgUrl);
         member.setNickname(nickName);
         ucenterMemberService.save(member);
    
         // 直接存入session在不同的域名中会存在跨域,通过jwt生p成token,将首页显示用户的信息通过token传输,防止跨域问题
         jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
     }
     // 跳转主页面
     return "redirect:http://localhost:3000?token=" + jwtToken;
    

    }

    @GetMapping(“login”) public String wechatLogin() {

     // 微信开放平台授权baseUrl
     String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
             "?appid=%s" +
             "&redirect_uri=%s" +
             "&response_type=code" +
             "&scope=snsapi_login" +
             "&state=%s" +
             "#wechat_redirect";
    
     // 回调地址
     String redirectUrl = ConstantWechatUtil.WX_OPEN_REDIRECT_URL;
    
     try {
         redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
     } catch (Exception exception) {
         throw new GuliException(20001, exception.getMessage());
     }
    
     // 防止csrf攻击(跨站请求伪造攻击)
     // 创建标识,作为判断,一般情况可以使用随机数
     String state = "atguigu-peanut";
     // 并将标识存入redis中,设置过期事件为10分钟
     redisTemplate.opsForValue().set("wechar-open-state", state, 10, TimeUnit.MINUTES);
    
     // 拼接跳转路径
     String qrcodeUrl = String.format(
             baseUrl,
             ConstantWechatUtil.WX_OPEN_APP_ID,
             redirectUrl,
             state
     );
     return "redirect:" + qrcodeUrl;
    

    }

}


-  创建httpclient工具类,将老师资料提供的工具类拷贝到项目中 
-  service中实现查询是否有appid重复的用户  
```java
@Override
public UcenterMember getByOpenId(String openId) {
    QueryWrapper<UcenterMember> ucenterMemberQueryWrapper = new QueryWrapper<>();
    ucenterMemberQueryWrapper.eq("openid" , openId);
    UcenterMember ucenterMember = baseMapper.selectOne(ucenterMemberQueryWrapper);
    return ucenterMember;
}

前端

  • 修改default显示页面,通过判断地址中是否带token=XXX的参数来调用显示方法

    created() {
     // 路径中是否有wechat重定向过来的token
     this.token = this.$route.query.token;
     // 有即进行wechat登录
     if(this.token) {
       this.wechatLogin();
     }
    
     // 路径中无token值
     this.showLoginInfo();
    },
    methods: {
     // 微信登录显示
     wechatLogin(){
       console.log(this.token)
       // 将token设置到cookie中
       cookie.set("guli_token", this.token, {
               domain: "localhost",
             });
       cookie.set("guli_ucenter","", {
               domain: "localhost",
           });
       // 调用查询根据token查询信息的后端接口
       loginApi.getLoginInfo()
       .then((response) => {
           this.loginInfo = response.data.data.loginInfoVO;
           cookie.set("guli_ucenter",JSON.stringify(this.loginInfo), {
               domain: "localhost",
           });
       })
     },