文章目录

  • 加密算法
    • 1. 可逆加密算法
      • 1.1 对称加密
      • 1.2 非对称加密
    • 2. 不可逆加密算法
    • 3. Base64编码
    • 4. Base64URL算法
  • JWT令牌
    • 1. 访问令牌的类型
    • 2. JWT的组成—JWT头、有效载荷和签名
      • 2.1 JWT头
      • 2.2 有效载荷
      • 2.3 签名哈希
    • 3. JWT的原则
    • 4. JWT的用法☆
    • 5. JWT问题和趋势
    • 6. JJWT签发与验证token
  • 用户登陆功能☆☆
    • 1. 单一服务器模式
      • 传统用户身份验证
    • 2. `SSO`(single sign on)模式
    • 3. Token模式
    • 4. 第三方应用授权登录
  • CAS实现单点登录原理
    • 1. CAS是什么?
    • 2. CAS实现SSO单点登录原理
    • 3. CAS的安全性
  • JWT实现单点登录
    • 1. 搭建环境
      • 引入依赖
      • 创建JWT工具类☆
      • 创建MD5工具类
    • 2. 登录的后端逻辑
      • 2.1 创建LoginVo用于数据封装
      • 2.2 创建controller编写登录方法
      • 2.3 创建service接口和实现类
        • UcenterMemberService接口
        • UcenterMemberServiceImpl实现类
    • 3. 登录的前端逻辑
      • 3.1 前端页面
        • 补充—根据token查询用户信息的后端逻辑
      • 3.2 login.js
      • 3.3 修改layouts中的default.vue页面
      • 3.4 在request.js添加拦截器,用于传递token信息
  • 微信登陆功能实现
    • 1. 实现微信登录准备工作
    • 2. 得到微信登录二维码
      • 2.1 添加配置
      • 2.2 创建常量类
      • 2.3 创建controller
      • 2.4 授权url参数说明
      • 2.5 测试
    • 3. 完善微信登录后端
      • 3.1 后台开发
        • 添加依赖
      • 3.2 创建httpclient工具类
      • 3.3 创建回调controller方法
    • 4. 微信登陆前端部分
      • 4.1 首页布局default.vue页面

加密算法

加密算法包括可逆加密算法和不可逆加密算法。

1. 可逆加密算法

解释: 加密后, 密文可以反向解密得到密码原文。

1.1 对称加密

文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥

  • 解释: 在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。
  • 优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
  • 缺点: 没有非对称加密安全。
  • 用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。

常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256

1.2 非对称加密

两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密。

  • 解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端。

  • 加密与解密:

    • 私钥加密,持有私钥或公钥才可以解密

    • 公钥加密,持有私钥才可解密

  • 签名:

    • 私钥签名, 持有公钥进行验证是否被篡改过.
  • 优点: 非对称加密与对称加密相比,其安全性更好;

  • 缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。

  • 用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.

常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)

2. 不可逆加密算法

  • 解释: 一旦加密就不能反向解密得到密码原文。
  • 种类: Hash加密算法, 散列算法, 摘要算法等。
  • 用途:一般用于效验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。
  • 常见的不可逆加密算法有: MD5、SHA、HMAC

3. Base64编码

  1. Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。Base64编码可用于在HTTP环境下传递较长的标识信息。采用Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。注意:Base64只是一种编码方式,不算加密方法。
  2. 在线编码工具:http://www.jsons.cn/img2base64/

4. Base64URL算法

  1. JWT头和有效载荷序列化的算法都用到了**Base64URL。该算法和常见Base64算法**类似,稍有差别。
  2. 作为令牌的JWT可以放在URL中(例如**api.example/?token=xxx**)。
  3. **Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL**算法。

JWT令牌

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

1. 访问令牌的类型

2. JWT的组成—JWT头、有效载荷和签名

  1. 典型的,一个JWT看起来如下图
  2. 该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
  3. 每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名

2.1 JWT头

JWT头部分是一个描述JWT元数据的JSON对象

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

2.2 有效载荷

  1. 有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

  2. JWT指定七个默认字段供选择。

  3. iss:发行人
    exp:到期时间
    sub:主题
    aud:用户
    nbf:在此之前不可用
    iat:发布时间
    jti:JWT ID用于标识该JWT
    
  4. 除以上默认字段外,我们还可以自定义私有字段,如下例

  5. {"sub": "1234567890","name": "Helen","admin": true
    }
    
  6. 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

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

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

2.3 签名哈希

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

  2. 首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。

  3. 然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

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

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

3. JWT的原则

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

4. JWT的用法☆

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

5. JWT问题和趋势

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

6. JJWT签发与验证token

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

用户登陆功能☆☆

1. 单一服务器模式

传统用户身份验证

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

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

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

2. SSO(single sign on)模式

  1. 分布式,SSO(single sign on)模式
  2. 优点 :
    1. 用户身份信息独立管理,更好的分布式管理。
    2. 可以自己扩展安全策略。
  3. 缺点:
    1. 认证服务器访问压力较大。

