赞
踩
加密算法包括可逆加密算法和不可逆加密算法。
解释:
加密后, 密文可以反向解密得到密码原文。
文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥
解释:
在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。优点:
对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。缺点:
没有非对称加密安全。用途:
一般用于保存用户手机号、身份证等敏感但能解密的信息。
常见的对称加密算法有
:AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
两个密钥:
公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密。
解释:
同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端。加密与解密:
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
签名:
- 私钥签名, 持有公钥进行验证是否被篡改过.
优点:
非对称加密与对称加密相比,其安全性更好;
缺点:
非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
用途:
一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.
常见的非对称加密算法有:
RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)
解释:
一旦加密就不能反向解密得到密码原文。种类:
Hash加密算法, 散列算法, 摘要算法等。用途:
一般用于效验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。常见的不可逆加密算法有:
MD5、SHA、HMAC
。
- Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。Base64编码可用于在HTTP环境下传递较长的标识信息。采用Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
注意:Base64只是一种编码方式,不算加密方法。
- 在线编码工具:http://www.jsons.cn/img2base64/
- JWT头和有效载荷序列化的算法都用到了**
Base64URL
。该算法和常见Base64算法
**类似,稍有差别。- 作为令牌的JWT可以放在URL中(例如**
api.example/?token=xxx
**)。- **
Base64
中用的三个字符是"+","/"和"="
,由于在URL中有特殊含义,因此Base64URL中
对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换
,这就是Base64URL
**算法。
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
- 典型的,一个JWT看起来如下图
- 该对象为一个很长的字符串,字符之间通过
"."
分隔符分为三个子串。- 每一个子串表示了一个功能块,总共有以下三个部分:
JWT头、有效载荷和签名
JWT头部分是一个
描述JWT元数据的JSON对象
。
alg属性
表示签名使用的算法,默认为HMAC SHA256(写为HS256)
;typ属性
表示令牌的类型,JWT令牌统一写为JWT。- 最后,使用
Base64 URL算法
将上述JSON对象转换为字符串保存。
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。
JWT指定七个默认字段供选择。
iss:发行人 exp:到期时间 sub:主题 aud:用户 nbf:在此之前不可用 iat:发布时间 jti:JWT ID用于标识该JWT
- 1
- 2
- 3
- 4
- 5
- 6
- 7
除以上默认字段外,我们还可以自定义私有字段,如下例
{ "sub": "1234567890", "name": "Helen", "admin": true }
- 1
- 2
- 3
- 4
- 5
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON对象也使用**
Base64 URL算法
**转换为字符串保存。{ "sub": "1234567890", "name": "Helen", "admin": true }
- 1
- 2
- 3
- 4
- 5
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。
然后,使用标头中指定的签名算法(默认情况下为
HMAC SHA256
)根据以下公式生成签名。在计算出签名哈希后,
JWT头,有效载荷和签名哈希的三个部分组合成一个字符串
,每个部分用"."分隔
,就构成整个JWT对象。HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
- 1
- JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
- 之后,当用户与服务器通信时,客户在请求中发回JSON对象。服务器仅依赖于这个JSON对象来标识用户。为了防止用户篡改数据,服务器将在生成对象时添加签名。
服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
- 客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
- 此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,
因此一般是将它放入HTTP请求的Header Authorization字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。
- JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库。
- 存储在客户端,不占用服务端的内存资源
JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。
当JWT未加密时,一些私密数据无法通过JWT传输。JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
- JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
传统用户身份验证
Internet服务无法与用户身份验证分开。一般过程如下:
- 用户向服务器发送用户名和密码。
- 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
- 服务器向用户返回session_id,session信息都会写入到用户的Cookie。
- 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
- 服务器收到session_id并对比之前保存的数据,确认用户的身份。
这种模式最大的问题是,没有分布式架构,无法支持横向扩展。
SSO
(single sign on)模式
- 分布式,
SSO(single sign on)
模式- 优点 :
- 用户身份信息独立管理,更好的分布式管理。
- 可以自己扩展安全策略。
- 缺点:
- 认证服务器访问压力较大。
- 业务流程图{用户访问业务时,必须登录的流程}
- 优点:
- 无状态: token无状态,session有状态的。
- 基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT)
- 缺点:
- 占用带宽。
- 无法在服务器端销毁。
注:基于微服务开发,选择token的形式相对较多,因此我使用token作为用户认证的标准。并进行详细地总结
在APP或者网页接入一些第三方应用时,经常会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录。
这里仅仅做一些CAS原理上的介绍,因为项目中使用的是JWT实现单点登录。
CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:
- 开源的企业级单点登录解决方案。
CAS Server 为需要独立部署的 Web 应用。
其实CAS服务端其实就是一个war包。CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用)
,包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。
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单点登录访问流程主要有以下步骤:
访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
定向认证:SSO客户端会重定向用户请求到SSO服务器。
用户认证:用户身份认证。
发放票据:SSO服务器会产生一个
随机的Service Ticket。
验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。
CAS 系统中设计了 5 中票据: TGC 、 ST 、 PGT 、 PGTIOU 、 PT 。
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 变得更加安全(事实上都是可以配置的):
ST 只能使用一次,
CAS 协议规定,无论 Service Ticket 验证是否成功, CAS Server 都会清除服务端缓存中的该Ticket ,从而可以确保一个 Service Ticket 不被使用两次。ST 在一段时间内失效
,CAS 规定 ST 只能存活一定的时间,然后 CAS Server 会让它失效。默认有效时间为 5 分钟。ST 是基于随机数生成的,
ST 必须足够随机,如果 ST 生成规则被猜出, Hacker 就等于绕过 CAS 认证,直接访问 对应的服务。
本项目:
持久层框架使用的是MyBatisPlus。
前端使用Nuxt框架搭建。
<dependencies>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
public static final long EXPIR
,token过期时间public static final String APP_SECRET
,自定义签名public static String getJwtToken(String id, String nickname)
,根据id和nickname获取tokenpublic static boolean checkToken(String jwtToken)
,判断token是否存在与有效public static boolean checkToken(HttpServletRequest request)
,判断token是否存在与有效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"); } }
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); } } }
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;
}
UcenterMemberController.java
此处的
R
是定义的统一返回结果对象,类似于ResponseEntity,详细的定义过程可以参考下面这篇文章:https://blog.csdn.net/weixin_45267102/article/details/108449377
@RestController @RequestMapping("/user/user-member") @CrossOrigin// 解决跨域问题 public class UcenterMemberController { @Autowired private UcenterMemberService ucenterMemberService; // 此处的R是定义的统一返回结果对象,类似于ResponseEntity @PostMapping("/loginUser") public R loginUser(@RequestBody LoginVo loginVo) { String token = ucenterMemberService.loginUser(loginVo); // 拿到处理后的token,返回到前端 return R.ok().data("token",token); } }
public interface UcenterMemberService extends IService<UcenterMember> {
/**
* 登陆的方法
*/
String loginUser(LoginVo loginVo);
}
这里使用到了统一异常处理,为什么使用以及具体使用方法见下面这篇文章:https://blog.csdn.net/weixin_45267102/article/details/108449918
@Service public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService { @Override public 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生成token String token = JwtUtils.getJwtToken(ucenterMember.getId(), ucenterMember.getNickname()); return token; } }
到此为止后端的逻辑已经结束了,现在我们梳理一下后端的流程:
前端通过手机号和密码向后端发出登录请求,去数据校验用户手机号以及密码是否正确。
如果手机号和密码都正确就返回经过加密后的token到前端,token中包含此用户的id和nickName。
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,去查询用户信息返回到前端。然而我们是要实现的是单点登陆功能,这又是怎么实现的呢?看下面的分析。
@RestController @RequestMapping("/user/user-member") @CrossOrigin public class UcenterMemberController { @Autowired private UcenterMemberService ucenterMemberService; // ... @GetMapping("/getLoginInfo") public R getLoginInfo(HttpServletRequest request) { // 调用jwt工具类的方法。根据request对象获取头信息,返回用户id String memberId = JwtUtils.getMemberIdByJwtToken(request); // 查询数据库根据用户id获取用户信息 UcenterMember ucenterMember = ucenterMemberService.getById(memberId); return R.ok().data("ucenterMember",ucenterMember); } }
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' }) }, }
因为这个是整个网站的页面的布局页,再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>
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
- 网站应用微信登录是基于
OAuth2.0协议
标准构建的微信OAuth2.0
授权登录系统。 在进行微信OAuth2.0
授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID
和AppSecret
,申请微信登录且通过审核后,可开始接入流程。- https://open.weixin.qq.com
- 准备工作:
- 注册
- 邮箱激活
- 完善开发者资料
- 开发者资质认证
- 准备营业执照,1-2个工作日审批、300元
- 创建网站应用
- 提交审核,7个工作日审批
- 熟悉微信登录流程
获取access_token时序图
- 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
- 通过code参数加上AppID和AppSecret等,通过API换取access_token;
- 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
根据appid,redirect_url,state获取二维码。
此处使用的是尚硅谷在线教育项目—谷粒学院提供的app_id和app_secret。
application.yml
wx:
open:
# 微信开放平台 appid
app_id: wxed9954c01bb89b47
# 微信开放平台 appsecret
app_secret: a7482517235173ddb4083788de60b90e
# 微信开放平台 重定向url
redirect_url: http://guli.shop/api/ucenter/wx/callback
创建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; @Override public void afterPropertiesSet() throws Exception { WX_OPEN_APP_ID = appId; WX_OPEN_APP_SECRET = appSecret; WX_OPEN_REDIRECT_URL = redirectUrl; } }
- WxLoginController
- 注意路径
/api/ucenter/wx/login
,然后重定向到https://open.weixin.qq.com/connect/qrconnect…
- 需要参数有:
- appid 通过工具类获得。
- redirect_uri 通过工具类获得后,对redirect_url进行URLEncoder编码
- state
- 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 { @Autowired private 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; } }
参数 | 是否必须 | 说明 |
---|---|---|
appid | 是 | 应用唯一标识 |
redirect_uri | 是 | 请使用urlEncode对链接进行处理 |
response_type | 是 | 填code |
scope | 是 | 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login即 |
state | 否 | 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验 |
- 访问:http://localhost:8006/api/ucenter/wx/login
- 访问授权url后会得到一个微信登录二维码
我们刚刚完成微信登录的第一步即:第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
我们下面完成:
- 通过code参数加上
AppID
和AppSecret
等,通过API换取access_token;- 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
注意:
- 由于我们做微信登录时很多人都在用同一个微信
appid
和appsecret
所以我们需要把当前微服务端口更换成8150,具体原因看下图,同时更改**nginx
**的跳转规则。- 同时我们需要使用到HttpClient的工具类。
<!--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>
HttpClientUtils.java放入utils包
- 了解内容
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=GBK if (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() { @Override public boolean verify(String arg0, SSLSession arg1) { return true; } @Override public void verify(String host, SSLSocket ssl) throws IOException { } @Override public void verify(String host, X509Certificate cert) throws SSLException { } @Override public 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 block e.printStackTrace(); } catch (SocketTimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
通过code参数加上
AppID
和AppSecret
等,通过API换取access_token,通过httpClient
工具类向该地址:https://api.weixin.qq.com/sns/oauth2/access_token…得到
accessTokenInfo
,使用gson
把accessTokenInfo转换成hashMapgson.fromJson(accessTokenInfo, HashMap.class)
从
hashMap
中获取openid
和access_token
因为每一个微信账号的
openid
都不一样,所以我们先通过openid
去数据库查询是否存在该UcenterMember
对象,如果UcenterMember
为空,我们则执行第三步。如果UCenterMember
不为空,则根据其中的id和nickname直接生产token重定向到首页。
接口略。
@Override public UcenterMember getOpenIdMember(String openid) { QueryWrapper<UcenterMember> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("openid", openid); UcenterMember ucenterMember = baseMapper.selectOne(queryWrapper); return ucenterMember; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
第三步:拿着得到
accsess_token
和openid
,再去请求微信提供固定的地址,获取到扫描人信息userInfo
,同时使用gson
将其转换成hashMap
从
hashMap
中获取用户的信息,加入到数据库。最后使用
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 { @Autowired private 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 和 openid String 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); //昵称nickname String nickname = (String)userInfoMap.get("nickname"); //头像headimgurl String 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,"登录失败"); } } }
<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中的token this.token = this.$route.query.token if(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 //将用户信息记录cookie cookie.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>
最后总结一下微信登陆的实现流程:
- 首先需要
根据app_id和redirect_uri以及state(自定义)
,使用String.format()方法拼接成url,通过return “redirect:” + url;
重定向获取微信登陆二维码。- 准备好HttpClientUtils.java工具类,在实现微信登陆需要通过code参数加上
AppID
和AppSecret
等,通过API换取access_token,通过HttpClientUtils
工具类向该地址:https://api.weixin.qq.com/sns/oauth2/access_token- 首先需要通过String.format()方法拼接好带有app_id和app_secret以及code的
accessTokenUrl
。- 然后通过HttpClientUtils.get(accessTokenUrl);获取accessTokenInfo(JSON字符串)。
- 然后使用Gson将accessTokenInfo转成HashMap(mapAccessToken),在mapAccessToken依次获取access_token和openId;
- 然后通过openId去数据库查询用户信息
- 如果查出来的UcenterMember为空则执行下面的逻辑:
- 依然使用String.format()拼接https://api.weixin.qq.com/sns/userinfo 和access_token以及openid成userInfoUrl。
- 还使用HttpClientUtils获取扫描二维码人的信息userInfo(JSON字符串)。
- 依然使用Gson转换成HashMap。根据nickname和headimgurl获取扫描人对应的微信名和微信头像。
- 然后将openid,nickname,headimgurl,封装到UcenterMember对象中,保存到数据库。此时的UcenterMember不为null。执行下面的逻辑:
- 如果查出来的UcenterMember不为空则说明用户已经登陆过一次直接使用JwtUtils根据用户id和nickname获取token。
- 然后重定向到首页,并通过路径传递token。
- 下面的逻辑请看
4.1 首页布局default.vue页面
部分结合着之前的就很容易理解。- 到这里微信登陆的功能主干部分大致已经完成了,因为微信登陆时基于OAuth2.0协议的有兴趣的可以去了解了解。最后欢迎一键三连,哈哈哈…
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。