1.用户登录业务介绍

1.1 单一服务器模式

早期单一服务器,用户认证。
缺点:单点性能压力,无法扩展

谷粒学院 - 图1

1.2 SSO(single sign on)模式

分布式,SSO(single sign on)模式

谷粒学院 - 图2

单点登录常见的三种方式:

1.session广播机制实现

2.cookie+redis实现

  1. redis key中生成唯一随机值,在value存放用户数据。cookie中存放rediskey值。

访问项目模块时,发送请求带着cookie进行发送,获取cookie值,到redis进行查询,如果查询到就是已登录,未查询到就重新登录。

3.使用token实现

token是什么?

  • 是按照一定规则生成的字符串,字符串可以包含用户信息

谷粒学院 - 图3

2.JWT进行跨域身份验证

传统用户身份验证

谷粒学院 - 图4

Internet服务无法与用户身份验证分开。一般过程如下︰

  1. 1.用户向服务器发送用户名和密码。
  2. 2.验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
  3. 3.服务器向用户返回session_id , session信息都会写入到用户的Cookie
  4. 4.用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
  5. 5.服务器收到session_id并对比之前保存的数据,确认用户的身份。
  6. 这种模式最大的问题是,没有分布式架构,无法支持横向扩展。

解决方案

  • session广播
  • 将透明令牌存入cookie,将用户身份信息写入redis

另外一种灵活的解决方案∶

  • 使用自包含令牌,通过客户端保存数据,而服务器不保存会话数据。JWT是这种解决方案的代表。

JWT令牌

访问令牌的类型

谷粒学院 - 图5

JWT生成字符串包含三部分

  • jwt头信息
  • 有效载荷 包含主题信息(用户信息)
  • 签名哈希 防伪标志

JWT的用法

客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。

此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。当跨域时,也可以将WT被放置于POST请求的数据主体中。

1.引入依赖

  1. <!-- JWT -->
  2. <dependency>
  3. <groupId>io.jsonwebtoken</groupId>
  4. <artifactId>jjwt</artifactId>
  5. <version>0.9.1</version>
  6. </dependency>

2.创建JWT工具类

  1. import io.jsonwebtoken.Claims;
  2. import io.jsonwebtoken.Jws;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.SignatureAlgorithm;
  5. import org.apache.commons.lang.StringUtils;
  6. import javax.servlet.http.HttpServletRequest;
  7. import java.util.Date;
  8. public class JwtUtils {
  9. /**EXPIRE设置token的过期时间*/
  10. public static final long EXPIRE = 1000 * 60 * 60 * 24;
  11. /**密钥*/
  12. public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
  13. /**生成token字符串的方法*/
  14. public static String getJwtToken(String id, String nickname){
  15. String JwtToken = Jwts.builder()
  16. /**设置jwt的头信息*/
  17. .setHeaderParam("typ", "JWT")
  18. .setHeaderParam("alg", "HS256")
  19. /**分类和过期时间*/
  20. .setSubject("hzb")
  21. .setIssuedAt(new Date())
  22. .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
  23. /**设置token的主体信息,存储用户信息*/
  24. .claim("id", id)
  25. .claim("nickname", nickname)
  26. .signWith(SignatureAlgorithm.HS256, APP_SECRET)
  27. .compact();
  28. return JwtToken;
  29. }
  30. /**
  31. * 判断token是否存在与有效
  32. * @param jwtToken
  33. * @return
  34. */
  35. public static boolean checkToken(String jwtToken) {
  36. if(StringUtils.isEmpty(jwtToken)) return false;
  37. try {
  38. Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. return false;
  42. }
  43. return true;
  44. }
  45. /**
  46. * 判断token是否存在与有效
  47. * @param request
  48. * @return
  49. */
  50. public static boolean checkToken(HttpServletRequest request) {
  51. try {
  52. String jwtToken = request.getHeader("token");
  53. if(StringUtils.isEmpty(jwtToken)) return false;
  54. Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
  55. } catch (Exception e) {
  56. e.printStackTrace();
  57. return false;
  58. }
  59. return true;
  60. }
  61. /**
  62. * 根据token获取会员id
  63. * @param request
  64. * @return
  65. */
  66. public static String getMemberIdByJwtToken(HttpServletRequest request) {
  67. String jwtToken = request.getHeader("token");
  68. if(StringUtils.isEmpty(jwtToken)) return "";
  69. Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
  70. Claims claims = claimsJws.getBody();
  71. return (String)claims.get("id");
  72. }
  73. }

