
① 登录采取弹出层的形式
② 登录方式:
③ 无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
④ 微信扫描登录成功必须绑定手机号码,即 :第一次扫描成功后绑定手机号,以后登录扫描直接登录成功
⑤ 网关统一判断登录状态,如何需要登录,页面弹出登录层
① 搭建 service-user 模块
搭建过程参考service-hosp模块

② 修改配置
A、修改 pom.xml
service com.fancy.yygh 1.0 4.0.0 service-user com.fancy.yygh service-cmn-client 1.0
B、添加配置文件 application.properties
# 服务端口
server.port=8203
# 服务名
spring.application.name=service-user# 环境设置:dev、test、prod
spring.profiles.active=dev# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.38.130:3306/yygh_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848#配置mapper xml文件的路径
# 服务端口
server.port=8203
# 服务名
spring.application.name=service-user# 环境设置:dev、test、prod
spring.profiles.active=dev# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.38.130:3306/yygh_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:mapper/*.xml
C、启动类
@SpringBootApplication
@ComponentScan(basePackages = "com.fancy.yygh")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.fancy.yygh")
public class ServiceUserApplication {public static void main(String[] args) {SpringApplication.run(ServiceUserApplication.class, args);}
}
D、配置网关
#设置路由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/**
① 添加 model
说明:由于实体对象没有逻辑,我们已经统一导入com.fancy.yygh.model.user.UserInfo


② 添加 Mapper
添加 com.fancy.yygh.user.UserInfoMapper
import com.fancy.yygh.model.user.UserInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;public interface UserInfoMapper extends BaseMapper {
}

添加 UserInfoMapper.xml
③ 添加 service 接口及实现类
添加 com.fancy.yygh.user.service.UserInfoService 接口
public interface UserInfoService extends IService {
}
添加 com.fancy.yygh.user.service.impl.UserInfoServiceImpl 接口实现
@Service
public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService {
}

④ 添加 controller
添加 com.fancy.yygh.user.api.UserInfoApiController 类
@RestController
@RequestMapping("/api/user")
public class UserInfoApiController {@Autowiredprivate UserInfoService userInfoService;
}
① 添加 service 接口与实现
A、在 UserInfoService 类添加接口
//会员登录
Map login(LoginVo loginVo);
B、在 UserInfoServiceImpl 类实现接口
@Service
public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService {@Overridepublic Map login(LoginVo loginVo) {String phone = loginVo.getPhone();String code = loginVo.getCode();//校验参数if(StringUtils.isEmpty(phone) ||StringUtils.isEmpty(code)) {throw new YyghException(ResultCodeEnum.PARAM_ERROR);}//TODO 校验校验验证码//手机号已被使用QueryWrapper queryWrapper = new QueryWrapper<>();queryWrapper.eq("phone", phone);//获取会员UserInfo userInfo = baseMapper.selectOne(queryWrapper);if(null == userInfo) {userInfo = new UserInfo();userInfo.setName("");userInfo.setPhone(phone);userInfo.setStatus(1);this.save(userInfo);}//校验是否被禁用if(userInfo.getStatus() == 0) {throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);}//TODO 记录登录//返回页面显示名称Map map = new HashMap<>();String name = userInfo.getName();if(StringUtils.isEmpty(name)) {name = userInfo.getNickName();}if(StringUtils.isEmpty(name)) {name = userInfo.getPhone();}map.put("name", name);map.put("token", "");return map;}
}
说明:
② 添加 controller 接口
A、在 UserInfoApiController 类添加方法
@ApiOperation(value = "会员登录")
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo, HttpServletRequest request) {loginVo.setIp(IpUtil.getIpAddr(request));Map info = userInfoService.login(loginVo);return Result.ok(info);
}
B、添加 IpUtil 工具类
public class IpUtil {private static final String UNKNOWN = "unknown";private static final String LOCALHOST = "127.0.0.1";private static final String SEPARATOR = ",";public static String getIpAddr(HttpServletRequest request) {System.out.println(request);String ipAddress;try {ipAddress = request.getHeader("x-forwarded-for");if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = request.getHeader("WL-Proxy-Client-IP");}if (ipAddress == null || ipAddress.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddress)) {ipAddress = request.getRemoteAddr();if (LOCALHOST.equals(ipAddress)) {InetAddress inet = null;try {inet = InetAddress.getLocalHost();} catch (UnknownHostException e) {e.printStackTrace();}ipAddress = inet.getHostAddress();}}// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割// "***.***.***.***".length()if (ipAddress != null && ipAddress.length() >15) {if (ipAddress.indexOf(SEPARATOR) >0) {ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));}}} catch (Exception e) {ipAddress = "";}return ipAddress;}
}
① JWT介绍
JWT 工具:
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上
JWT最重要的作用就是对 token 信息的防伪作用。
JWT的原理:
一个JWT由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行 Base64 编码得到 JWT。