3. Token模式

  1. 业务流程图{用户访问业务时,必须登录的流程}
  2. 优点:
    1. 无状态: token无状态,session有状态的。
    2. 基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT)
  3. 缺点:
    1. 占用带宽。
    2. 无法在服务器端销毁。
  4. 注:基于微服务开发,选择token的形式相对较多,因此我使用token作为用户认证的标准。并进行详细地总结

4. 第三方应用授权登录

在APP或者网页接入一些第三方应用时,经常会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录。

CAS实现单点登录原理

这里仅仅做一些CAS原理上的介绍,因为项目中使用的是JWT实现单点登录。

1. CAS是什么?

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:

  1. 开源的企业级单点登录解决方案。
  2. CAS Server 为需要独立部署的 Web 应用。其实CAS服务端其实就是一个war包。
  3. CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

2. CAS实现SSO单点登录原理

CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护 Web 应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同时, CAS Client 会分析 HTTP 请求中是否包含请求 Service Ticket,如果没有,则说明该用户是没有经过认证的;于是 CAS Client 会重定向用户请求到 CAS Server ,并传递 Service (要访问的目的资源地址)。在用户认证过程,如果用户提供了正确的身份认证信息( Credentials) , CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket ,并缓存以待将来验证,并且重定向用户到 Service 所在地址(附带刚才产生的 Service Ticket ) , 并为客户端浏览器设置一个 Ticket Granted Cookie ( TGC ) ; CAS Client 在拿到 Service 和新产生的 Ticket 过后,在 Step5 和 Step6 中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。

SSO单点登录访问流程主要有以下步骤:

  1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。

  2. 定向认证:SSO客户端会重定向用户请求到SSO服务器。

  3. 用户认证:用户身份认证。

  4. 发放票据:SSO服务器会产生一个随机的Service Ticket。

  5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。

  6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。

CAS 系统中设计了 5 中票据: TGC 、 ST 、 PGT 、 PGTIOU 、 PT 。

3. CAS的安全性

CAS 的安全性仅仅依赖于 SSL。CAS默认使用的是HTTPS协议,如果使用HTTPS协议需要SSL安全证书(需向特定的机构申请和购买) 。如果对安全要求不高或是在开发测试阶段,可使用HTTP协议。使用的时候可以搜索修改配置的过程,让CAS使用HTTP协议。

TGC安全性

  • 对于一个 CAS 用户来说,最重要是要保护它的 TGC ,如果 TGC 不慎被 CAS Server 以外的实体获得, Hacker 能够找到该 TGC ,然后冒充 CAS 用户访问 所有 授权资源。 PGT 的角色跟 TGC 是一样的。
  • 从基础模式可以看出, TGC 是 CAS Server 通过 SSL 方式发送给终端用户,因此,要截取 TGC 难度非常大,从而确保 CAS 的安全性。TGT 的存活周期默认为 120 分钟。

ST安全性

  • ST ( Service Ticket )是通过 Http 传送的,因此网络中的其他人可以 Sniffer 到其他人的 Ticket 。 CAS 通过以下几方面来使 ST 变得更加安全(事实上都是可以配置的):

    1. ST 只能使用一次,CAS 协议规定,无论 Service Ticket 验证是否成功, CAS Server 都会清除服务端缓存中的该Ticket ,从而可以确保一个 Service Ticket 不被使用两次。
    2. ST 在一段时间内失效,CAS 规定 ST 只能存活一定的时间,然后 CAS Server 会让它失效。默认有效时间为 5 分钟。
    3. ST 是基于随机数生成的,ST 必须足够随机,如果 ST 生成规则被猜出, Hacker 就等于绕过 CAS 认证,直接访问 对应的服务。

JWT实现单点登录

1. 搭建环境

本项目:

  • 持久层框架使用的是MyBatisPlus。

  • 前端使用Nuxt框架搭建。

引入依赖

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

创建JWT工具类☆

  1. public static final long EXPIR,token过期时间
  2. public static final String APP_SECRET ,自定义签名
  3. public static String getJwtToken(String id, String nickname) ,根据id和nickname获取token
  4. public static boolean checkToken(String jwtToken) ,判断token是否存在与有效
  5. public static boolean checkToken(HttpServletRequest request) ,判断token是否存在与有效
  6. public static String getMemberIdByJwtToken(HttpServletRequest request) ,根据token获取会员id
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;
/*** @Date 2020/4/28 21:50* @Version 10.21* @Author DuanChaojie*/
public class JwtUtils {public static final long EXPIRE = 1000 * 60 * 60 * 3;// 自定义的签名public static final String APP_SECRET = "DuanChaojieAndMiaomiao";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");}
}

