實(shí)戰(zhàn)分布式醫(yī)療掛號(hào)系統(tǒng)登錄接口整合阿里云短信詳情
前言
本篇文章完成的需求:
1,登錄采取彈出層的形式。
2,登錄方式:
- (1)手機(jī)號(hào)碼+手機(jī)驗(yàn)證碼
- (2)微信掃描(后文完成)
3,無(wú)注冊(cè)界面,第一次登錄根據(jù)手機(jī)號(hào)判斷系統(tǒng)是否存在,如果不存在則自動(dòng)注冊(cè)。
4,微信掃描登錄成功必須綁定手機(jī)號(hào)碼,即:第一次掃描成功后綁定手機(jī)號(hào),以后登錄掃描直接登錄成功。
5,網(wǎng)關(guān)統(tǒng)一判斷登錄狀態(tài),如何需要登錄,頁(yè)面彈出登錄層。
步驟1:搭建service-user用戶模塊
1.啟動(dòng)類(lèi)&配置網(wǎng)關(guān)
搭建service-user模塊用來(lái)做用戶登錄,其中:
使用@EnableDiscoveryClient注解將服務(wù)注冊(cè)到Nacos。
使用@EnableFeignClients(basePackages = "com.gql")注解開(kāi)啟遠(yuǎn)程服務(wù)調(diào)用。
使用@ComponentScan(basePackages = "com.gql")注解開(kāi)啟swagger掃描。
@SpringBootApplication @ComponentScan(basePackages = "com.gql") @EnableDiscoveryClient @EnableFeignClients(basePackages = "com.gql") public class ServiceUserApplication { public static void main(String[] args) { SpringApplication.run(ServiceUserApplication.class, args); } }
網(wǎng)關(guān)配置:由于項(xiàng)目使用Gateway作為網(wǎng)關(guān),現(xiàn)在添加了用戶模塊,需要在gateway模塊的配置文件中加上網(wǎng)關(guān)配置:
# 設(shè)置路由id spring.cloud.gateway.routes[2].id=service-user #設(shè)置路由的uri spring.cloud.gateway.routes[2].uri=lb://service-user #設(shè)置路由斷言,代理servicerId為auth-service的/auth/路徑 spring.cloud.gateway.routes[2].predicates= Path=/*/user/**
2.三層調(diào)用
Controller層的login(@RequestBody LoginVo loginVo)方法調(diào)用了Service層的loginUser(LoginVo loginVo)方法,進(jìn)而分別調(diào)用redisTemplate和baseMapper操作Redis和MySQL。
Controller層login(@RequestBody LoginVo loginVo)方法:
@Autowired private UserInfoService userInfoService; // 用戶手機(jī)號(hào)登錄接口 @PostMapping("login") public Result login(@RequestBody LoginVo loginVo) { Map<String, Object> info = userInfoService.loginUser(loginVo); return Result.ok(info); }
Service層loginUser(LoginVo loginVo) 方法:
@Autowired private RedisTemplate<String, String> redisTemplate; // 用戶手機(jī)號(hào)登錄接口 @Override public Map<String, Object> loginUser(LoginVo loginVo) { // 從loginVo獲取輸入的手機(jī)號(hào)和驗(yàn)證碼 String phone = loginVo.getPhone(); String code = loginVo.getCode(); // 判斷手機(jī)號(hào)和驗(yàn)證碼是否為空 if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) { throw new YyghException(ResultCodeEnum.PARAM_ERROR); } // 校驗(yàn)驗(yàn)證碼 String redisCode = redisTemplate.opsForValue().get(phone); if (!code.equals(redisCode)) { throw new YyghException(ResultCodeEnum.CODE_ERROR); } // 判斷是否是第一次登錄:根據(jù)手機(jī)號(hào)查詢數(shù)據(jù)庫(kù) QueryWrapper<UserInfo> wrapper = new QueryWrapper<>(); wrapper.eq("phone", phone); UserInfo userInfo = baseMapper.selectOne(wrapper); // 如果是第一次使用手機(jī)登錄 if (userInfo == null) { // 添加信息到數(shù)據(jù)庫(kù) userInfo = new UserInfo(); userInfo.setName(""); userInfo.setPhone(phone); userInfo.setStatus(1); baseMapper.insert(userInfo); } // 校驗(yàn)是否被禁用 if (userInfo.getStatus() == 0) { throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR); } // 不是第一次,就直接登錄 // 返回登錄信息 // 返回登錄用戶名 // 返回tocken信息 HashMap<String, Object> map = new HashMap<>(); String name = userInfo.getName(); // 如果用戶名稱(chēng)為空,就去得到昵稱(chēng) if (StringUtils.isEmpty(name)) { name = userInfo.getNickName(); } // 如果昵稱(chēng)還為空,就去得到手機(jī)號(hào) 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)是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開(kāi)放標(biāo)準(zhǔn)。JWT的聲明一般被用來(lái)在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源。比如用在用戶登錄上。JWT最重要的作用就是對(duì) token信息的防偽作用。
一個(gè)JWT是由三個(gè)部分組成:公共部分、私有部分、簽名部分。這三者組合進(jìn)行base64編碼得到JWT。由于base64編碼并不是加密,只是把明文信息變成了不可見(jiàn)的字符串。但是其實(shí)只要用一些工具就可以把base64編碼解成明文,所以不要在JWT中放入涉及私密的信息。
整合JWT至common-util模塊:版本已在yygh-parent父模塊pom.xml添加
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
在common-util模塊編寫(xiě)JwtHelper類(lèi):
public class JwtHelper { // 過(guò)期時(shí)間 private static long tokenExpiration = 24 * 60 * 60 * 1000; // 簽名密鑰 private static String tokenSignKey = "123456"; // 根據(jù)參數(shù)生成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; } // 根據(jù)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(); } // 根據(jù)token字符串得到用戶的名稱(chēng) 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"); } // 測(cè)試 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.啟動(dòng)類(lèi)&配置網(wǎng)關(guān)
搭建service-msm模塊用來(lái)做短信登錄,其中:
使用@EnableDiscoveryClient注解將服務(wù)注冊(cè)到Nacos。
使用@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)注解取消數(shù)據(jù)源自動(dòng)配置,因?yàn)榘l(fā)送短信不需要調(diào)用MySQL數(shù)據(jù)庫(kù)。
使用@ComponentScan(basePackages = "com.gql")注解開(kāi)啟swagger掃描。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient // swagger掃描 @ComponentScan(basePackages = {"com.gql"}) public class ServiceMsmApplication { public static void main(String[] args) { SpringApplication.run(ServiceMsmApplication.class, args); } }
網(wǎng)關(guān)配置:由于項(xiàng)目使用Gateway作為網(wǎng)關(guān),現(xiàn)在添加了短信模塊,需要在gateway模塊的配置文件中加上網(wǎng)關(guān)配置:
# 設(shè)置路由id spring.cloud.gateway.routes[3].id=service-msm #設(shè)置路由的uri spring.cloud.gateway.routes[3].uri=lb://service-msm #設(shè)置路由斷言,代理servicerId為auth-service的/auth/路徑 spring.cloud.gateway.routes[3].predicates= Path=/*/msm/**
2.短信配置文件&讀取配置類(lèi)
短信配置文件:在短信模塊的properties中添加阿里云短信的regionId、accessKeyId、secret:
# 這里使用杭州結(jié)點(diǎn)的阿里云服務(wù)器 aliyun.sms.regionId=cn-hangzhou aliyun.sms.accessKeyId=[保密] aliyun.sms.secret=[保密]
讀取配置文件類(lèi):在配置類(lèi)中讀取配置文件內(nèi)容:
@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.生成驗(yàn)證碼類(lèi)
此類(lèi)中有生成4位數(shù)的驗(yàn)證碼方法、6位數(shù)的驗(yàn)證碼方法。
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)); } /** * 給定數(shù)組,抽取n個(gè)數(shù)據(jù) * @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>(); // 生成隨機(jī)數(shù)字并存入HashMap for (int i = 0; i < list.size(); i++) { int number = random.nextInt(100) + 1; hashMap.put(number, i); } // 從HashMap導(dǎo)入數(shù)組 Object[] robjs = hashMap.values().toArray(); ArrayList r = new ArrayList(); // 遍歷數(shù)組并打印數(shù)據(jù) 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.三層調(diào)用
Controller層的sendCode(@PathVariable String phone) 方法直接調(diào)用redisTemplate獲取生成的驗(yàn)證碼,然后調(diào)用Service層的send(phone, code)方法通過(guò)阿里云發(fā)送手機(jī)驗(yàn)證碼。
@RestController @RequestMapping("/api/msm") public class MsmApiController { @Autowired private MsmService msmService; @Autowired private RedisTemplate<String, String> redisTemplate; // 發(fā)送手機(jī)驗(yàn)證碼 @GetMapping("send/{phone}") public Result sendCode(@PathVariable String phone) { // 從redis獲取手機(jī)驗(yàn)證碼,如果獲取到就返回ok // (key:手機(jī)號(hào),value:驗(yàn)證碼) String code = redisTemplate.opsForValue().get(phone); if (!StringUtils.isEmpty(code)) { return Result.ok(); } // 如果獲取不到,生成6位驗(yàn)證碼 code = RandomUtil.getSixBitRandom(); // 偷偷打印到控制臺(tái) System.out.println(code); // 調(diào)用service返回,通過(guò)整合短信服務(wù)進(jìn)行發(fā)送 boolean isSend = msmService.send(phone, code); // 將生成的驗(yàn)證碼放入redis中,并設(shè)置有效時(shí)間 if (isSend) { // 驗(yàn)證碼超過(guò)1分鐘失效 redisTemplate.opsForValue().set(phone, code, 1, TimeUnit.MINUTES); return Result.ok(); } else { return Result.fail().message("發(fā)送短信失敗"); } } }
Service層發(fā)送手機(jī)驗(yàn)證碼:
@Service public class MsmServiceImpl implements MsmService { // 發(fā)送手機(jī)驗(yàn)證碼 @Override public boolean send(String phone, String code) { // 判斷手機(jī)號(hào)是否為空 if (StringUtils.isEmpty(phone)) { return false; } // 整合阿里云短信服務(wù) // 設(shè)置相關(guān)參數(shù) DefaultProfile profile = DefaultProfile. getProfile(ConstantPropertiesUtils.REGION_Id, ConstantPropertiesUtils.ACCESS_KEY_ID, ConstantPropertiesUtils.SECRECT); IAcsClient client = new DefaultAcsClient(profile); CommonRequest request = new CommonRequest(); // 如果是HTTPS方式就需要設(shè)置↓ //request.setProtocol(ProtocolType.HTTPS); request.setMethod(MethodType.POST); request.setDomain("dysmsapi.aliyuncs.com"); request.setVersion("2017-05-25"); request.setAction("SendSms"); //手機(jī)號(hào) request.putQueryParameter("PhoneNumbers", phone); //簽名名稱(chēng) request.putQueryParameter("SignName", "袋鼠佳日"); //模板code request.putQueryParameter("TemplateCode", "SMS_215315088"); //驗(yàn)證碼 使用json格式 {"code":"123456"} Map<String, Object> param = new HashMap(); param.put("code", code); request.putQueryParameter("TemplateParam", JSONObject.toJSONString(param)); //調(diào)用方法進(jìn)行短信發(fā)送 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:登錄頁(yè)面前端
1.封裝api請(qǐng)求
創(chuàng)建api文件夾,創(chuàng)建/api/userInfo.js、/api/msm.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 }) } }
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的命令行執(zhí)行:npm install js-cookie。
登錄彈窗組件是一個(gè)公共層,因此我們把它放在頭部組件里面,修改layouts/myheader.vue文件:具體代碼點(diǎn)擊這里查看倉(cāng)庫(kù)。
3.登錄全局事件
目前登錄彈窗層在myheader組件中,登錄按鈕也在同一個(gè)組件里面,我們點(diǎn)擊登錄,調(diào)用showLogin()方法即可。
在預(yù)約掛號(hào)頁(yè)面,選擇科室去掛號(hào)時(shí)我們需要判斷當(dāng)前是否登錄,如果登錄可以進(jìn)入下一個(gè)頁(yè)面;如果沒(méi)有登錄需要顯示登錄層。我們可以注冊(cè)一個(gè)全局登錄事件,當(dāng)需要登錄層時(shí),我們發(fā)送一個(gè)登錄事件,頭部監(jiān)聽(tīng)登錄事件,然后我們觸發(fā)登錄按鈕的點(diǎn)擊事件即可打開(kāi)登錄層。
頭部注冊(cè)和監(jiān)聽(tīng)登錄事件,修改myheader.vue組件:
①.引入vue
import Vue from 'vue'
②注冊(cè)與監(jiān)聽(tīng)事件
// 頁(yè)面渲染之后執(zhí)行 mounted() { // 注冊(cè)全局登錄事件對(duì)象 window.loginEvent = new Vue(); // 監(jiān)聽(tīng)登錄事件 loginEvent.$on("loginDialogEvent", function () { document.getElementById("loginDialog").click(); }); // 觸發(fā)事件,顯示登錄層:loginEvent.$emit('loginDialogEvent') },
預(yù)約掛號(hào)頁(yè)面調(diào)整,修改/pages/hospital/_hoscode.vue組件:
①引入cookie
import cookie from 'js-cookie'
②修改方法
schedule(depcode) { // 登錄判斷 let token = cookie.get("token"); if (!token) { loginEvent.$emit("loginDialogEvent"); return; } window.location.href = "/hospital/schedule?hoscode=" + this.hoscode + "&depcode=" + depcode; },
附加:用戶認(rèn)證與網(wǎng)關(guān)整合
思路:
所有請(qǐng)求都會(huì)經(jīng)過(guò)服務(wù)網(wǎng)關(guān),服務(wù)網(wǎng)關(guān)對(duì)外暴露服務(wù),在網(wǎng)關(guān)進(jìn)行統(tǒng)一用戶認(rèn)證;既然要在網(wǎng)關(guān)進(jìn)行用戶認(rèn)證,網(wǎng)關(guān)需要知道對(duì)哪些url進(jìn)行認(rèn)證,所以我們得對(duì)ur制定規(guī)則。Api接口異步請(qǐng)求的,我們采取url規(guī)則匹配,如:/api//auth/,凡是滿足該規(guī)則的都必須用戶認(rèn)證。
因此,我們需要對(duì)server-gateway模塊進(jìn)行調(diào)整。
1.在服務(wù)網(wǎng)關(guān)添加fillter
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath(); System.out.println("===" + path); //內(nèi)部服務(wù)接口,不允許外部訪問(wèn) if (antPathMatcher.match("/**/inner/**", path)) { ServerHttpResponse response = exchange.getResponse(); return out(response, ResultCodeEnum.PERMISSION); } //api接口,異步請(qǐng)求,校驗(yàn)用戶必須登錄 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); }
網(wǎng)站網(wǎng)關(guān)filter代碼詳見(jiàn)倉(cāng)庫(kù)。
2.調(diào)整前端代碼
請(qǐng)求服務(wù)器端接口時(shí)我們默認(rèn)帶上token,需要登錄的接口如果沒(méi)有token或者token過(guò)期,服務(wù)器端會(huì)返回208狀態(tài),然后發(fā)送登錄事件打開(kāi)登錄彈出層登錄。需要修改utils/request.js文件:
import axios from 'axios' import { MessageBox, Message } from 'element-ui' import cookie from 'js-cookie' // 創(chuàng)建axios實(shí)例 const service = axios.create({ baseURL: 'http://localhost:9000', timeout: 15000 // 請(qǐng)求超時(shí)時(shí)間 }) // http request (請(qǐng)求)攔截器 service.interceptors.request.use( config => { // token 先不處理,后續(xù)使用時(shí)在完善 // 判斷cookie中是否有token值 if (cookie.get('token')) { // 將token值放到cookie里面 config.headers['token'] = cookie.get('token') } return config }, err => { return Promise.reject(err) }) // http response (響應(yīng))攔截器 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
至此,已經(jīng)將阿里云短信整合到項(xiàng)目中,更多關(guān)于分布式醫(yī)療掛號(hào)系統(tǒng)登錄接口整合阿里云短信的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 分布式醫(yī)療掛號(hào)系統(tǒng)整合Gateway網(wǎng)關(guān)解決跨域問(wèn)題
- 分布式醫(yī)療掛號(hào)系統(tǒng)Nacos微服務(wù)Feign遠(yuǎn)程調(diào)用數(shù)據(jù)字典
- 實(shí)戰(zhàn)分布式醫(yī)療掛號(hào)系統(tǒng)開(kāi)發(fā)醫(yī)院科室及排班的接口
- 分布式醫(yī)療掛號(hào)系統(tǒng)SpringCache與Redis為數(shù)據(jù)字典添加緩存
- 分布式醫(yī)療掛號(hào)系統(tǒng)EasyExcel導(dǎo)入導(dǎo)出數(shù)據(jù)字典的使用
- 分布式開(kāi)發(fā)醫(yī)療掛號(hào)系統(tǒng)數(shù)據(jù)字典模塊前后端實(shí)現(xiàn)
相關(guān)文章
JAVA發(fā)送http get/post請(qǐng)求,調(diào)用http接口、方法詳解
這篇文章主要介紹了Java發(fā)送http get/post請(qǐng)求調(diào)用接口/方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04springboot大文件上傳、分片上傳、斷點(diǎn)續(xù)傳、秒傳的實(shí)現(xiàn)
本文主要介紹了springboot大文件上傳、分片上傳、斷點(diǎn)續(xù)傳、秒傳的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Java8 用Lambda表達(dá)式給List集合排序的實(shí)現(xiàn)
這篇文章主要介紹了Java8 用Lambda表達(dá)式給List集合排序的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Java實(shí)現(xiàn)XML格式與JSON格式互相轉(zhuǎn)換的方法
這篇文章主要介紹了Java實(shí)現(xiàn)XML格式與JSON格式互相轉(zhuǎn)換的方法,方法通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),選擇使用哪種格式通常取決于項(xiàng)目的需求和上下文,所以格式轉(zhuǎn)換就成了我們必備的技能,具體實(shí)現(xiàn)代碼跟隨小編一起看看吧2023-10-10Java?properties?和?yml?的區(qū)別解析
properties和yml都是Spring?Boot支持的兩種配置文件,它們可以看做Spring?Boot在不同時(shí)期的兩種“產(chǎn)品”,這篇文章主要介紹了Java?properties?和?yml?的區(qū)別,需要的朋友可以參考下2023-02-02