邮箱登录
需求
完成这样一个案例
1,登录采取弹出层的形式
2,登录方式:
(1)手机号码+手机验证码
(2)微信扫描
3,无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
4,微信扫描登录成功必须绑定手机号码,即:第一次扫描成功后绑定手机号,以后登录扫描直接登录成功
5,网关统一判断登录状态,如何需要登录,页面弹出登录层
登录完成
思路
1、前端发送登录信息登录邮箱
2、service-msm模块发送邮箱验证码,同时将验证码存放到redis中,方便也会登录的时候用到
3、前端得知验证码已经发送成功、发送登录的信息,包含邮箱和验证码
4、service-user获取登录传来的信息,校验验证码
5、如果验证码通过返回用户名和token,方便判断是否登录
service-user模块
创建service-user的maven的模块
尤其要注意的是要配置网关,和启动类
配置网关
#id
spring.cloud.gateway.routes[2].id=service-user
#uri
spring.cloud.gateway.routes[2].uri=lb://service-user
#servicerId?auth-service?/auth/??
spring.cloud.gateway.routes[2].predicates= Path=/*/user/**
启动类开启mubatis的注解
@EnableDiscoveryClient
@SpringBootApplication
@ComponentScan(basePackages = "com.example") //扫描文件
@EnableFeignClients(basePackages = "com.example")
public class ServiceUserApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceUserApplication.class,args);
}
}
Controller
//用手机号登录接口
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo){
Map<String,Object> info = userInfoService.login(loginVo);
return Result.ok(info);
}
Model
@Data
@ApiModel(description="登录对象")
public class LoginVo {
@ApiModelProperty(value = "openid")
private String openid;
@ApiModelProperty(value = "手机号")
private String phone;
@ApiModelProperty(value = "密码")
private String code;
@ApiModelProperty(value = "IP")
private String ip;
}
Service
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Autowired
public JavaMailSenderImpl javaMailSenderImpl;
@Autowired
public RedisTemplate<String,String> redisTemplate;
//手机登录接口
@Override
public Map<String, Object> login(LoginVo loginVo) {
//从loginVo获取输入的手机号和验证码
String phone = loginVo.getPhone();
String code = loginVo.getCode();
//判断手机号和验证码是否未空
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//判断手机验证码和输入的验证码是否一致
//手机验证码,这里我们用邮箱代替
String s = redisTemplate.opsForValue().get(phone);
System.out.println("======"+s);
if(!code.equals(s)){
throw new YyghException(ResultCodeEnum.CODE_ERROR);
}
System.out.println(phone);
//判断是否是第一次登录
QueryWrapper<UserInfo> qw = new QueryWrapper<>();
qw.eq("phone",phone);
//是第一次登录
UserInfo userInfo = baseMapper.selectOne(qw);
if (userInfo == null) {
//说明是第一次使用这个手机号登录
userInfo = new UserInfo();
userInfo.setName("");
userInfo.setPhone(phone);
userInfo.setStatus(1);
baseMapper.insert(userInfo);
}
//如果该用户被禁用就返回这个异常
if(userInfo.getStatus() == 0){
throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);
}
//不是第一次登录直接登录
Map<String, Object> map = new HashMap<>();
//返回登录信息
//返回登录用户名
//返回token信息
String name = userInfo.getName();
if(StringUtils.isEmpty(name)) {
name = userInfo.getNickName();
}
if(StringUtils.isEmpty(name)) {
name = userInfo.getPhone();
}
map.put("name", name);
//token生成,JWT工具
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token", token);
return map;
}
}
Repository类
@Repository
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
service-msm模块
这是一个发送验证码给邮箱的模块,如果要了解更多springboot发送邮箱关注我之前写过的一篇博客,任务安全。
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
Controlle
@RestController
@RequestMapping("/api/msm")
@Slf4j
public class MsmApiController {
@Autowired
private MsmService msmService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
//发送手机号
@GetMapping("send/{phone}")
public Result sendCode(@PathVariable(value = "phone") String phone){
log.info(phone);
//从redis获取验证码,如果获取到返回ok
/*String code = redisTemplate.opsForValue().get(phone);
if (!StringUtils.isEmpty(code)){
return Result.ok();
}*/
//如果从redis取不到
//生成验证码,通过整合短信服务进行发送
String code = RandomUtil.getFourBitRandom();
msmService.send(phone,code);
redisTemplate.opsForValue().set(phone, code,2, TimeUnit.MINUTES);
return Result.ok();
}
}
Service类
@Service
@Slf4j
public class MsmServiceImpl implements MsmService {
@Autowired
public JavaMailSenderImpl javaMailSenderImpl;
//邮箱发送
@Override
@Async
public void send(String phone, String code) {
log.info(phone);
//判断邮箱是否为空
if (StringUtils.isEmpty(phone)) {
return;
}
//1.创建一个简单的的消息邮件
System.out.println(code);
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("正在登录YYGH系统");
simpleMailMessage.setText("验证码:"+code);
simpleMailMessage.setTo(phone);
simpleMailMessage.setFrom("2590416618@qq.com");
javaMailSenderImpl.send(simpleMailMessage);
}
}
Util类
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;
}
}
值得注意的是我们的启动类需要排除DataSource这一部分,因为我们的配置类里面没有关于mysql的东西
@EnableDiscoveryClient//注册服务
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = "com.example") //swagger扫描文件
@EnableFeignClients(basePackages = "com.example")
@EnableAsync
public class ServiceMsmApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceMsmApplication.class,args);
}
}
测试一下我们的接口
成功运行
前端
登录成功,我们要把用户信息记录在cookie里面
命令行执行:
npm install js-cookie
<template>
<div class="header-container">
<div class="wrapper">
<!-- logo -->
<div class="left-wrapper v-link selected">
<img style="width: 50px" width="50" height="50" src="~assets/images/logo.png">
<span class="text">YYGH 预约挂号统一平台</span>
</div>
<!-- 搜索框 -->
<div class="search-wrapper">
<div class="hospital-search animation-show">
<div id="search" style="display: block;">
<el-autocomplete
class="search-input"
prefix-icon="el-icon-search"
v-model="hosname"
:fetch-suggestions="querySearchAsync"
:trigger-on-focus="false"
@select="handleSelect"
placeholder="点击输入医院名称"
>
<span slot="suffix" class="search-btn v-link highlight clickable selected">搜索 </span>
</el-autocomplete>
</div>
</div>
</div>
<!-- 右侧 -->
<div class="right-wrapper">
<span class="v-link clickable">帮助中心</span>
<span v-if="name == ''" class="v-link clickable" @click="showLogin()" id="loginDialog">登录/注册</span>
<el-dropdown v-if="name != ''" @command="loginMenu">
<span class="el-dropdown-link">
{{ name }}<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu class="user-name-wrapper" slot="dropdown">
<el-dropdown-item command="/user">实名认证</el-dropdown-item>
<el-dropdown-item command="/order">挂号订单</el-dropdown-item>
<el-dropdown-item command="/patient">就诊人管理</el-dropdown-item>
<el-dropdown-item command="/logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- 登录弹出层 -->
<el-dialog :visible.sync="dialogUserFormVisible" style="text-align: left;" top="50px" :append-to-body="true"
width="960px" @close="closeDialog()">
<div class="container">
<!-- 手机登录 #start -->
<div class="operate-view" v-if="dialogAtrr.showLoginType === 'phone'">
<div class="wrapper" style="width: 100%">
<div class="mobile-wrapper" style="position: static;width: 70%">
<span class="title">{{ dialogAtrr.labelTips }}</span>
<el-form>
<el-form-item>
<el-input v-model="dialogAtrr.inputValue" :placeholder="dialogAtrr.placeholder"
:maxlength="dialogAtrr.maxlength" class="input v-input">
<span slot="suffix" class="sendText v-link" v-if="dialogAtrr.second > 0">{{
dialogAtrr.second
}}s </span>
<span slot="suffix" class="sendText v-link highlight clickable selected"
v-if="dialogAtrr.second == 0" @click="getCodeFun()">重新发送 </span>
</el-input>
</el-form-item>
</el-form>
<div class="send-button v-button" @click="btnClick()"> {{ dialogAtrr.loginBtn }}</div>
</div>
<div class="bottom">
<div class="wechat-wrapper" @click="weixinLogin()"><span
class="iconfont icon"></span></div>
<span class="third-text"> 第三方账号登录 </span></div>
</div>
</div>
<!-- 手机登录 #end -->
<!-- 微信登录 #start -->
<div class="operate-view" v-if="dialogAtrr.showLoginType === 'weixin'">
<div class="wrapper wechat" style="height: 400px">
<div>
<div id="weixinLogin"></div>
</div>
<div class="bottom wechat" style="margin-top: -80px;">
<div class="phone-container">
<div class="phone-wrapper" @click="phoneLogin()"><span
class="iconfont icon"></span></div>
<span class="third-text"> 手机短信验证码登录 </span></div>
</div>
</div>
</div>
<!-- 微信登录 #end -->
<div class="info-wrapper">
<div class="code-wrapper">
<div><img src="//img.114yygh.com/static/web/code_login_wechat.png" class="code-img">
<div class="code-text"><span class="iconfont icon"></span>微信扫一扫关注
</div>
<div class="code-text"> “快速预约挂号”</div>
</div>
<div class="wechat-code-wrapper"><img
src="//img.114yygh.com/static/web/code_app.png"
class="code-img">
<div class="code-text"> 扫一扫下载</div>
<div class="code-text"> “预约挂号”APP</div>
</div>
</div>
<div class="slogan">
<div>xxxxxx官方指定平台</div>
<div>快速挂号 安全放心</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import cookie from 'js-cookie'
import Vue from 'vue'
import userInfoApi from '@/api/userInfo'
import smsApi from '@/api/msm'
// import hospitalApi from '@/api/hosp/hospital'
// import weixinApi from '@/api/user/weixin'
const defaultDialogAtrr = {
showLoginType: 'phone', // 控制手机登录与微信登录切换
labelTips: '电子邮箱', // 输入框提示
inputValue: '', // 输入框绑定对象
placeholder: '请输入您的邮箱', // 输入框placeholder
maxlength: 20, // 输入框长度控制
loginBtn: '获取验证码', // 登录按钮或获取验证码按钮文本
sending: true, // 是否可以发送验证码
second: -1, // 倒计时间 second>0 : 显示倒计时 second=0 :重新发送 second=-1 :什么都不显示
clearSmsTime: null // 倒计时定时任务引用 关闭登录层清除定时任务
}
export default {
data() {
return {
userInfo: {
phone: '',
code: '',
openid: ''
},
dialogUserFormVisible: false,
// 弹出层相关属性
dialogAtrr: defaultDialogAtrr,
name: '' // 用户登录显示的名称
}
},
created() {
this.showInfo()
},
mounted() {
// 注册全局登录事件对象
window.loginEvent = new Vue();
// 监听登录事件
loginEvent.$on('loginDialogEvent', function () {
document.getElementById("loginDialog").click();
})
// 触发事件,显示登录层:loginEvent.$emit('loginDialogEvent')
//初始化微信js
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
document.body.appendChild(script)
// 微信登录回调处理
let self = this;
window["loginCallback"] = (name, token, openid) => {
debugger
self.loginCallback(name, token, openid);
}
},
methods: {
schedule(depcode) {
// 登录判断
let token = cookie.get('token')
if (!token) {
loginEvent.$emit('loginDialogEvent')
return
}
window.location.href = '/hospital/schedule?hoscode=' + this.hospital.hoscode + "&depcode="+ depcode
},
loginCallback(name, token, openid) {
// 打开手机登录层,绑定手机号,改逻辑与手机登录一致
if (openid != '') {
this.userInfo.openid = openid
this.showLogin()
} else {
this.setCookies(name, token)
}
},
// 绑定登录或获取验证码按钮
btnClick() {
// 判断是获取验证码还是登录
if (this.dialogAtrr.loginBtn == '获取验证码') {
this.userInfo.phone = this.dialogAtrr.inputValue
// 获取验证码
this.getCodeFun()
} else {
// 登录
this.login()
}
},
// 绑定登录,点击显示登录层
showLogin() {
this.dialogUserFormVisible = true
// 初始化登录层相关参数
this.dialogAtrr = {...defaultDialogAtrr}
},
// 登录
login() {
debugger
this.userInfo.code = this.dialogAtrr.inputValue
if (this.dialogAtrr.loginBtn == '正在提交...') {
this.$message.error('重复提交')
return;
}
if (this.userInfo.code == '') {
this.$message.error('验证码必须输入')
return;
}
if (this.userInfo.code.length != 4) {
this.$message.error('验证码格式不正确')
return;
}
this.dialogAtrr.loginBtn = '正在提交...'
userInfoApi.login(this.userInfo).then(response => {
console.log(response.data)
// 登录成功 设置cookie
this.setCookies(response.data.name, response.data.token)
}).catch(e => {
this.dialogAtrr.loginBtn = '马上登录'
})
},
setCookies(name, token) {
cookie.set('token', token, {domain: 'localhost'})
cookie.set('name', name, {domain: 'localhost'})
window.location.reload()
},
// 获取验证码
getCodeFun() {
if (!(/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(this.userInfo.phone))) {
this.$message.error('邮箱格式不正确')
return;
}
// 初始化验证码相关属性
this.dialogAtrr.inputValue = ''
this.dialogAtrr.placeholder = '请输入验证码'
this.dialogAtrr.maxlength = 4
this.dialogAtrr.loginBtn = '马上登录'
// 控制重复发送
if (!this.dialogAtrr.sending) return;
// 发送短信验证码
this.timeDown();
this.dialogAtrr.sending = false;
smsApi.sendCode(this.userInfo.phone).then(response => {
this.timeDown();
})
},
// 倒计时
timeDown() {
if (this.clearSmsTime) {
clearInterval(this.clearSmsTime);
}
this.dialogAtrr.second = 90;
this.dialogAtrr.labelTips = '验证码已发送至' + this.userInfo.phone
this.clearSmsTime = setInterval(() => {
--this.dialogAtrr.second;
if (this.dialogAtrr.second < 1) {
clearInterval(this.clearSmsTime);
this.dialogAtrr.sending = true;
this.dialogAtrr.second = 0;
}
}, 1000);
},
// 关闭登录层
closeDialog() {
if (this.clearSmsTime) {
clearInterval(this.clearSmsTime);
}
},
showInfo() {
let token = cookie.get('token')
if (token) {
this.name = cookie.get('name')
console.log(this.name)
}
},
loginMenu(command) {
if ('/logout' == command) {
cookie.set('name', '', {domain: 'localhost'})
cookie.set('token', '', {domain: 'localhost'})
//跳转页面
window.location.href = '/'
} else {
window.location.href = command
}
},
/* // 搜索
querySearchAsync(queryString, cb) {
if(queryString == '') return
hospitalApi.getByHosname(queryString).then(response => {
for (let i = 0, len = response.data.length; i < len; i++) {
response.data[i].value = response.data[i].hosname
}
cb(response.data)
})
},*/
handleSelect(item) {
window.location.href = '/hospital/' + item.hoscode
},
/*weixinLogin() {
this.dialogAtrr.showLoginType = 'weixin'
weixinApi.getLoginParam().then(response => {
var obj = new WxLogin({
self_redirect:true,
id: 'weixinLogin', // 需要显示的容器id
appid: response.data.appid, // 公众号appid wx*******
scope: response.data.scope, // 网页默认即可
redirect_uri: response.data.redirectUri, // 授权成功后回调的url
state: response.data.state, // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css文件url,需要https
})
})
},*/
phoneLogin() {
this.dialogAtrr.showLoginType = 'phone'
this.showLogin()
}
}
}
</script>
这样就完成了我们前端的整合
服务网关
我们现在有这个样一个需求点击挂号,如果没有登录就弹出登录页面
类似于这样
com.example.yygh.gateway.Filter.AuthGlobalFilter
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
System.out.println("==="+path);
//内部服务接口,不允许外部访问
if(antPathMatcher.match("/**/inner/**", path)) {
ServerHttpResponse response = exchange.getResponse();
return out(response, ResultCodeEnum.PERMISSION);
}
Long userId = this.getUserId(request);
//api接口,异步请求,校验用户必须登录
if(antPathMatcher.match("/api/**/department/**", path)) {
if(StringUtils.isEmpty(userId)) {
ServerHttpResponse response = exchange.getResponse();
return out(response, ResultCodeEnum.LOGIN_AUTH);
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
/**
* api接口鉴权失败返回数据
* @param response
* @return
*/
private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
Result result = Result.build(null, resultCodeEnum);
byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
/**
* 获取当前登录用户id
* @param request
* @return
*/
private Long getUserId(ServerHttpRequest request) {
String token = "";
List<String> tokenList = request.getHeaders().get("token");
if(null != tokenList) {
token = tokenList.get(0);
}
if(!StringUtils.isEmpty(token)) {
return JwtHelper.getUserId(token);
}
return null;
}
}
之前我们在service-user的login方法中封装了token现在就有用了获取登录状态,有token就是登录,没有就是没登录
这样做之后就可以做到这样的效果