总体思路
创建数据库表
create table t_member( id int(11) not null auto_increment, loginacct varchar(255) not null, userpswd char(200) not null, username varchar(255), email varchar(255), authstatus tinyint(4) comment '实名认证状态 0 - 未实名认证, 1 - 实名认证申请中, 2 - 已实名认证', usertype tinyint(4) comment ' 0 - 个人, 1 - 企业', realname varchar(255), cardnum varchar(255), accttype tinyint(4) comment '0 - 企业, 1 - 个体, 2 - 个人, 3 - 政府', primary key (id));
api接口调用微服务service
发送验证码
目标
1、将验证码发送到用户手机上
2、将验证码存入redis
思路
准备短信发送API
导入依赖
<dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.15version> dependency> <dependency> <groupId>commons-langgroupId> <artifactId>commons-langartifactId> <version>2.6version> dependency> <dependency> <groupId>org.eclipse.jettygroupId> <artifactId>jetty-utilartifactId> <version>9.3.7.v20160115version> dependency>
加入HttpUtil类
依赖
<dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.15version> dependency> <dependency> <groupId>org.apache.httpcomponentsgroupId> <artifactId>httpclientartifactId> <version>4.2.1version> dependency> <dependency> <groupId>org.apache.httpcomponentsgroupId> <artifactId>httpcoreartifactId> <version>4.2.1version> dependency> <dependency> <groupId>commons-langgroupId> <artifactId>commons-langartifactId> <version>2.6version> dependency> <dependency> <groupId>org.eclipse.jettygroupId> <artifactId>jetty-utilartifactId> <version>9.3.7.v20160115version> dependency>
代码
package com.aliyun.api.gateway.demo.util; public class HttpUtils { /** * get * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doGet(String host, String path, String method, Mapheaders, Map querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpGet request = new HttpGet(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } /** * post form * * @param host * @param path * @param method * @param headers * @param querys * @param bodys * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, Map bodys) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (bodys != null) { List nameValuePairList = new ArrayList (); for (String key : bodys.keySet()) { nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key))); } UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8"); formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); request.setEntity(formEntity); } return httpClient.execute(request); } /** * Post String * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Post stream * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map headers, Map querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Put String * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map headers, Map querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Put stream * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map headers, Map querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Delete * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doDelete(String host, String path, String method, Map headers, Map querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpDelete request = new HttpDelete(buildUrl(host, path, querys)); for (Map.Entry e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } private static String buildUrl(String host, String path, Map querys) throws UnsupportedEncodingException { StringBuilder sbUrl = new StringBuilder(); sbUrl.append(host); if (!StringUtils.isBlank(path)) { sbUrl.append(path); } if (null != querys) { StringBuilder sbQuery = new StringBuilder(); for (Map.Entry query : querys.entrySet()) { if (0 < sbQuery.length()) { sbQuery.append("&"); } if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { sbQuery.append(query.getValue()); } if (!StringUtils.isBlank(query.getKey())) { sbQuery.append(query.getKey()); if (!StringUtils.isBlank(query.getValue())) { sbQuery.append("="); sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8")); } } } if (0 < sbQuery.length()) { sbUrl.append("?").append(sbQuery); } } return sbUrl.toString(); } private static HttpClient wrapClient(String host) { HttpClient httpClient = new DefaultHttpClient(); if (host.startsWith("https://")) { sslClient(httpClient); } return httpClient; } private static void sslClient(HttpClient httpClient) { try { SSLContext ctx = SSLContext.getInstance("TLS"); X509TrustManager tm = new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(X509Certificate[] xcs, String str) { } @Override public void checkServerTrusted(X509Certificate[] xcs, String str) { } }; ctx.init(null, new TrustManager[]{tm}, null); SSLSocketFactory ssf = new SSLSocketFactory(ctx); ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); ClientConnectionManager ccm = httpClient.getConnectionManager(); SchemeRegistry registry = ccm.getSchemeRegistry(); registry.register(new Scheme("https", 443, ssf)); } catch (KeyManagementException ex) { throw new RuntimeException(ex); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } } }
创建工具方法
/** * @param host 短信接口调用url地址 * @param path 具体发送短信地址 * @param method 请求方法 * @param phoneNum 手机号 * @param appCode 应用码 * @param sign * @param skin * @return 成功返回验证码 */ public static ResultEntitysendCodeByShortMessage( String host, String path, String method, String phoneNum, String appCode, String sign, String skin) { Map headers = new HashMap (); //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.put("Authorization", "APPCODE " + appCode); Map querys = new HashMap (); // 验证码 StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < 4; i++) { int random = (int) (Math.random() * 10); stringBuilder.append(random); } String code = stringBuilder.toString(); querys.put("param", code); querys.put("phone", phoneNum); querys.put("sign", sign); querys.put("skin", skin); //JDK 1.8示例代码请在这里下载: http://code.fegine.com/Tools.zip try { /** * 重要提示如下: * HttpUtils请从 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java * 或者直接下载: * http://code.fegine.com/HttpUtils.zip * 下载 * * 相应的依赖请参照 * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml * 相关jar包(非pom)直接下载: * http://code.fegine.com/aliyun-jar.zip */ HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys); //System.out.println(response.toString());如不输出json, 请打开这行代码,打印调试头部状态码。 //状态码: 200 正常;400 URL无效;401 appCode错误; 403 次数用完; 500 API网管错误 StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); String reasonPhrase = statusLine.getReasonPhrase(); if (statusCode == 200) { return ResultEntity.sucessWithData(code); } return ResultEntity.failed(reasonPhrase); } catch (Exception e) { e.printStackTrace(); return ResultEntity.failed(e.getMessage()); } }
从yml配置文件读取数据
导入依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-configuration-processorartifactId> <optional>trueoptional> dependency>
编写配置文件
编写短信property类
package com.adom.authentication.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "short.message") public class ShortMessageProperties { // 调用短信发送接口时的访问地址 private String host; // 具体访问路径 private String path; // 请求方式 private String method; // 登录阿里云后,进入管理控制台->云市场->已购买服务,复制AppCode private String appCode; // 签名编号 private String sign; // 模板编号 private String skin; public ShortMessageProperties() { } public ShortMessageProperties(String host, String path, String method, String appCode, String sign, String skin) { this.host = host; this.path = path; this.method = method; this.appCode = appCode; this.sign = sign; this.skin = skin; }
前端思路(忽略)
后端代码
创建发送短信验证码的controller
1、先发送短信,若发送成功,则把验证码加入到redis
@ResponseBody @RequestMapping(value = "/auth/member/send/short/message.json", method = RequestMethod.POST) public ResultEntitysendMessage(@RequestParam("phoneNum") String phoneNum) { // 1.发送验证码到手机 ResultEntity sendMeassageResultEntity = CrowdUtil.sendCodeByShortMessage( shortMessageProperties.getHost(), shortMessageProperties.getPath(), shortMessageProperties.getMethod(), phoneNum, shortMessageProperties.getAppCode(), shortMessageProperties.getSign(), shortMessageProperties.getSkin()); // 2,判断短信发送的结果 if (ResultEntity.SUCCESS.equals(sendMeassageResultEntity.getResult())) { // 3.如果发送成功,则将验证码存入redis String code = sendMeassageResultEntity.getData(); String key = ConstantUtil.REDIS_CODE_PREFIX + phoneNum; ResultEntity saveCodeResultEntity = redisRemoteService.setRedisKeyValueRemoteWithTimeout(key, code, 15, TimeUnit.MINUTES); if (ResultEntity.SUCCESS.equals(saveCodeResultEntity.getResult())) { return ResultEntity.successWithoutData(); } else { return saveCodeResultEntity; } } else { return sendMeassageResultEntity; } }
redis中保存验证码数据的key
声明redis-provider功能api
@FeignClient("adom-crowd-redis") public interface RedisRemoteService { @RequestMapping("/set/redis/key/value/remote") ResultEntitysetRedisKeyValueRemote( @RequestParam("key") String key, @RequestParam("value") String value); @RequestMapping("/set/redis/key/value/remote/with/timeout") ResultEntity setRedisKeyValueRemoteWithTimeout( @RequestParam("key") String key, @RequestParam("value") String value, @RequestParam("time") long time, @RequestParam("timeUnix") TimeUnit timeUnit); @RequestMapping("/get/redis/string/value/by/key") ResultEntity getRedisStringValueByKeyRemote(@RequestParam("key") String key); @RequestMapping("/remove/redis/key/remote") ResultEntity removeRedisKeyRemote(@RequestParam("key") String key); }
Redis-provider功能实现
@RestController public class RedisController { @Autowired private StringRedisTemplate redisTemplate; @RequestMapping("set/redis/key/value/remote") ResultEntitysetRedisKeyValueRemote( @RequestParam("key") String key, @RequestParam("value")String value){ try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key,value); return ResultEntity.successWithoutData(); } catch (Exception e) { e.printStackTrace(); return ResultEntity.failed(e.getMessage()); } } @RequestMapping("set/redis/key/value/remote/with/timeout") ResultEntity setRedisKeyValueRemoteWithTimeout( @RequestParam("key") String key, @RequestParam("value")String value, @RequestParam("time") long time, @RequestParam("timeUnit") TimeUnit timeUnit){ try { ValueOperations operations = redisTemplate.opsForValue(); operations.set(key,value,time,timeUnit); return ResultEntity.successWithoutData(); } catch (Exception e) { e.printStackTrace(); return ResultEntity.failed(e.getMessage()); } } @RequestMapping("get/redis/string/value/by/key") ResultEntity getRedisStringValueByKey(@RequestParam("key") String key){ try { ValueOperations operations = redisTemplate.opsForValue(); String value = operations.get(key); return ResultEntity.sucessWithData(value); } catch (Exception e) { e.printStackTrace(); return ResultEntity.failed(e.getMessage()); } } @RequestMapping("remove/redis/key/remote") ResultEntity removeRedisKeyRemote(@RequestParam("key") String key){ try { redisTemplate.delete(key); return ResultEntity.successWithoutData(); } catch (Exception e) { e.printStackTrace(); return ResultEntity.failed(e.getMessage()); } } }
代码缺陷(未解决)
分布式事务
举例
发送短信操作和redis保存操作在执行时都有可能失败.如果发生其中一个操作失败,另一个成功,那么数据整体就会发生不一致的情况.
分析
如果是在以前关系型数据库的事务操作中,可以采用回滚机制,一个事物多个操作中有任何
一个失败,则整个事务全部回滚.
在分布式环境下,没办法将多个具体操作纳入到一个统一的事务中,一起提交、一起回滚.
也不能使用逆操作手动撤销,因为逆操作除了功能和原本操作不同,其他方面和原本的操作性质是一样的.
解决方法
从yml配置文件读取数据
加入依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-configuration-processorartifactId> <optional>trueoptional> dependency>
yml配置
这些配置项全部都是自定义的,完全没有框架的支持.
short:
message:
app-code: 303208760dc14fb994c6dfba5c82e1e6
host: https://feginesms.market.alicloudapi.com
method: GET
path: /codeNotice
sign: 1
skin: 1
在java类中引用方式
@Component @ConfigurationProperties(prefix = "short.message") public class ShortMessageProperties {
意义:以后在SpringBoot环境下开发时,如果某些信息不能在java代码中写死,就可以使用这样的机制在yml配置文件中配置,再使用@value注解读取.yml或properties文件都可以.
执行注册
流程分析
目标
如果针对注册操作所做的各项验证能够通过,则将Member信息存入数据库.
思路
1、接受表单数据 a)登陆账号 b)密码 c)手机号 d)验证码 2、检查验证码是否有效 a)无效:返回失败信息,停止执行 b)有效继续执行 3、检查手机号是否有效 a)无效:返回失败信息,停止执行 b)有效:继续执行 4、拼接接收到验证码的key 5、调用redis-provider的api方法获取对应的验证码值 a)没有查询到值:返回失败信息,停止执行 b)查询到有效值:继续执行 6、进行比较 a)表单提交的验证码 b)redis取回的验证码 7、不一致:返回失败信息,停止执行 8、一致 a)从redis中删除当前key对应的验证码 b)继续执行 9、调用数据库api方法检查登陆账号是否被占用 a)已经被占用:返回失败信息,停止执行 b)没有被占用:继续执行 10、密码加密 11、调用数据库api方法检查登陆账号是否被占用
封装MemberVO
public class MemberVO { private String loginAcct; private String userPswd; private String email; private String userName; private String phoneNum; private String code; public MemberVO() { } public MemberVO(String loginAcct, String userPswd, String email, String userName, String phoneNum, String code) { this.loginAcct = loginAcct; this.userPswd = userPswd; this.email = email; this.userName = userName; this.phoneNum = phoneNum; this.code = code; }
调用数据库provider的api
@RequestMapping("/save/member/remote") public ResultEntitysaveMember(@RequestBody MemberPO memberPO);
数据库provider
数据库保存
Controller
@RequestMapping("/save/member/remote") public ResultEntitysaveMember(@RequestBody MemberPO memberPO) { try { memberService.saveMember(memberPO); return ResultEntity.successWithoutData(); } catch (Exception e) { if (e instanceof DuplicateKeyException) { return ResultEntity.failed(ConstantUtil.MESSAGE_LOGIN_ACCT_ALREADY_IN); } return ResultEntity.failed(e.getMessage()); } }
Service
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class, readOnly = false) public void saveMember(MemberPO memberPO) { memberPOMapper.insertSelective(memberPO); }
Mapper
int insertSelective(MemberPO record);
Sql
<insert id="insertSelective" parameterType="com.example.entity.po.MemberPO" > insert into t_member <trim prefix="(" suffix=")" suffixOverrides="," > <if test="id != null" > id, if> <if test="loginAcct != null" > login_acct, if> <if test="userPswd != null" > user_pswd, if> <if test="userName != null" > user_name, if> <if test="email != null" > email, if> <if test="authStatus != null" > auth_status, if> <if test="userType != null" > user_type, if> <if test="realName != null" > real_name, if> <if test="cardNum != null" > card_num, if> <if test="acctType != null" > acct_type, if> trim> <trim prefix="values (" suffix=")" suffixOverrides="," > <if test="id != null" > #{id,jdbcType=INTEGER}, if> <if test="loginAcct != null" > #{loginAcct,jdbcType=VARCHAR}, if> <if test="userPswd != null" > #{userPswd,jdbcType=CHAR}, if> <if test="userName != null" > #{userName,jdbcType=VARCHAR}, if> <if test="email != null" > #{email,jdbcType=VARCHAR}, if> <if test="authStatus != null" > #{authStatus,jdbcType=INTEGER}, if> <if test="userType != null" > #{userType,jdbcType=INTEGER}, if> <if test="realName != null" > #{realName,jdbcType=VARCHAR}, if> <if test="cardNum != null" > #{cardNum,jdbcType=VARCHAR}, if> <if test="acctType != null" > #{acctType,jdbcType=INTEGER}, if> trim> insert>
执行注册
controller
@RequestMapping("/auth/do/member/register") public String register(MemberVO memberVO, ModelMap modelMap) { // 1.获取用户输入的手机号 String phoneNum = memberVO.getPhoneNum(); // 2.拼redis中存储验证码的key String key = ConstantUtil.REDIS_CODE_PREFIX + phoneNum; // 3.从redis读取key对应的value ResultEntityresultEntity = redisRemoteService.getRedisStringValueByKeyRemote(key); // 4.查询操作是否有效 String result = resultEntity.getResult(); if (ResultEntity.FAILED.equals(result)) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, resultEntity.getMessage()); return "member-reg"; } String redisCode = resultEntity.getData(); if (redisCode == null) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, ConstantUtil.MESSAGE_CODE_NOT_EXISTS); return "member-reg"; } // 5.如果能够到value则比较表单的验证码和redis的验证码 String formCode = memberVO.getCode(); if (!Objects.equals(formCode, redisCode)) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, ConstantUtil.MESSAGE_CODE_INVALID); return "member-reg"; } // 6.如果验证码一致,则从redis删除 redisRemoteService.removeRedisKeyRemote(key); // 7.执行密码加密 BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String userpswd = memberVO.getUserPswd(); String encode = passwordEncoder.encode(userpswd); memberVO.setUserPswd(encode); // 8.执行保存 // 创建空的MemberPO对象 MemberPO memberPO = new MemberPO(); // 复制属性 BeanUtils.copyProperties(memberVO, memberPO); // 调用远程方法 ResultEntity saveMemberResultEntity = mySQLRemoteService.saveMember(memberPO); if (ResultEntity.FAILED.equals(saveMemberResultEntity)) { modelMap.addAttribute(ConstantUtil.ATTR_NANE_MESSAGE, saveMemberResultEntity.getMessage()); return "member-reg"; } return "redirect:/auth/member/to/login/page"; }
一些问题
SpringBoot注入失败的原因有哪些
SpringIOC找不到Bean对象
1、没有开启扫描的包
2、没有注入Bean对象
3、没有添加组件注解
4、方法没有@Bean修饰
@FeignClients需要加入扫描哪些包才可以找到bean对象
mybatis之 trim prefix="(" suffix=")"
trim元素的主要功能是可以在自己包含的内容前加上某些前缀,也可以在其后加上某些后缀,与之对应的属性是prefix和suffix;可以把包含内容的首部某些内容覆盖,即忽略,也可以把尾部的某些内容覆盖,对应的属性是prefixOverrides和suffixOverrides;正因为trim有这样的功能,所以我们也可以非常简单的利用trim来代替where元素的功能。
1.
prefix:在trim标签内sql语句加上前缀。
suffix:在trim标签内sql语句加上后缀。
suffixOverrides:指定去除多余的后缀内容,如:suffixOverrides=",",去除trim标签内sql语句多余的后缀","。
prefixOverrides:指定去除多余的前缀内容
suffixOverrides=","
执行的sql语句也许是这样的:insert into cart (id,user_id,deal_id,) values(1,2,1,);显然是错误的
指定之后语句就会变成insert into cart (id,user_id,deal_id) values(1,2,1);这样就将“,”去掉了。
前缀同理。
获取上述短信接口的appcode
阿里云-》云市场-》API-》电子商务找到如下:
购买成功后