前言
本篇文章完成的需求:
1,登录采取弹出层的形式。
2,登录方式:
- (1)手机号码+手机验证码
- (2)微信扫描(后文完成)
3,无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册。
4,微信扫描登录成功必须绑定手机号码,即:第一次扫描成功后绑定手机号,以后登录扫描直接登录成功。
5,网关统一判断登录状态,如何需要登录,页面弹出登录层。
步骤1:搭建service-user用户模块
1.启动类&配置网关
搭建service-user模块用来做用户登录,其中:
使用@EnableDiscoveryClient注解将服务注册到Nacos。
使用@EnableFeignClients(basePackages = "com.gql")注解开启远程服务调用。
使用@ComponentScan(basePackages = "com.gql")注解开启swagger扫描。
1
2
3
4
5
6
7
8
9
|
@SpringBootApplication @ComponentScan (basePackages = "com.gql" ) @EnableDiscoveryClient @EnableFeignClients (basePackages = "com.gql" ) public class ServiceUserApplication { public static void main(String[] args) { SpringApplication.run(ServiceUserApplication. class , args); } } |
网关配置:由于项目使用Gateway作为网关,现在添加了用户模块,需要在gateway模块的配置文件中加上网关配置:
1
2
3
4
5
6
|
# 设置路由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/** |
2.三层调用
Controller层的login(@RequestBody LoginVo loginVo)方法调用了Service层的loginUser(LoginVo loginVo)方法,进而分别调用redisTemplate和baseMapper操作Redis和MySQL。
Controller层login(@RequestBody LoginVo loginVo)方法:
1
2
3
4
5
6
7
8
|
@Autowired private UserInfoService userInfoService; // 用户手机号登录接口 @PostMapping ( "login" ) public Result login( @RequestBody LoginVo loginVo) { Map<String, Object> info = userInfoService.loginUser(loginVo); return Result.ok(info); } |
Service层loginUser(LoginVo loginVo) 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
@Autowired private RedisTemplate<String, String> redisTemplate; // 用户手机号登录接口 @Override public Map<String, Object> loginUser(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 redisCode = redisTemplate.opsForValue().get(phone); if (!code.equals(redisCode)) { throw new YyghException(ResultCodeEnum.CODE_ERROR); } // 判断是否是第一次登录:根据手机号查询数据库 QueryWrapper<UserInfo> wrapper = new QueryWrapper<>(); wrapper.eq( "phone" , phone); UserInfo userInfo = baseMapper.selectOne(wrapper); // 如果是第一次使用手机登录 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); } // 不是第一次,就直接登录 // 返回登录信息 // 返回登录用户名 // 返回tocken信息 HashMap<String, Object> 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); // 使用JWT生成tocken字符串 String token = JwtHelper.createToken(userInfo.getId(), name); map.put( "tocken" , token); return map; } |
步骤2:整合JWT
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。JWT最重要的作用就是对 token信息的防伪作用。
一个JWT是由三个部分组成:公共部分、私有部分、签名部分。这三者组合进行base64编码得到JWT。由于base64编码并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
整合JWT至common-util模块:版本已在yygh-parent父模块pom.xml添加
1
2
3
4
|
< dependency > < groupId >io.jsonwebtoken</ groupId > < artifactId >jjwt</ artifactId > </ dependency > |
在common-util模块编写JwtHelper类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public class JwtHelper { // 过期时间 private static long tokenExpiration = 24 * 60 * 60 * 1000 ; // 签名密钥 private static String tokenSignKey = "123456" ; // 根据参数生成token 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; } // 根据token字符串得到用户id public static Long getUserId(String token) { if (StringUtils.isEmpty(token)) { return null ; } Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token); Claims claims = claimsJws.getBody(); Integer userId = (Integer) claims.get( "userId" ); return userId.longValue(); } // 根据token字符串得到用户的名称 public static String getUserName(String token) { if (StringUtils.isEmpty(token)) { return "" ; } Jws<Claims> 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, "Hudie" ); // token = 头信息+主体+签名哈希 System.out.println(token); System.out.println(JwtHelper.getUserId(token)); System.out.println(JwtHelper.getUserName(token)); } } |
步骤3: 搭建service-msm短信模块(整合阿里云短信)
1.启动类&配置网关
搭建service-msm模块用来做短信登录,其中:
使用@EnableDiscoveryClient注解将服务注册到Nacos。
使用@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)注解取消数据源自动配置,因为发送短信不需要调用MySQL数据库。
使用@ComponentScan(basePackages = "com.gql")注解开启swagger扫描。
1
2
3
4
5
6
7
8
9
|
@SpringBootApplication (exclude = DataSourceAutoConfiguration. class ) @EnableDiscoveryClient // swagger扫描 @ComponentScan (basePackages = { "com.gql" }) public class ServiceMsmApplication { public static void main(String[] args) { SpringApplication.run(ServiceMsmApplication. class , args); } } |
网关配置:由于项目使用Gateway作为网关,现在添加了短信模块,需要在gateway模块的配置文件中加上网关配置:
1
2
3
4
5
6
|
# 设置路由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/** |
2.短信配置文件&读取配置类
短信配置文件:在短信模块的properties中添加阿里云短信的regionId、accessKeyId、secret:
1
2
3
4
|
# 这里使用杭州结点的阿里云服务器 aliyun.sms.regionId=cn-hangzhou aliyun.sms.accessKeyId=[保密] aliyun.sms.secret=[保密] |
读取配置文件类:在配置类中读取配置文件内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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; @Override public void afterPropertiesSet() throws Exception { REGION_Id = regionId; ACCESS_KEY_ID = accessKeyId; SECRECT = secret; } } |
3.生成验证码类
此类中有生成4位数的验证码方法、6位数的验证码方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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; } } |
4.三层调用
Controller层的sendCode(@PathVariable String phone) 方法直接调用redisTemplate获取生成的验证码,然后调用Service层的send(phone, code)方法通过阿里云发送手机验证码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@RestController @RequestMapping ( "/api/msm" ) public class MsmApiController { @Autowired private MsmService msmService; @Autowired private RedisTemplate<String, String> 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(); } // 如果获取不到,生成6位验证码 code = RandomUtil.getSixBitRandom(); // 偷偷打印到控制台 System.out.println(code); // 调用service返回,通过整合短信服务进行发送 boolean isSend = msmService.send(phone, code); // 将生成的验证码放入redis中,并设置有效时间 if (isSend) { // 验证码超过1分钟失效 redisTemplate.opsForValue().set(phone, code, 1 , TimeUnit.MINUTES); return Result.ok(); } else { return Result.fail().message( "发送短信失败" ); } } } |
Service层发送手机验证码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@Service public class MsmServiceImpl implements MsmService { // 发送手机验证码 @Override public 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(); // 如果是HTTPS方式就需要设置↓ //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" , "袋鼠佳日" ); //模板code request.putQueryParameter( "TemplateCode" , "SMS_215315088" ); //验证码 使用json格式 {"code":"123456"} Map<String, Object> 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 ; } } |
步骤4:登录页面前端
1.封装api请求
创建api文件夹,创建/api/userInfo.js、/api/msm.js
1
2
3
4
5
6
7
8
9
10
11
|
import request from '@/utils/request' const api_name = `/api/user` export default { login(userInfo) { return request({ url: `${api_name}/login`, method: `post`, data: userInfo }) } } |
1
2
3
4
5
6
7
8
9
10
|
import request from '@/utils/request' const api_name = `/api/msm` export default { sendCode(mobile) { return request({ url: `${api_name}/send/${mobile}`, method: `get` }) } } |
2.添加登录组件
登录成功后,我们需要把用户信息记录在cookie里面,所以在vscode的命令行执行:npm install js-cookie。
登录弹窗组件是一个公共层,因此我们把它放在头部组件里面,修改layouts/myheader.vue文件:具体代码点击这里查看仓库。
3.登录全局事件
目前登录弹窗层在myheader组件中,登录按钮也在同一个组件里面,我们点击登录,调用showLogin()方法即可。
在预约挂号页面,选择科室去挂号时我们需要判断当前是否登录,如果登录可以进入下一个页面;如果没有登录需要显示登录层。我们可以注册一个全局登录事件,当需要登录层时,我们发送一个登录事件,头部监听登录事件,然后我们触发登录按钮的点击事件即可打开登录层。
头部注册和监听登录事件,修改myheader.vue组件:
①.引入vue
1
|
import Vue from 'vue' |
②注册与监听事件
1
2
3
4
5
6
7
8
9
10
|
// 页面渲染之后执行 mounted() { // 注册全局登录事件对象 window.loginEvent = new Vue(); // 监听登录事件 loginEvent.$on( "loginDialogEvent" , function () { document.getElementById( "loginDialog" ).click(); }); // 触发事件,显示登录层:loginEvent.$emit('loginDialogEvent') }, |
预约挂号页面调整,修改/pages/hospital/_hoscode.vue组件:
①引入cookie
1
|
import cookie from 'js-cookie' |
②修改方法
1
2
3
4
5
6
7
8
9
10
|
schedule(depcode) { // 登录判断 let token = cookie.get( "token" ); if (!token) { loginEvent.$emit( "loginDialogEvent" ); return ; } window.location.href = "/hospital/schedule?hoscode=" + this .hoscode + "&depcode=" + depcode; }, |
附加:用户认证与网关整合
思路:
所有请求都会经过服务网关,服务网关对外暴露服务,在网关进行统一用户认证;既然要在网关进行用户认证,网关需要知道对哪些url进行认证,所以我们得对ur制定规则。Api接口异步请求的,我们采取url规则匹配,如:/api//auth/,凡是满足该规则的都必须用户认证。
因此,我们需要对server-gateway模块进行调整。
1.在服务网关添加fillter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@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); } //api接口,异步请求,校验用户必须登录 if (antPathMatcher.match( "/api/**/auth/**" , path)) { Long userId = this .getUserId(request); if (StringUtils.isEmpty(userId)) { ServerHttpResponse response = exchange.getResponse(); return out(response, ResultCodeEnum.LOGIN_AUTH); } } return chain.filter(exchange); } |
2.调整前端代码
请求服务器端接口时我们默认带上token,需要登录的接口如果没有token或者token过期,服务器端会返回208状态,然后发送登录事件打开登录弹出层登录。需要修改utils/request.js文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import axios from 'axios' import { MessageBox, Message } from 'element-ui' import cookie from 'js-cookie' // 创建axios实例 const service = axios.create({ baseURL: 'http://localhost:9000' , 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 => { if (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 |
至此,已经将阿里云短信整合到项目中,更多关于分布式医疗挂号系统登录接口整合阿里云短信的资料请关注服务器之家其它相关文章!
原文链接:https://guoqianliang.blog.csdn.net/article/details/116993404