前台的登录和注册
使用腾讯云短信服务
注册服务
- 先开通微信公众号
- 登录腾讯云,注册账号后,开通短信服务
- 创建签名,注意此时选择公众号,并在微信公众号申请开通
- 创建模板
- 创建子AK(也可以使用主AK)

- 新建用户,授权短信权限,保存好secretId和secretKey
- 在线测试短信,可以直接生成代码

可以参考自动生成的代码。
SmsSdkAppId查找方式
项目集成短信
- 创建项目
配置文件
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; } } }
登录业务介绍
单一服务器模式
早期单一服务器,用户认证。

缺点:单点性能压力,无法扩展
SSO(single sign on)模式
分布式,SSO(single sign on)模式

优点 :
用户身份信息独立管理,更好的分布式管理。
可以自己扩展安全策略
缺点:
认证服务器访问压力较大。
Token模式
业务流程图{用户访问业务时,必须登录的流程}

优点:
无状态: 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进行跨域身份验证
传统用户身份验证

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

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

该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分: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 # 微信开放平台 重定向urlcontroller
```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.jsimport 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文件夹中创建注册和登录页面,调用方法
- 创建登录页面
```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"> </em> <span class="vam ml5">登录</span> </a> | <a href="/register" title="注册"> <span class="vam ml5">注册</span> </a> </li> <li v-if="loginInfo.id" id="is-login-one" class="mr10"> <a id="headerMsgCountId" href="#" title="消息"> <em class="icon18 news-icon"> </em> </a> <q class="red-point" style="display: none"> </q> </li> <li v-if="loginInfo.id" id="is-login-two" class="h-r-user"> <a href="/ucenter" title> <img :src="loginInfo.avatar" width="30" height="30" class="vam picImg" alt > <span id="userName" class="vam disIb">{{ loginInfo.nickname }}</span> </a> <a href="javascript:void(0);" title="退出" @click="logout()" class="ml5">退出</a> </li> <!-- /未登录显示第1 li;登录后显示第2,3 li --> </ul>
微信登录
调用微信接口流程图

后端
导入依赖
<!--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 # 微信开放平台 重定向urlcontroller,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", }); }) },