JWT问题和趋势

  • WT不仅可用于认证,还可用于信息交换。善用WT有助于减少服务器请求数据库的次数。
  • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
  • 存储在客户端,不占用服务端的内存资源
  • JWT默认不加密,但可以加密。生或原始令牌后,可以再次对其进行加密。
  • 当JWT未加密时,—些私密数据无法通过JWT传输
  • JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦WT签发,在有效期内将会一直有效。
  • JWT本身包含认证信息、,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
  • 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

整合JWT令牌

1.在callback中生成jwt

  1. //生成jwt
  2. String token = JwtUtils.getJwtToken(member.getId(),member.getNickname());
  3. //存入cookie
  4. //CookieUtils.setCookie(request,response,"guli-jwt-token",token);
  5. //因为端口号不同存在跨域问题,cookie不能跨域,所有使用url重写
  6. return "redirect:http://localhost:3000?token=" + token;

2.在前端获取url的token值,去后台获取用户信息

  1. //前端http request 拦截器,request.js
  2. service.interceptors.request.use(
  3. config => {
  4. //debugger
  5. //判断cookie里面guli_token是否有值
  6. if (cookie.get('token')) {
  7. //吧获取的token放在头部headers中
  8. config.headers['token'] = cookie.get('token');
  9. }
  10. return config
  11. },
  12. err => {
  13. return Promise.reject(err);
  14. }
  15. )
  16. //default.vue
  17. created() {
  18. //获取路径里面token值
  19. this.token = this.$route.query.token
  20. //console.log(this.token)
  21. if(this.token) {//判断路径是否有token值
  22. this.wxLogin()
  23. }
  24. this.showInfo()
  25. },
  26. methods:{
  27. //微信登录显示的方法
  28. wxLogin() {
  29. //console.log('************'+this.token)
  30. //把token值放到cookie里面
  31. cookie.set('token',this.token,{domain: 'localhost'})
  32. cookie.set('user-center','',{domain: 'localhost'})
  33. //console.log('====='+cookie.get('guli_token'))
  34. //调用接口,根据token值获取用户信息
  35. loginApi.getUserInfo()
  36. .then(response => {
  37. //console.log('################'+response.data.data.userInfo)
  38. this.loginInfo = response.data
  39. cookie.set('user-center',this.loginInfo,{domain: 'localhost'})
  40. })
  41. },
  42. //创建方法,从cookie获取用户信息
  43. showInfo() {
  44. //从cookie获取用户信息
  45. var userStr = cookie.get('user-center')
  46. // 把字符串转换json对象(js对象)
  47. if(userStr) {
  48. this.loginInfo = JSON.parse(userStr)
  49. }
  50. },
  51. //退出
  52. logout() {
  53. //清空cookie值
  54. cookie.set('token','',{domain: 'localhost'})
  55. cookie.set('user-center','',{domain: 'localhost'})
  56. //回到首页面
  57. window.location.href = "/";
  58. }
  59. }

3.用户微服务

微信扫码登录

谷粒学院 - 图6

OAuth2

谷粒学院 - 图7

是针对特定问题的一种解决方案

主要解决两个问题

  • 开放系统间的授权
  • 分布式访问问题

优势

谷粒学院 - 图8

误解

谷粒学院 - 图9

令牌类型

谷粒学院 - 图10

OAuth2提出的背景

照片拥有者想要在云冲印服务上打印照片,云冲印服务需要访问云存储服务上的资源

谷粒学院 - 图11

资源拥有者:照片拥有者

客户应用:云冲印

受保护的资源:图片

谷粒学院 - 图12

方式一:用户名密码复制

适用于同一公司内部的多个系统,不适用于不受信的第三方应用

谷粒学院 - 图13

方式二:通用开发者key

适用于合作商或者授信不同业务的部门之间

方式三:办法令牌

接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议

wx

谷粒学院 - 图14

微信支付

谷粒学院 - 图15

  1. weixin:
  2. pay:
  3. #关联的公众号appid
  4. appid: wx74862e0dfcf69954
  5. #商户号
  6. partner: 1558950191
  7. #商户key
  8. partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  9. #回调地址
  10. notifyurl: http://guli.shop/api/order/weixinPay/weixinNotify