A、公共部分
主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等。
Key=ATGUIGU
B、私有部分
用户自定义的内容,根据实际需要真正要封装的信息。
userInfo{用户的Id,用户的昵称nickName}
C、签名部分
SaltIP: 当前服务器的IP地址 !{linux 中配置代理服务器的ip}
主要用户对 JWT 生成字符串的时候,进行加密{盐值}
最终组成 key+salt+userInfo token!
Base64 编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把 Base64 编码解成明文,所以不要在 JWT 中放入涉及私密的信息。
② 集成JWT
在 common-util 模块 pom.xml 添加依赖
io.jsonwebtoken jjwt
版本已在 yygh-parent 父模块 pom.xml 添加
在 common-util 模块编写 JwtHelper 类
public class JwtHelper {private static long tokenExpiration = 24*60*60*1000;private static String tokenSignKey = "123456";public static String createToken(Long userId, String userName) {String token = Jwts.builder().setSubject("YYGH-USER").setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).claim("userId", userId).claim("userName", userName).signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();return token;}public static Long getUserId(String token) {if(StringUtils.isEmpty(token)) return null;Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();Integer userId = (Integer)claims.get("userId");return userId.longValue();}public static String getUserName(String token) {if(StringUtils.isEmpty(token)) return "";Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();return (String)claims.get("userName");}public static void main(String[] args) {String token = JwtHelper.createToken(1L, "55");System.out.println(token);System.out.println(JwtHelper.getUserId(token));System.out.println(JwtHelper.getUserName(token));}
}
说明:执行 main 方法测试
③ 完善登录 service 接口
修改 UserInfoServiceImpl 类登录方法
public Map loginUser(LoginVo loginVo) {…………//jwt生成 token 字符串String token = JwtHelper.createToken(userInfo.getId(), name);map.put("token",token);return map;
}
④ 使用 Swagger 测试接口
测试多次,第一次注册,以后直接登录
请求参数:

① 阿里云短信介绍

A、开通阿里云短信服务

B、添加签名管理与模板管理


注:审批通过后方可使用
C、获取用户 AccessKey