创建MD5工具类

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public final class MD5 {/*** encrypt()MD5加密的方式*/public static String encrypt(String strSrc) {try {char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8','9', 'a', 'b', 'c', 'd', 'e', 'f' };byte[] bytes = strSrc.getBytes();// 确定使用MD5算法MessageDigest md = MessageDigest.getInstance("MD5");// 使用指定的字节数组对摘要进行更新md.update(bytes);// 通过执行诸如填充之类的最终操作来完成哈希计算。进行此调用后,摘要将重置。bytes = md.digest();int j = bytes.length;char[] chars = new char[j * 2];int k = 0;for (int i = 0; i < bytes.length; i++) {byte b = bytes[i];chars[k++] = hexChars[b >>> 4 & 0xf];chars[k++] = hexChars[b & 0xf];}return new String(chars);} catch (NoSuchAlgorithmException e) {e.printStackTrace();throw new RuntimeException("MD5加密出错!!+" + e);}}
}

2. 登录的后端逻辑

2.1 创建LoginVo用于数据封装

LoginVo登录对象

/*** @Date 2020/4/28 23:01* @Version 10.21* @Author DuanChaojie*/
@Data
@ApiModel(value="登录对象", description="登录对象")// swagger2注解
public class LoginVo {@ApiModelProperty(value = "手机号")private String mobile;@ApiModelProperty(value = "密码")private String password;
}

2.2 创建controller编写登录方法

UcenterMemberController.java

此处的R是定义的统一返回结果对象,类似于ResponseEntity,详细的定义过程可以参考下面这篇文章:https://blog.csdn.net/weixin_45267102/article/details/108449377

@RestController
@RequestMapping("/user/user-member")
@CrossOrigin// 解决跨域问题
public class UcenterMemberController {@Autowiredprivate UcenterMemberService ucenterMemberService;// 此处的R是定义的统一返回结果对象,类似于ResponseEntity@PostMapping("/loginUser")public R loginUser(@RequestBody LoginVo loginVo) {String token = ucenterMemberService.loginUser(loginVo);// 拿到处理后的token,返回到前端return R.ok().data("token",token);}
}

2.3 创建service接口和实现类

UcenterMemberService接口
public interface UcenterMemberService extends IService<UcenterMember> {/*** 登陆的方法*/String loginUser(LoginVo loginVo);
}
UcenterMemberServiceImpl实现类

这里使用到了统一异常处理,为什么使用以及具体使用方法见下面这篇文章:https://blog.csdn.net/weixin_45267102/article/details/108449918

@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {@Overridepublic String loginUser(LoginVo loginVo) {String mobile = loginVo.getMobile();String password = loginVo.getPassword();// 校验数据,非空校验以及规格校验前端是必须的if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {throw new GuliException(20001,"请重新输入!");}// 创建查询条件QueryWrapper wrapper = new QueryWrapper();wrapper.eq("mobile",mobile);// 根据手机号往数据库查询UcenterMember对象UcenterMember ucenterMember = baseMapper.selectOne(wrapper);if (ucenterMember == null) {// 此处是项目定义的全局异常throw new GuliException(20001,"请重新输入!");}// 检验是否封号if( ucenterMember.getIsDisabled()) {throw new GuliException(20001,"该账号已冻结!");}// 校验密码,需要注意的是数据库中的密码是经过加密后的(是在注册的时候加密后存到数据库中的)if (!MD5.encrypt(password).equals(ucenterMember.getPassword())) {throw new GuliException(20001,"密码错误!");}// 如果代码走到这里,则说明手机号和密码和数据库里面用户信息是匹配的// 此时根据用户的id和nickName生成tokenString token = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());return token;}
}

到此为止后端的逻辑已经结束了,现在我们梳理一下后端的流程:

前端通过手机号和密码向后端发出登录请求,去数据校验用户手机号以及密码是否正确。如果手机号和密码都正确就返回经过加密后的token到前端,token中包含此用户的id和nickName。

3. 登录的前端逻辑

3.1 前端页面

login.vue,在这个页面输入登陆信息,向后端发送登陆请求。

<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="_self" href="http://localhost:8150/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  loginApi from "@/api/login"// 需要使用npm install js-cookie// 安装js-cookie插件import cookie from 'js-cookie'export default {layout: 'sign',data () {return {user:{mobile:'',password:''},loginInfo:{}}},methods: {submitLogin(){// 第一步 调用接口进行登录,返回token字符串loginApi.submitLogin(this.user).then( response => {// 第二步 获取token字符串放到cookie里面// 第一个参数cookie名称,第二个参数值,第三个参数作用范围cookie.set('guli_token',response.data.data.token,{domain: 'localhost'})//第四步 调用接口 根据token获取用户信息,为了首页面显示loginApi.getLoginInfo(response.data.data.token).then( response => {// 提示消息this.$message({type: 'success',message: '登录成功!'})this.loginInfo = response.data.data.ucenterMember//获取返回用户信息,放到cookie里面cookie.set('guli_ucenter',this.loginInfo,{domain: "localhost"})//跳转页面this.$route.push({path:'/'})//window.location.href = '/'})})},// 校验手机号格式,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>

上面的代码,我们很容易明白我们第一次登陆的时候,是通过手机号密码去拿到加密后的token,把token放到cookie中,然后根据token去查询用户信息,后端拿到token,通过JWT工具类根据request对象获取头信息,返回用户id。然后根据用户id,去查询用户信息返回到前端。

然而我们是要实现的是单点登陆功能,这又是怎么实现的呢?看下面的分析。

补充—根据token查询用户信息的后端逻辑
@RestController
@RequestMapping("/user/user-member")
@CrossOrigin
public class UcenterMemberController {@Autowiredprivate UcenterMemberService ucenterMemberService;// ...@GetMapping("/getLoginInfo")public R getLoginInfo(HttpServletRequest request) {// 调用jwt工具类的方法。根据request对象获取头信息,返回用户idString memberId = JwtUtils.getMemberIdByJwtToken(request);// 查询数据库根据用户id获取用户信息UcenterMember ucenterMember = ucenterMemberService.getById(memberId);return R.ok().data("ucenterMember",ucenterMember);}}

3.2 login.js

import request from '@/utils/request'export default {// 登录的方法,获取token信息submitLogin(userInfo) {return request({url: `/user/user-member/loginUser`,method: 'post',data: userInfo})},// 根据token获取用户信息getLoginInfo(token) {return request({url: `/user/user-member/getLoginInfo`,method: 'get'})},
}

3.3 修改layouts中的default.vue页面

因为这个是整个网站的页面的布局页,再created()中去根据var jsonStr = cookie.get("guli_ucenter");去拿到用户的信息。这样就实现了单点登录的功能。

<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() {// 从cookie中获取用户的信息this.showLoginInfo()},methods: {showLoginInfo(){var jsonStr = cookie.get("guli_ucenter");if( jsonStr ) {this.loginInfo = JSON.parse(jsonStr);}},// 退出登陆的功能,即清空cookie信息,跳转到首页就可以了logout() {cookie.set('guli_ucenter', "", {domain: 'localhost'})cookie.set('guli_token', "", {domain: 'localhost'})//跳转页面window.location.href = "/"}   }};
</script>

本项目git地址:https://gitee.com/duanchaojie/guli-school.git

3.4 在request.js添加拦截器,用于传递token信息

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_urlbaseURL: 'http://localhost:9001',timeout: 15000 // 请求超时时间})// http request 拦截器
service.interceptors.request.use(config => {//debuggerif (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 => {//debuggerif (response.data.code == 28004) {console.log("response.data.resultCode是28004")// 返回 错误代码-1 清除ticket信息并跳转到登录页面//debuggerwindow.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

微信登陆功能实现

1. 实现微信登录准备工作

  1. 网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。 在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppIDAppSecret,申请微信登录且通过审核后,可开始接入流程。
  2. https://open.weixin.qq.com
  3. 准备工作:
    1. 注册
    2. 邮箱激活
    3. 完善开发者资料
    4. 开发者资质认证
      • 准备营业执照,1-2个工作日审批、300元
    5. 创建网站应用
      • 提交审核,7个工作日审批
    6. 熟悉微信登录流程
      • 参考文档:https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=e547653f995d8f402704d5cb2945177dc8aa4e7e&lang=zh_CN
  4. 获取access_token时序图
    1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
    2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
    3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

2. 得到微信登录二维码

根据appid,redirect_url,state获取二维码。

2.1 添加配置

此处使用的是尚硅谷在线教育项目—谷粒学院提供的app_id和app_secret。

application.yml

wx:open:# 微信开放平台 appidapp_id: wxed9954c01bb89b47# 微信开放平台 appsecretapp_secret: a7482517235173ddb4083788de60b90e# 微信开放平台 重定向urlredirect_url: http://guli.shop/api/ucenter/wx/callback

2.2 创建常量类

创建util包,创建ConstantPropertiesUtil.java常量类

/*** @Date 2020/4/30 0:55* @Version 10.21* @Author DuanChaojie*/
@Component
public class ConstantPropertiesUtil 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;@Overridepublic void afterPropertiesSet() throws Exception {WX_OPEN_APP_ID = appId;WX_OPEN_APP_SECRET = appSecret;WX_OPEN_REDIRECT_URL = redirectUrl;}
}

2.3 创建controller

  1. WxLoginController
  2. 注意路径/api/ucenter/wx/login,然后重定向到https://open.weixin.qq.com/connect/qrconnect…
    1. 需要参数有:

      1. appid 通过工具类获得。
      2. redirect_uri 通过工具类获得后,对redirect_url进行URLEncoder编码
      3. state
    2. String.format()使用详解:https://blog.csdn.net/anita9999/article/details/82346552
/*** @Date 2020/4/30 0:56* @Version 10.21* @Author DuanChaojie* 注意这里没有配置 @RestController,只是请求地址,不需要返回数据*/
@CrossOrigin
@Controller
@RequestMapping("/api/ucenter/wx")
public class WxLoginController {@Autowiredprivate UcenterMemberService ucenterMemberService;/*** 获取微信二维码* @return*/@GetMapping("/login")public String getWxCode() {// 微信开放平台授权baseUrl  %s相当于?代表占位符String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +"?appid=%s" +"&redirect_uri=%s" +"&response_type=code" +"&scope=snsapi_login" +"&state=%s" +"#wechat_redirect";//对redirect_url进行URLEncoder编码String redirectUrl = ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL;try {redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}//设置%s里面值String url = String.format(baseUrl,ConstantPropertiesUtil.WX_OPEN_APP_ID,redirectUrl,"atguigu");//重定向到请求微信地址里面return "redirect:"+url;}
}

2.4 授权url参数说明

参数 是否必须 说明
appid 应用唯一标识
redirect_uri 请使用urlEncode对链接进行处理
response_type 填code
scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即
state 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验

2.5 测试

  1. 访问:http://localhost:8006/api/ucenter/wx/login
  2. 访问授权url后会得到一个微信登录二维码

3. 完善微信登录后端

我们刚刚完成微信登录的第一步即:第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;

我们下面完成:

  1. 通过code参数加上AppIDAppSecret等,通过API换取access_token;
  2. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

注意:

  1. 由于我们做微信登录时很多人都在用同一个微信appidappsecret所以我们需要把当前微服务端口更换成8150,具体原因看下图,同时更改**nginx**的跳转规则。
  2. 同时我们需要使用到HttpClient的工具类。

3.1 后台开发

添加依赖
 <!--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>

3.2 创建httpclient工具类

  1. HttpClientUtils.java放入utils包
  2. 了解内容
package com.atguigu.userservice.utils;import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;/**
*  依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
* @author zhaoyb
*/
public class HttpClientUtils {public static final int connTimeout=10000;public static final int readTimeout=10000;public static final String charset="UTF-8";private static HttpClient client = null;static {PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();cm.setMaxTotal(128);cm.setDefaultMaxPerRoute(128);client = HttpClients.custom().setConnectionManager(cm).build();}public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);}public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);}public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,SocketTimeoutException, Exception {return postForm(url, params, null, connTimeout, readTimeout);}public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,SocketTimeoutException, Exception {return postForm(url, params, null, connTimeout, readTimeout);}public static String get(String url) throws Exception {return get(url, charset, null, null);}public static String get(String url, String charset) throws Exception {return get(url, charset, connTimeout, readTimeout);}/*** 发送一个 Post 请求, 使用指定的字符集编码.** @param url* @param body RequestBody* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3* @param charset 编码* @param connTimeout 建立链接超时时间,毫秒.* @param readTimeout 响应超时时间,毫秒.* @return ResponseBody, 使用指定的字符集编码.* @throws ConnectTimeoutException 建立链接超时异常* @throws SocketTimeoutException  响应超时* @throws Exception*/public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)throws ConnectTimeoutException, SocketTimeoutException, Exception {HttpClient client = null;HttpPost post = new HttpPost(url);String result = "";try {if (StringUtils.isNotBlank(body)) {HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));post.setEntity(entity);}// 设置参数Builder customReqConf = RequestConfig.custom();if (connTimeout != null) {customReqConf.setConnectTimeout(connTimeout);}if (readTimeout != null) {customReqConf.setSocketTimeout(readTimeout);}post.setConfig(customReqConf.build());HttpResponse res;if (url.startsWith("https")) {// 执行 Https 请求.client = createSSLInsecureClient();res = client.execute(post);} else {// 执行 Http 请求.client = HttpClientUtils.client;res = client.execute(post);}result = IOUtils.toString(res.getEntity().getContent(), charset);} finally {post.releaseConnection();if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {((CloseableHttpClient) client).close();}}return result;}/*** 提交form表单** @param url* @param params* @param connTimeout* @param readTimeout* @return* @throws ConnectTimeoutException* @throws SocketTimeoutException* @throws Exception*/public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,SocketTimeoutException, Exception {HttpClient client = null;HttpPost post = new HttpPost(url);try {if (params != null && !params.isEmpty()) {List<NameValuePair> formParams = new ArrayList<NameValuePair>();Set<Entry<String, String>> entrySet = params.entrySet();for (Entry<String, String> entry : entrySet) {formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));}UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);post.setEntity(entity);}if (headers != null && !headers.isEmpty()) {for (Entry<String, String> entry : headers.entrySet()) {post.addHeader(entry.getKey(), entry.getValue());}}// 设置参数Builder customReqConf = RequestConfig.custom();if (connTimeout != null) {customReqConf.setConnectTimeout(connTimeout);}if (readTimeout != null) {customReqConf.setSocketTimeout(readTimeout);}post.setConfig(customReqConf.build());HttpResponse res = null;if (url.startsWith("https")) {// 执行 Https 请求.client = createSSLInsecureClient();res = client.execute(post);} else {// 执行 Http 请求.client = HttpClientUtils.client;res = client.execute(post);}return IOUtils.toString(res.getEntity().getContent(), "UTF-8");} finally {post.releaseConnection();if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {((CloseableHttpClient) client).close();}}}/*** 发送一个 GET 请求** @param url* @param charset* @param connTimeout  建立链接超时时间,毫秒.* @param readTimeout  响应超时时间,毫秒.* @return* @throws ConnectTimeoutException   建立链接超时* @throws SocketTimeoutException   响应超时* @throws Exception*/public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)throws ConnectTimeoutException,SocketTimeoutException, Exception {HttpClient client = null;HttpGet get = new HttpGet(url);String result = "";try {// 设置参数Builder customReqConf = RequestConfig.custom();if (connTimeout != null) {customReqConf.setConnectTimeout(connTimeout);}if (readTimeout != null) {customReqConf.setSocketTimeout(readTimeout);}get.setConfig(customReqConf.build());HttpResponse res = null;if (url.startsWith("https")) {// 执行 Https 请求.client = createSSLInsecureClient();res = client.execute(get);} else {// 执行 Http 请求.client = HttpClientUtils.client;res = client.execute(get);}result = IOUtils.toString(res.getEntity().getContent(), charset);} finally {get.releaseConnection();if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {((CloseableHttpClient) client).close();}}return result;}/*** 从 response 里获取 charset** @param ressponse* @return*/@SuppressWarnings("unused")private static String getCharsetFromResponse(HttpResponse ressponse) {// Content-Type:text/html; charset=GBKif (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {String contentType = ressponse.getEntity().getContentType().getValue();if (contentType.contains("charset=")) {return contentType.substring(contentType.indexOf("charset=") + 8);}}return null;}/*** 创建 SSL连接* @return* @throws GeneralSecurityException*/private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {try {SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {return true;}}).build();SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {@Overridepublic boolean verify(String arg0, SSLSession arg1) {return true;}@Overridepublic void verify(String host, SSLSocket ssl)throws IOException {}@Overridepublic void verify(String host, X509Certificate cert)throws SSLException {}@Overridepublic void verify(String host, String[] cns,String[] subjectAlts) throws SSLException {}});return HttpClients.custom().setSSLSocketFactory(sslsf).build();} catch (GeneralSecurityException e) {throw e;}}public static void main(String[] args) {try {String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);//String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");/*Map<String,String> map = new HashMap<String,String>();map.put("name", "111");map.put("page", "222");String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/System.out.println(str);} catch (ConnectTimeoutException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (SocketTimeoutException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}

3.3 创建回调controller方法

  1. 通过code参数加上AppIDAppSecret等,通过API换取access_token,通过httpClient工具类向该地址:https://api.weixin.qq.com/sns/oauth2/access_token…

  2. 得到accessTokenInfo,使用gsonaccessTokenInfo转换成hashMapgson.fromJson(accessTokenInfo, HashMap.class)

  3. hashMap中获取openidaccess_token

  4. 因为每一个微信账号的openid都不一样,所以我们先通过openid去数据库查询是否存在该UcenterMember对象,如果UcenterMember为空,我们则执行第三步。如果UCenterMember不为空,则根据其中的id和nickname直接生产token重定向到首页。

    1. 接口略。

    2.     @Overridepublic UcenterMember getOpenIdMember(String openid) {QueryWrapper<UcenterMember> queryWrapper = new QueryWrapper<>();queryWrapper.eq("openid", openid);UcenterMember ucenterMember = baseMapper.selectOne(queryWrapper);return ucenterMember;}
      
  5. 第三步:拿着得到accsess_tokenopenid,再去请求微信提供固定的地址,获取到扫描人信息userInfo,同时使用gson将其转换成hashMap

  6. hashMap中获取用户的信息,加入到数据库。

  7. 最后使用jwt根据member对象的id和nickname生成token字符串,最后带着token重定向到首页。

/*** @Date 2020/4/30 0:56* @Version 10.21* @Author DuanChaojie* //注意这里没有配置 @RestController,只是请求地址,不需要返回数据*/
@CrossOrigin
@Controller
@RequestMapping("/api/ucenter/wx")
public class WxLoginController {@Autowiredprivate UcenterMemberService ucenterMemberService;/*** 获取扫描人信息,添加数据* @param code* @param state* @param session* @return*/@GetMapping("callback")public String callback(String code, String state, HttpSession session) {try {//1 获取code值,临时票据,类似于验证码//2 拿着code请求 微信固定的地址,得到两个值 accsess_token 和 openidString baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +"?appid=%s" +"&secret=%s" +"&code=%s" +"&grant_type=authorization_code";//拼接三个参数 :id  秘钥 和 code值String accessTokenUrl = String.format(baseAccessTokenUrl,ConstantPropertiesUtil.WX_OPEN_APP_ID,ConstantPropertiesUtil.WX_OPEN_APP_SECRET,code);//请求这个拼接好的地址,得到返回两个值 accsess_token 和 openid//使用httpclient发送请求,得到返回结果String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);//从accessTokenInfo字符串获取出来两个值 accsess_token 和 openid//把accessTokenInfo字符串转换map集合,根据map里面key获取对应值//使用json转换工具 Gson// Gson是谷歌官方推出的支持 JSON -- Java Object 相互转换的 Java序列化/反序列化库Gson gson = new Gson();HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);String access_token = (String)mapAccessToken.get("access_token");String openid = (String) mapAccessToken.get("openid");//把扫描人信息添加数据库里面//判断数据表里面是否存在相同微信信息,根据openid判断UcenterMember ucenterMember = ucenterMemberService.getOpenIdMember(openid);//memeber是空,表没有相同微信数据,进行添加if (ucenterMember == null) {//3 拿着得到accsess_token 和 openid,再去请求微信提供固定的地址,获取到扫描人信息//访问微信的资源服务器,获取用户信息String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +"?access_token=%s" +"&openid=%s";//拼接两个参数String userInfoUrl = String.format(baseUserInfoUrl,access_token,openid);//发送请求String userInfo = HttpClientUtils.get(userInfoUrl);//获取返回userinfo字符串扫描人信息HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);//昵称nicknameString nickname = (String)userInfoMap.get("nickname");//头像headimgurlString headimgurl = (String)userInfoMap.get("headimgurl");ucenterMember = new UcenterMember();ucenterMember.setOpenid(openid);ucenterMember.setNickname(nickname);ucenterMember.setAvatar(headimgurl);ucenterMemberService.save(ucenterMember);}//使用jwt根据member对象生成token字符串String jwtToken = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname());//最后:返回首页面,通过路径传递token字符串return "redirect:http://localhost:3000?token="+jwtToken;} catch (Exception e) {throw new GuliException(20001,"登录失败");}}
}

4. 微信登陆前端部分

4.1 首页布局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() {// 拿到url中的tokenthis.token = this.$route.query.tokenif(this.token) {this.wxLogin()}this.showLoginInfo()},methods: {// 微信登录wxLogin() {if (this.token == '') return//把token存在cookie中、也可以放在localStorage中cookie.set('guli_token', this.token, {domain: 'localhost'})cookie.set('guli_ucenter', '', {domain: 'localhost'})//登录成功根据token获取用户信息userApi.getLoginInfo().then(response => {this.loginInfo = response.data.data.ucenterMember//将用户信息记录cookiecookie.set('guli_ucenter', this.loginInfo, {domain: 'localhost'})})},showLoginInfo(){var jsonStr = cookie.get("guli_ucenter");//  ?????if( jsonStr ) {this.loginInfo = JSON.parse(jsonStr);}},logout() {cookie.set('guli_ucenter', "", {domain: 'localhost'})cookie.set('guli_token', "", {domain: 'localhost'})//跳转页面window.location.href = "/"}}};
</script>

最后总结一下微信登陆的实现流程:

  1. 首先需要根据app_id和redirect_uri以及state(自定义),使用String.format()方法拼接成url,通过return “redirect:” + url;重定向获取微信登陆二维码。
  2. 准备好HttpClientUtils.java工具类,在实现微信登陆需要通过code参数加上AppIDAppSecret等,通过API换取access_token,通过HttpClientUtils工具类向该地址:https://api.weixin.qq.com/sns/oauth2/access_token
  3. 首先需要通过String.format()方法拼接好带有app_id和app_secret以及code的accessTokenUrl
  4. 然后通过HttpClientUtils.get(accessTokenUrl);获取accessTokenInfo(JSON字符串)。
  5. 然后使用Gson将accessTokenInfo转成HashMap(mapAccessToken),在mapAccessToken依次获取access_token和openId;
  6. 然后通过openId去数据库查询用户信息
    1. 如果查出来的UcenterMember为空则执行下面的逻辑:

      1. 依然使用String.format()拼接https://api.weixin.qq.com/sns/userinfo 和access_token以及openid成userInfoUrl。
      2. 还使用HttpClientUtils获取扫描二维码人的信息userInfo(JSON字符串)。
      3. 依然使用Gson转换成HashMap。根据nickname和headimgurl获取扫描人对应的微信名和微信头像。
      4. 然后将openid,nickname,headimgurl,封装到UcenterMember对象中,保存到数据库。此时的UcenterMember不为null。执行下面的逻辑:
    2. 如果查出来的UcenterMember不为空则说明用户已经登陆过一次直接使用JwtUtils根据用户id和nickname获取token。
    3. 然后重定向到首页,并通过路径传递token。
    4. 下面的逻辑请看4.1 首页布局default.vue页面部分结合着之前的就很容易理解。
  7. 到这里微信登陆的功能主干部分大致已经完成了,因为微信登陆时基于OAuth2.0协议的有兴趣的可以去了解了解。最后欢迎一键三连,哈哈哈…

【项目经验】— 登录功能相关推荐

  1. 【博客项目】—登录功能实现( 四)

    [博客项目]-登录功能实现( 四) 创建用户集合,初始化用户 连接数据库 创建用户集合 初始化项目

  2. Session | 基于Session改造oa项目的登录功能

    目录 一:总结域对象 二:基于Session改造oa项目的登录功能 三:oa项目的安全退出系统 一:总结域对象 (1)request(对应的类名:HttpServletRequest) 请求域(请求级 ...

  3. react招聘项目——使用cookie实现项目自动登录功能

    cookie实现自动登录功能 最近在项目中使用了cookie实现自动登录的功能,下面我将使用项目中的例子来对cookie进行简单的分析. 功能需求是:在用户访问网址的时候,使用cookie对用户的操作 ...

  4. day07-vue项目-搭建项目到登录功能

    文章目录 1.电商业务概述 2.项目初始化 3.码云相关操作 B.安装git D.在本地创建公钥:在终端运行:ssh-keygen -t rsa -C "xxx@xxx.com" ...

  5. Vue后台管理系统项目——实现登录功能

    文章目录 登录功能 1. 登录业务流程 2. 登录业务相关技术点 3. 登录-- token 原理分析 4. 登录功能的实现(都记牢点) 登录页面的布局 创建登录组件 登录组件页面布局 登录组件头部布 ...

  6. [Vue项目实战]登录功能实现

    登录功能实现 写在前面 登录概述 登录业务流程 登录业务的相关技术点 登录---token原理分析 登录功能实现 登录页面的布局 在components文件下创建一个vue文件 配置路由(并添加路由重 ...

  7. ‘仿微信发表朋友圈’项目中登录功能的业务逻辑

    登录功能 手机号验证码都通过后端验证后 返回用户数据 登陆成功 成功后 调用store中的setUser方法 store中的setUser方法 将后端返回的用户信息存储到localStorage中 同 ...

  8. 前端学习(1389):多人管理项目9登录功能具体实现

    blog.js const express = require('express'); //创建网站服务器 const app = express(); //开放静态资源文件 const path = ...

  9. JavaWeb网上图书商城完整项目--day02-14.登录功能的login页面处理

    1.现在注册成功之后,我们来到登录页面,登录页面在于 在登录页面.我们也需要向注册页面一样对登录的用户名.密码 验证码等在jsp页面中进行校验,校验我们单独放置一个login.js文件中进行处理,然后 ...

  10. C# ASP.NET程序员整合Java门户单点登录PHPwind论坛博客软件集成项目经验总结

    为什么80%的码农都做不了架构师?>>>    有人曾问,如何正确估算项目周期?你是天天写C#程序的程序员,若做了一个 PHPwind 的单点登录,那估计需要几天时间? 客户只提了一 ...

最新文章

  1. 又有多省明确开学时间!哪个省份的高校全国最早开学?
  2. 知乎热问:国家何时整治程序员的高薪现象?太可怕了!
  3. python 监视图_python获取zabbix监控图
  4. Mysql存储结构B树与B+树与索引
  5. html解析のBeautifulSoup
  6. PAT乙级(1035 插入与归并)
  7. geth+remix+metamask 实现私有链智能合约部署
  8. 美股数据获取 python_python3+tesseract获取美股PEG图像上的数据
  9. react-native 解决“Could not get BatchedBridge...” 的问题
  10. 学生签到系统c代码_手把手教你做一个Java web学生信息、选课、签到考勤、成绩管理系统附带完整源码及视频开发教程...
  11. 手机屏幕什么计算机,手机屏幕和电脑屏幕的区别
  12. 状态压缩.种花小游戏
  13. Shopee虾皮怎么发货?虾皮物流
  14. python学生姓名添加删除_python-函数-实现学生管理系统,完成对学员的增,删,改,查和退出学生管理系统。...
  15. 新一代红米参数模糊的背后 是否有欺诈嫌疑?
  16. GCTA学习3 | GCTA的两篇NG:fast-LMM和fast-GLMM
  17. 成都百知教育称跨境电商将进入最好的时代!
  18. 深度学习模型CPT的环境配置经验
  19. Echarts 生成地图html
  20. NN入门,手把手教你用Numpy手撕NN(一)

热门文章

  1. Thymeleaf 自定义标签
  2. foobar_Foobar挑战:Google的秘密招聘流程
  3. AXIS P3225-LVE Mk II Network Camera - AXIS P3225-LVE Mk II 网络摄像机
  4. 游戏开发48课 性能优化6
  5. 中移动深圳开启4G体验:上网速率是3G的20倍
  6. 交换排序算法之冒泡排序-C语言版(带图详细)
  7. Axure 点图片外区域即隐藏_奔驰V260内饰改装隐藏六座炮筒式仪表盘、后排沙发床、可拆卸小桌板车_搜狐汽车...
  8. WindowsMouseEvent鼠标模拟事件
  9. 运用REST提取ROI信号值与结果显示
  10. 政务外网环境下面springboot项目部署解决方案