引入依赖

  1. <dependency>
  2. <groupId>com.github.wxpay</groupId>
  3. <artifactId>wxpay-sdk</artifactId>
  4. <version>0.0.3</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.alibaba</groupId>
  8. <artifactId>fastjson</artifactId>
  9. </dependency>

工具类 HttpClient

  1. package cn.cinz.utils;
  2. import org.apache.http.Consts;
  3. import org.apache.http.HttpEntity;
  4. import org.apache.http.NameValuePair;
  5. import org.apache.http.client.ClientProtocolException;
  6. import org.apache.http.client.entity.UrlEncodedFormEntity;
  7. import org.apache.http.client.methods.*;
  8. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
  9. import org.apache.http.conn.ssl.SSLContextBuilder;
  10. import org.apache.http.conn.ssl.TrustStrategy;
  11. import org.apache.http.entity.StringEntity;
  12. import org.apache.http.impl.client.CloseableHttpClient;
  13. import org.apache.http.impl.client.HttpClients;
  14. import org.apache.http.message.BasicNameValuePair;
  15. import org.apache.http.util.EntityUtils;
  16. import javax.net.ssl.SSLContext;
  17. import java.io.IOException;
  18. import java.security.cert.CertificateException;
  19. import java.security.cert.X509Certificate;
  20. import java.text.ParseException;
  21. import java.util.HashMap;
  22. import java.util.LinkedList;
  23. import java.util.List;
  24. import java.util.Map;
  25. /**
  26. * http请求客户端
  27. *
  28. * @author qy
  29. *
  30. */
  31. public class HttpClient {
  32. private String url;
  33. private Map<String, String> param;
  34. private int statusCode;
  35. private String content;
  36. private String xmlParam;
  37. private boolean isHttps;
  38. public boolean isHttps() {
  39. return isHttps;
  40. }
  41. public void setHttps(boolean isHttps) {
  42. this.isHttps = isHttps;
  43. }
  44. public String getXmlParam() {
  45. return xmlParam;
  46. }
  47. public void setXmlParam(String xmlParam) {
  48. this.xmlParam = xmlParam;
  49. }
  50. public HttpClient(String url, Map<String, String> param) {
  51. this.url = url;
  52. this.param = param;
  53. }
  54. public HttpClient(String url) {
  55. this.url = url;
  56. }
  57. public void setParameter(Map<String, String> map) {
  58. param = map;
  59. }
  60. public void addParameter(String key, String value) {
  61. if (param == null)
  62. param = new HashMap<String, String>();
  63. param.put(key, value);
  64. }
  65. public void post() throws ClientProtocolException, IOException {
  66. HttpPost http = new HttpPost(url);
  67. setEntity(http);
  68. execute(http);
  69. }
  70. public void put() throws ClientProtocolException, IOException {
  71. HttpPut http = new HttpPut(url);
  72. setEntity(http);
  73. execute(http);
  74. }
  75. public void get() throws ClientProtocolException, IOException {
  76. if (param != null) {
  77. StringBuilder url = new StringBuilder(this.url);
  78. boolean isFirst = true;
  79. for (String key : param.keySet()) {
  80. if (isFirst)
  81. url.append("?");
  82. else
  83. url.append("&");
  84. url.append(key).append("=").append(param.get(key));
  85. }
  86. this.url = url.toString();
  87. }
  88. HttpGet http = new HttpGet(url);
  89. execute(http);
  90. }
  91. /**
  92. * set http post,put param
  93. */
  94. private void setEntity(HttpEntityEnclosingRequestBase http) {
  95. if (param != null) {
  96. List<NameValuePair> nvps = new LinkedList<NameValuePair>();
  97. for (String key : param.keySet())
  98. nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
  99. http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
  100. }
  101. if (xmlParam != null) {
  102. http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
  103. }
  104. }
  105. private void execute(HttpUriRequest http) throws ClientProtocolException,
  106. IOException {
  107. CloseableHttpClient httpClient = null;
  108. try {
  109. if (isHttps) {
  110. SSLContext sslContext = new SSLContextBuilder()
  111. .loadTrustMaterial(null, new TrustStrategy() {
  112. // 信任所有
  113. public boolean isTrusted(X509Certificate[] chain,
  114. String authType)
  115. throws CertificateException {
  116. return true;
  117. }
  118. }).build();
  119. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
  120. sslContext);
  121. httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
  122. .build();
  123. } else {
  124. httpClient = HttpClients.createDefault();
  125. }
  126. CloseableHttpResponse response = httpClient.execute(http);
  127. try {
  128. if (response != null) {
  129. if (response.getStatusLine() != null)
  130. statusCode = response.getStatusLine().getStatusCode();
  131. HttpEntity entity = response.getEntity();
  132. // 响应内容
  133. content = EntityUtils.toString(entity, Consts.UTF_8);
  134. }
  135. } finally {
  136. response.close();
  137. }
  138. } catch (Exception e) {
  139. e.printStackTrace();
  140. } finally {
  141. httpClient.close();
  142. }
  143. }
  144. public int getStatusCode() {
  145. return statusCode;
  146. }
  147. public String getContent() throws ParseException, IOException {
  148. return content;
  149. }
  150. }