② 搭建 service-msm 模块
A、搭建 service-msm 模块
搭建过程参考 service-hosp 模块
B、修改配置
修改 pom.xml
com.aliyun aliyun-java-sdk-core
添加配置文件 application.properties
# 服务端口
server.port=8204
# 服务名
spring.application.name=service-msm#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8spring.redis.host=192.168.44.165
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848aliyun.sms.regionId=default
aliyun.sms.accessKeyId=LT6I0Y5633pX89qC
aliyun.sms.secret=jX8D04Dm12I3gGKj345FYSzu0fq8mT
启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置
@EnableDiscoveryClient
public class ServiceMsmApplication {public static void main(String[] args) {SpringApplication.run(ServiceMsmApplication.class, args);}
}
配置网关
#设置路由id
spring.cloud.gateway.routes[3].id=service-msm
#设置路由的uri
spring.cloud.gateway.routes[3].uri=lb://service-msm
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[3].predicates= Path=/*/msm/**
③ 封装注册短信验证码接口
A、添加配置类
@Component
public class ConstantPropertiesUtils implements InitializingBean {@Value("${aliyun.sms.regionId}")private String regionId;@Value("${aliyun.sms.accessKeyId}")private String accessKeyId;@Value("${aliyun.sms.secret}")private String secret;public static String REGION_Id;public static String ACCESS_KEY_ID;public static String SECRECT;@Overridepublic void afterPropertiesSet() throws Exception {REGION_Id=regionId;ACCESS_KEY_ID=accessKeyId;SECRECT=secret;}
}
B、封装 service 接口和实现类
public interface MsmService {//发送手机验证码boolean send(String phone, String code);
}@Service
public class MsmServiceImpl implements MsmService {@Overridepublic boolean send(String phone, String code) {//判断手机号是否为空if(StringUtils.isEmpty(phone)) {return false;}//整合阿里云短信服务//设置相关参数DefaultProfile profile = DefaultProfile.getProfile(ConstantPropertiesUtils.REGION_Id, ConstantPropertiesUtils.ACCESS_KEY_ID, ConstantPropertiesUtils.SECRECT);IAcsClient client = new DefaultAcsClient(profile);CommonRequest request = new CommonRequest();//request.setProtocol(ProtocolType.HTTPS);request.setMethod(MethodType.POST);request.setDomain("dysmsapi.aliyuncs.com");request.setVersion("2017-05-25");request.setAction("SendSms");//手机号request.putQueryParameter("PhoneNumbers", phone);//签名名称request.putQueryParameter("SignName", "我的谷粒在线教育网站");//模板coderequest.putQueryParameter("TemplateCode", "SMS_180051135");//验证码 使用json格式 {"code":"123456"}Map param = new HashMap();param.put("code",code);request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param));//调用方法进行短信发送try {CommonResponse response = client.getCommonResponse(request);System.out.println(response.getData());return response.getHttpResponse().isSuccess();} catch (ServerException e) {e.printStackTrace();} catch (ClientException e) {e.printStackTrace();}return false;}
}
C、封装 controller 接口
@RestController
@RequestMapping("/api/msm")
public class MsmApiController {@Autowiredprivate MsmService msmService;@Autowiredprivate RedisTemplate redisTemplate;//发送手机验证码@GetMapping("send/{phone}")public Result sendCode(@PathVariable String phone) {//从redis获取验证码,如果获取获取到,返回ok// key 手机号 value 验证码String code = redisTemplate.opsForValue().get(phone);if(!StringUtils.isEmpty(code)) {return Result.ok();}//如果从redis获取不到,// 生成验证码,code = RandomUtil.getSixBitRandom();//调用service方法,通过整合短信服务进行发送boolean isSend = msmService.send(phone,code);//生成验证码放到redis里面,设置有效时间if(isSend) {redisTemplate.opsForValue().set(phone,code,2, TimeUnit.MINUTES);return Result.ok();} else {return Result.fail().message("发送短信失败");}}
}
D、工具类
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
E、完善登录 service 接口
修改 UserInfoServiceImpl 类登录方法,编写验证码校验
//校验校验验证码
String mobleCode = redisTemplate.opsForValue().get(phone);
if(!code.equals(mobleCode)) {throw new YyghException(ResultCodeEnum.CODE_ERROR);
}
① 封装 API 请求
创建 API 文件夹,创建 /api/userInfo.js
import request from '@/utils/request'const api_name = `/api/user`export default {login(userInfo) {return request({url: `${api_name}/login`,method: `post`,data: userInfo})}
}
创建 /api/msm.js
import request from '@/utils/request'const api_name = `/api/msm`export default {sendCode(mobile) {return request({url: `${api_name}/send/${mobile}`,method: `get`})}
}
② 安装 cookie
登录成功,我们要把用户信息记录在 cookie 里面
命令行执行:
npm install js-cookie
③ 添加登录组件
登录层是一个公共层,因此我们把它放在头部组件里面
修改 layouts/myheader.vue 文件
{{ dialogAtrr.labelTips }} 0">{{ dialogAtrr.second }}s 重新发送 {{ dialogAtrr.loginBtn }} 第三方账号登录 手机短信验证码登录
微信扫一扫关注 “快速预约挂号” 扫一扫下载 “预约挂号”APP xxxxxx官方指定平台快速挂号 安全放心
说明:登录成功用户信息记录cookie
④ 头部显示登录状态
A、获取登录信息
created() {this.showInfo()
},
showInfo() {let token = cookie.get('token')if (token) {this.name = cookie.get('name')}
},
B、登录页面控制
帮助中心登录/注册{{name}}实名认证 挂号订单 就诊人管理 退出登录
说明: command 的值为下拉列表的访问路径,即:实名认证的访问路径,后续会有对应的页面,目前我们将它处理好,方便后续使用
添加下拉列表事件处理:
loginMenu(command) {if('/logout' == command) {cookie.set('name', '', {domain: 'localhost'})cookie.set('token', '', {domain: 'localhost'})//跳转页面window.location.href = '/'} else {window.location.href = command}
},
目前登录层在 myheader 组件里面,登录按钮也在同一个组件里面,我们点击登录,调用 showLogin() 方法即可。
目前的问题是,我们在预约挂号页面,选择科室去挂号时我们需要判断当前是否登录,如果登录可以进入下一个页面;如果没有登录需要显示登录层,那么这个问题怎么解决呢,我们不能直接调用头部登录方法,我们目前的组件是包含在nuxt里面的问题总是能够解决的,其实很简单,我们可以注册一个全局登录事件,当需要登录层是,我们发送一个登录事件,头部监听登录事件,然后我们触发登录按钮的点击事件即可打开登录层,下面我们来试试。
① 头部注册和监听登录事件
修改 myheader.vue 组件
A、引入Vue
import Vue from 'vue'
B、注册与监听事件
mounted() {// 注册全局登录事件对象window.loginEvent = new Vue();// 监听登录事件loginEvent.$on('loginDialogEvent', function () {document.getElementById("loginDialog").click();})
// 触发事件,显示登录层:loginEvent.$emit('loginDialogEvent')
}
② 预约挂号页面调整
修改 /pages/hospital/_hoscode.vue 组件
A、引入cookie
import cookie from 'js-cookie'
B、修改方法
schedule(depcode) {// 登录判断let token = cookie.get('token')if (!token) {loginEvent.$emit('loginDialogEvent')return}window.location.href = '/hospital/schedule?hoscode=' + this.hospital.hoscode + "&depcode="+ depcode
},
说明:清除 cookie,点击科室测试
尚医通 预约挂号统一平台搜索 帮助中心登录/注册{{ name }}实名认证 挂号订单 就诊人管理 退出登录 {{ dialogAtrr.labelTips }} 0">{{ dialogAtrr.second }}s 重新发送 {{ dialogAtrr.loginBtn }} 第三方账号登录 手机短信验证码登录
微信扫一扫关注 “快速预约挂号” 扫一扫下载 “预约挂号”APP xxxxxx官方指定平台快速挂号 安全放心
思路:
所有请求都会经过服务网关,服务网关对外暴露服务,在网关进行统一用户认证
既然要在网关进行用户认证,网关得知道对哪些 url 进行认证,所以我们得对ur制定规则
API 接口异步请求的,我们采取 url 规则匹配,如:/api/**/auth/**,如凡是满足该规则的都必须用户认证
① 调整 server-gateway 模块
A、在服务网关添加 fillter
@Override
public Mono 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/**/auth/**", path)) {if(StringUtils.isEmpty(userId)) {ServerHttpResponse response = exchange.getResponse();return out(response, ResultCodeEnum.LOGIN_AUTH);}}return chain.filter(exchange);
}
B、在服务网关中判断用户登录状态
在网关中如何获取用户信息:
我们统一从 header 头信息中获取
如何判断用户信息合法:
登录时我们返回用户 token,在服务网关中获取到 token 后,我在到 redis 中去查看用户 id,如何用户 id 存在,则 token 合法,否则不合法
C、取用户信息
/*** 获取当前登录用户id* @param request* @return*/
private Long getUserId(ServerHttpRequest request) {String token = "";List tokenList = request.getHeaders().get("token");if(null != tokenList) {token = tokenList.get(0);}if(!StringUtils.isEmpty(token)) {return JwtHelper.getUserId(token);}return null;
}
D、网关 filter 完整代码
/*** * 全局Filter,统一处理会员登录与外部不允许访问的服务*
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {private AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono 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/**/auth/**", path)) {if(StringUtils.isEmpty(userId)) {ServerHttpResponse response = exchange.getResponse();return out(response, ResultCodeEnum.LOGIN_AUTH);}}return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}/*** api接口鉴权失败返回数据* @param response* @return*/private Mono 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 tokenList = request.getHeaders().get("token");if(null != tokenList) {token = tokenList.get(0);}if(!StringUtils.isEmpty(token)) {return JwtHelper.getUserId(token);}return null;}
}
② 调整前端 yygh-site
请求服务器端接口时我们默认带上 token,需要登录的接口如果 token 没有或者token 过期,服务器端会返回 208 状态,然后发送登录事件打开登录弹出层登录
修改 utils/request.js 文件
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import cookie from 'js-cookie'// 创建axios实例
const service = axios.create({baseURL: 'http://localhost',timeout: 15000 // 请求超时时间
})
// http request 拦截器
service.interceptors.request.use(config => {// token 先不处理,后续使用时在完善//判断cookie是否有token值if(cookie.get('token')) {//token值放到cookie里面config.headers['token']=cookie.get('token')}return config
},err => {return Promise.reject(err)
})
// http response 拦截器
service.interceptors.response.use(response => {//状态码是208if(response.data.code === 208) {//弹出登录输入框loginEvent.$emit('loginDialogEvent')return} else {if (response.data.code !== 200) {Message({message: response.data.message,type: 'error',duration: 5 * 1000})return Promise.reject(response.data)} else {return response.data}}},error => {return Promise.reject(error.response)
})
export default service