创建微信支付订单,传入生成的订单号

  1. //这是接口的实现类
  2. @Override
  3. public Result getQRCode(String orderNo) {
  4. try {
  5. //1.根据订单号查询订单信息
  6. Order order = orderService.getOrderByOrderNo(orderNo);
  7. //2.使用map设置生成二维码需要参数
  8. Map<String,String> map = new HashMap<>();
  9. map.put("appid","wx74862e0dfcf69954");
  10. map.put("mch_id", "1558950191");
  11. map.put("nonce_str", WXPayUtil.generateNonceStr());//UID
  12. map.put("body", order.getCourseTitle());
  13. map.put("out_trade_no", orderNo); //订单号
  14. map.put("total_fee", order.getTotalFee().multiply(new BigDecimal("100")).longValue()+"");//订单价格
  15. map.put("spbill_create_ip", "127.0.0.1"); //本地调试
  16. map.put("notify_url", "http://guli.shop/api/order/weixiPay/weixinNotify\n");
  17. map.put("trade_type", "NATIVE");
  18. //3.发送httpclient请求,传递参数xml格式,微信支付提供的固定地址
  19. HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
  20. //设置xml格式的参数
  21. httpClient.setXmlParam(WXPayUtil.generateSignedXml(map, "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"));//设置签名和商户key
  22. httpClient.setHttps(true);
  23. //执行请求
  24. httpClient.post();
  25. //4.得到发送请求返回结果
  26. //返回内容是xml格式
  27. String xml = httpClient.getContent();
  28. //把xml格式转换map集合
  29. Map<String,String> resultMap = WXPayUtil.xmlToMap(xml);
  30. //最终返回数据的封装
  31. Map<String,String> result = new HashMap<>();
  32. result.put("out_trade_no", orderNo);
  33. result.put("course_id", order.getCourseId());
  34. result.put("total_fee", String.valueOf(order.getTotalFee()));
  35. result.put("result_code", resultMap.get("result_code")); // 返回二维码操作状态码
  36. result.put("code_url", resultMap.get("code_url")); // 二维码地址
  37. return Result.SUCCESS().data(result);
  38. } catch (Exception e) {
  39. e.printStackTrace();
  40. throw new BaseException();
  41. }
  42. }

定时任务

1.启动类上添加注解@EnableScheduling //开启定时任务

  1. @EnableScheduling //开启定时任务

2.创建定时任务类

在这个类里面,使用表达式设置什么时候去执行

  • cron表达式设置执行规则
  • @Scheduled(cron = "0/5 * * * * ?") 每隔五秒执行一次
  1. import cn.cinz.statistics.service.StatisticsDailyService;
  2. import cn.cinz.utils.DateUtil;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.scheduling.annotation.Scheduled;
  7. import org.springframework.stereotype.Component;
  8. import java.util.Date;
  9. @Component
  10. public class ScheduledTask {
  11. private final Logger logger = LoggerFactory.getLogger(ScheduledTask.class);
  12. @Autowired
  13. private StatisticsDailyService statisticsDailyService;
  14. // 每隔五秒执行一次
  15. // @Scheduled(cron = "0/5 * * * * ?")
  16. // public void task1(){
  17. // logger.info("执行了定时任务");
  18. // }
  19. //每天凌晨0点,把前一天数据进行添加
  20. @Scheduled(cron = "0 0 0 1/1 * ?")
  21. public void task(){
  22. logger.info("每天零点执行任务统计任务");
  23. statisticsDailyService.getRegisterDayCount(DateUtil.getYestdayStr(new Date()));
  24. }
  25. }

3.在线生成cron表达式

https://cron.qqe2.com/

https://www.pppet.net/

4.分布式任务调度平台 xxl-job

Canal

1.应用场景

在前面的统计分析功能中,我们采取了服务调用获取统计数据,这样耦合度高,效率相对较低,目前我采取另一种实现方式,通过实时同步数据库表的方式实现,例如我们要统计每天注册与登录人数,我们只需把会员表同步到统计库中,实现本地统计就可以了,这样效率更高,耦合度更低,canal就是一个很好的数据库同步工具。canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL。

2.Canal环境搭建

canal的原理是基于mysql binlog技术,所以这里一定需要开启mysql的binlog写入功能开启mysql服务:service mysql start(或者systemctl start mysqld.service )

spring security

谷粒学院 - 图16

jenkins

必备环境

java环境

  1. #安装jdk8
  2. $ yum install -y java-1.8.0-openjdk.x86_64
  3. # 环境变量配置
  4. $ vim /etc/profile
  5. # 添加如下信息
  6. export JAVA_HOME=/usr/local/jdk
  7. export JRE_HOME=$JAVA_HOME/jre
  8. export CLASSPATH=./$CLASSPATH:$JAVA_HOME/lib:$JRE_HOME/lib
  9. export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
  10. #保存后,让文件立即生效
  11. $ source /etc/profile

安装maven

安装docker

更新并安装Docker-CE

  1. $ yum makecache fast
  2. $ yum -y install docker-ce
  3. #开启docker服务
  4. $ service docker start
  5. #测试是否安装成功
  6. $ docker -v

服务器部署

1.安装nacos

官方文档:https://nacos.io/zh-cn/docs/quick-start.html

  1. #下载nacos,我下载的是1.4.3的版本
  2. #服务器选择nacos-server-1.4.3.tar.gz
  3. https://github.com/alibaba/nacos/releases
  4. #放置服务器目录如:/usr/local下,解压后的目录结构为/usr/local/nacos
  5. $ tar -zxvf nacos-server-1.4.3.tar.gz

a.修改nacos/conf目录下的application.properties

  1. $ vim nacos/conf/application.properties

谷粒学院 - 图17

b.编辑 vim startup.sh脚本,修改

谷粒学院 - 图18

保存退出。

c.首先创建一个数据库nacos,然后运行数据库脚本,在源码包有,在 nacos/conf/nacos-mysql.sql,把这个导入到数据中

d.启动nacos

  • 注:Nacos的运行需要以至少2C4g60g*3的机器配置下运行。

运行文件在nacos/bin

  1. #Linux/Unix/Mac
  2. #启动命令(standalone代表着单机模式运行,非集群模式):
  3. $ sh startup.sh -m standalone
  4. #如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:
  5. $ bash startup.sh -m standalone
  6. $Windows
  7. 启动命令(standalone代表着单机模式运行,非集群模式):
  8. $ startup.cmd -m standalone

e.关闭nacos

运行文件在nacos/bin

  1. #Linux/Unix/Mac
  2. $sh shutdown.sh
  3. #Windows
  4. $shutdown.cmd
  5. #或者双击shutdown.cmd运行文件。

f.将nacos config配置中心的文件导入

谷粒学院 - 图19

2.安装jdk

修改环境变量 vim /etc/profile

添加如下信息

  1. export JAVA_HOME=jdk安装位置
  2. export PATH=$JAVA_HOME/bin:$PATH
  3. export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

然后刷新下文件 source /etc/profile

查看是否配置成功,使用 java -versionecho $JAVA_HOME 分别查看

3.打包本地jar,放置服务器运行

运行脚本可参考

  1. $ nohup java -jar -Xms50m -Xmx100m -XX:PermSize=64M -XX:MaxPermSize=128M /root/app/halo.jar > nohup 2>&1 &

解读:后台不间断运行java程序 并将日志文件输出到 nohub 中

列出所有包含 .jar java 的进程

  1. $ ps -ef | grep java | grep .jar

强制关闭掉包含 .jar java 的进程

  1. $ ps -ef | grep java | grep .jar | xargs kill -9

4.安装redis

因为项目中有使用redis,这里需要安装

5.安装mysql

mysql安装选择8.*版本

6.安装nginx

配置nginx.conf文件

  1. #*.*.*.* 为服务器ip地址
  2. location ~ /admin/acl
  3. {
  4. #最终转发到服务器的地址
  5. proxy_pass http://*.*.*.*:8011;
  6. }
  7. location ~ /api/
  8. {
  9. #最终转发到服务器的地址
  10. proxy_pass http://*.*.*.*:8011;
  11. }