[TOC]
网页静态化技术和缓存技术的共同点都是为了减轻数据库的访问压力,但是具体的应用场景不同,缓存比较适合小规模的数据,而网页静态化比较适合大规模且相对变化不太频繁的数据。另外网页静态化还有利于 SEO(搜索引擎优化)。静态界面通过 Nginx 服务器部署可以达到5万的并发,而Tomcat只有几百。
Freemarker 模板引擎,基于模板来生成文本输出。与web容器无关。
模板文件的元素
- 文本,直接输出的部分
- 注释,<#-- 该内容不会输出 -->
- 插值,${...} 将使用数据模型中的部分来替代输出
- FTL 指令,实现逻辑
生成文件
public static void main(String[] args) throws IOException, TemplateException {
// 1. 创建一个配置对象
Configuration configuration = new Configuration(Configuration.getVersion());
// 2. 设置模板所在的目录
configuration.setDirectoryForTemplateLoading(new File("E:\\eclipse-workspace\\freemarkerDemo\\src\\main\\resources\\"));
// 3. 设置默认字符编码
configuration.setDefaultEncoding("utf-8");
// 4. 加载模板,创建一个模板对象
Template template = configuration.getTemplate("test.ftl");
// 5. 模板的数据集模型
Map<String, String> map = new HashMap<String, String>();
map.put("name", "Mindyu");
map.put("message", "this is a freemarker demo!");
// 6. 模板输出流对象
Writer out = new FileWriter("d:\\src\\test.html");
// 7. 输出文件
template.process(map, out);
// 8. 关闭输出流对象
out.close();
}
FTL 指令
-
assgin 用于在页面上定义一个变量:<#assign info={"mobile":"aa",'address':'11'} >
-
include 用于模板文件的嵌套:<#include "head.ftl">
-
if 指令 条件判断语句
-
list 指令 对集合的遍历 (goods_index 获得索引)
<#list goodsList as goods> ${goods_index+1} 商品名称: ${goods.name} 价格:${goods.price}<br> </#list>
内建函数 (语法格式:变量+?+函数名称)
- ${goodsList?size} 获取集合的大小
- <#assign object=text?eval> 转换 JSON 字符串为对象
- ${today?date} 当前日期 (dataModel.put("today", new Date());)
- ${today?time} 当前时间
- ${today?datetime} 当前日期+时间
- ${today?string("yyyy年MM月")} 日期格式化
- ${number} 数字会以每三位一个分隔符显示 123,456,789
- ${number?c} 将数字转换为字符串
- 空值处理运算符
- variable?? 判断变量是否存在,存在则返回true
- ${aaa!'-'} 缺失变脸默认值,若aaa为空值则使用默认值‘-’
- 运算符
- 算数运算符 +、-、*、/
- 逻辑运算符 && || !
- 比较运算符 = 、==、!=、>(gt)、<(lt)、>=(gte)、<=(lte)
商品详情页的数据显示
创建 pinyougou-page-interface 工程,创建 com.pinyougou.page.service 包,包下创建接口 ItemPageService。然后再创建服务层,来实现接口方法。pom 文件中添加 freemarker 依赖。Spring 配置文件中添加 freemarker 的bean.
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/ftl/" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
服务层生成静态页面的方法:
@Override
public boolean genItemHtml(Long goodsId) {
try {
Configuration configuration = freeMarkerConfig.getConfiguration();
Template template = configuration.getTemplate("item.ftl");
// 创建数据模型
Map<Object, Object> dataModel = new HashMap<>();
// 1.商品主表信息
TbGoods goods = goodsMapper.selectByPrimaryKey(goodsId);
dataModel.put("goods", goods);
// 2.商品详细信息
TbGoodsDesc goodsDesc = goodsDescMapper.selectByPrimaryKey(goodsId);
dataModel.put("goodsDesc", goodsDesc);
// 3.读取商品分类
String itemCat1 = itemCatMapper.selectByPrimaryKey(goods.getCategory1Id()).getName();
String itemCat2 = itemCatMapper.selectByPrimaryKey(goods.getCategory2Id()).getName();
String itemCat3 = itemCatMapper.selectByPrimaryKey(goods.getCategory3Id()).getName();
dataModel.put("itemCat1", itemCat1);
dataModel.put("itemCat2", itemCat2);
dataModel.put("itemCat3", itemCat3);
// 4.读取SKU列表信息
TbItemExample example = new TbItemExample();
Criteria criteria = example.createCriteria();
criteria.andGoodsIdEqualTo(goodsId); // 设置SPU信息
criteria.andStatusEqualTo("1"); // 存在状态
example.setOrderByClause("is_default desc"); // 按是否默认降序排序,目的是为了方便前端可以直接取出默认选项
List<TbItem> itemList = itemMapper.selectByExample(example);
dataModel.put("itemList", itemList);
Writer out = new FileWriter("D:\\src\\item\\"+goodsId+".html");
template.process(dataModel, out);
out.close();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}
return false;
}
在运营商管理后台引入依赖,因为需要在运营商审核之后生成静态页面。
freemarker 图片列表的生成(扩展属性、规格列表类似)
通过 assign指令,将字符串转换为对象格式<#assign imageList=goodsDesc.itemImages?eval />
,然后在图片显示区遍历图片对象。
<!--默认第一个预览-->
<div id="preview" class="spec-preview">
<#if (imageList?size>0)>
<span class="jqzoom"><img jqimg="${imageList[0].url}" src="${imageList[0].url}" width="400px" height="400px"/></span>
</#if>
</div>
<!--下方的缩略图-->
<div class="spec-scroll">
<a class="prev"><</a>
<!--左右按钮-->
<div class="items">
<ul>
<#list imageList as item>
<li><img src="${item.url}" bimg="${item.url}" onmousemove="preview(this)" /></li>
</#list>
</ul>
</div>
<a class="next">></a>
</div>
商品详情页-前端逻辑
静态页面的动态效果,就需要 angularjs 来实现。比如商品购买数量的点击事件对应到angularjs的变量中、规格的选择。都已变量的形式与页面进行绑定。
不同规格的标题、价格等信息都不相同(SKU信息),为了实现静态页面的效果可以在将SKU信息生成到静态页面。以变量的形式保存在前端。然后用户点击不同规格时,去匹配对应的SKU列表中的某一条数据。
//控制层
app.controller('itemController' ,function($scope){
$scope.specificationItems={}; // 存储用户选择的规格
// 数量加减
$scope.addNum=function(x){
$scope.num+=x;
if ($scope.num<1) $scope.num=1;
}
// 选择规格
$scope.selectSpecification=function(key,value){
$scope.specificationItems[key]=value;
searchSku(); // 查询sku
}
// 判断规格是否被选中
$scope.isSelected=function(key,value){
if($scope.specificationItems[key]==value){
return true;
}return false;
}
$scope.sku={};
// 加载默认的sku信息
$scope.loadSku=function(){
$scope.sku=skuList[0];
$scope.specificationItems=JSON.parse(JSON.stringify($scope.sku.spec)); // 深克隆
}
// 判断两个对象是否匹配
isEqual=function(map1,map2){
for(var k in map1){
if(map1[k]!=map2[k]){
return false;
}
}
for(var k in map2){
if(map2[k]!=map1[k]){
return false;
}
}
return true;
}
// 根据规格查询sku信息
searchSku=function(){
for(var i=0;i<skuList.length;i++){
if( isEqual($scope.specificationItems, skuList[i].spec) ){
$scope.sku=skuList[i];
return;
}
}
$scope.sku={id:0,title:'--------',price:0};
}
// 添加到购物车
$scope.addToCart=function(){
alert('sku_id:'+ $scope.sku.id);
}
});
系统模块的对接
运营商管理后台在审核之后进行静态页面的生成。创建 page-web 工程,用于存储生成页面。实现前端 angular 动态逻辑和静态模板的实现。
修改搜索系统模块中的search.html。点击搜索页面的图片跳转到静态页面。
消息中间件
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。对于消息中间件,常见的角色大致也就有 Producer(生产者)、Consumer(消费者)。
常见产品:
- ActiveMQ Apache 出品,最流行的,能力强劲的开源消息总线。
- RabbitMQ AMQP 协议的领导实现,支持多种场景。
- ZeroMQ 史上最快的消息队列系统
- Kafka 高吞吐,在一台普通的服务器上就可以达到 10W/s的吞吐速率;完全的分布式系统。适合处理海量数据。
JMS(Java 消息服务)
Java 平台上有关面向消息中间件的技术规范,它便于消息系统中的 Java 应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发。是一系列接口规范。
消息是 JMS 中的一种类型对象,由两部分组成:报头和消息主体。报头由路由信息以及有关该消息的元数据组成。消息主体则携带着应用程序的数据或有效负载。消息正文格式:
- TextMessage--一个字符串对象
- MapMessage--一套名称-值对
- ObjectMessage--一个序列化的 Java 对象
- BytesMessage--一个字节的数据流
- StreamMessage -- Java 原始值的数据流
JMS 消息传递类型
- 点对点模式:一个生产者一个消费者,存在多个消费者时,只有一个消费者可以获取消息。(未消费的消息会存储在队列中直到被消费)
- 发布订阅模式:一个生产者产生消息并进行发送后,可以由多个消费者进 行接收。(如果消息发送时没有消费者,那么这个消息无效,不会再被消费)
安装
下载、解压、赋权、启动服务(./activemq start)。ActiveMQ 管理页面端口8161。(用户:admin 密码:admin)
点对点模式案例
引入依赖
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>5.13.4</version>
</dependency>
消息生产者:
public static void main(String[] args) throws JMSException {
// 1. 创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.130:61616");
// 2. 创建连接对象
Connection connection = connectionFactory.createConnection();
// 3. 启动连接
connection.start();
// 4. 获取session(会话对象) 参数1:是否启动事务 参数2:消息确认方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 创建队列对象
Queue queue = session.createQueue("test-queue");
// 6. 创建消息生产者对象
MessageProducer producer = session.createProducer(queue);
// 7. 创建消息对象(TextMessage)
TextMessage message = session.createTextMessage("这是一条text消息");
// 8. 发送消息
producer.send(message);
// 9. 关闭资源
producer.close();
session.close();
connection.close();
}
注:创建session的第二个参数为消息确认模式:AUTO_ACKNOWLEDGE = 1 自动确认、CLIENT_ACKNOWLEDGE = 2 客户端手动确认、DUPS_OK_ACKNOWLEDGE = 3 自动批量确认、SESSION_TRANSACTED = 0 事务提交并确认。
消息消费者:
public static void main(String[] args) throws JMSException, IOException {
// 1. 创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.130:61616");
// 2. 创建连接对象
Connection connection = connectionFactory.createConnection();
// 3. 启动连接
connection.start();
// 4. 获取session(会话对象) 参数1:是否启动事务 参数2:消息确认方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 5. 创建队列对象
Queue queue = session.createQueue("test-queue");
// 6. 创建消息的消费者对象
MessageConsumer consumer = session.createConsumer(queue);
// 7. 设置监听
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(""+ textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// 8. 等待键盘输入
System.in.read();
// 9. 关闭资源
consumer.close();
session.close();
connection.close();
}
发布订阅模式
只需要修改上述第五步中,创建对应的主题对象即可Topic topic = session.createTopic("test-topic");
JMS 应用
运营商后台管理模块中,商品审核之后需要导入 solr 索引库和生成静态页面。对于这种同步调用的情况存在耦合度高、后期不易维护、同步执行、导致审核过程缓慢、用户体验性不好等多种问题。我们可以采用消息中间件来进行解耦,实现运营商后端与搜索服务的零耦合。运营商执行审核后,向activeMQ 发送消息(SKU列表),搜索服务从activeMQ接收到消息执行导入操作。
然后搜索模块采用 solr 系统实现,那么我们可以采用点对点的方式实现消息服务,而静态页面生成服务,由于静态页面存储于多个服务器,并且各个服务器数据相同,需要实现服务器之间同步更新的效果,所以需要采用发布订阅的方式实现。
导入搜索系统的消息生产者实现:
- 解除耦合(移除itemService服务依赖)
- 引入activeMQ客户端依赖、spring-jms依赖。
- 创建jms生产者配置文件
<!-- 真正可以产生Connection的ConnectionFactory,由对应的 JMS服务厂商提供-->
<bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.25.130:61616"/>
</bean>
<!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory -->
<property name="targetConnectionFactory" ref="targetConnectionFactory"/>
</bean>
<!-- Spring提供的JMS工具类,它可以进行消息发送、接收等 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<!-- 这个connectionFactory对应的是我们定义的Spring提供的那个ConnectionFactory对象 -->
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<!--这个是队列目的地,点对点的 文本信息-->
<bean id="queueSolrDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="pinyougou_queue_solr"/>
</bean>
<!--这个是队列目的地,点对点的 文本信息,删除操作-->
<bean id="queueSolrDeleteDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="pinyougou_queue_solr_delete"/>
</bean>
<!--这个是订阅模式 生成页面-->
<bean id="topicPageDestination" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="pinyougou_topic_page"/>
</bean>
<!--这个是订阅模式 删除页面-->
<bean id="topicPageDeleteDestination" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="pinyougou_topic_page_delete"/>
</bean>
- web.xml文件中引入该配置文件(contextConfigLocation)
- 代码实现,注入所用的对象服务(jmsTemplate、queueSolrDestination、queueSolrDeleteDestination)
/********导入到索引库**********/
// 得到需要的SKU列表
List<TbItem> itemList = goodsService.findItemListByGoodsIdAndStatus(ids, status);
// 导入到solr
// itemSearchService.importItemList(itemList);
final String jsonString = JSON.toJSONString(itemList); // 转换为json字符串
jmsTemplate.send(queueSolrDestination, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(jsonString);
}
});
/********生成静态页面**********/
/*for (final Long id : ids) {
itemPageService.genItemHtml(id);
}*/
jmsTemplate.send(topicPageDestination, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(ids);
}
});
消息消费者(搜索服务)
- 添加 activeMQ 依赖
- 添加spring配置文件 applicationContext-jms-consumer.xml
<!-- 真正可以产生Connection的ConnectionFactory,由对应的 JMS服务厂商提供-->
<bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.25.130:61616"/>
</bean>
<!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory -->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory -->
<property name="targetConnectionFactory" ref="targetConnectionFactory"/>
</bean>
<!--这个是队列目的地,导入到索引库-->
<bean id="queueSolrDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="pinyougou_queue_solr"/>
</bean>
<!-- 消息监听容器 -->
<bean class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="queueSolrDestination" />
<property name="messageListener" ref="itemSearchListener" />
</bean>
<!--这个是队列目的地,删除索引库-->
<bean id="queueSolrDeleteDestination" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="pinyougou_queue_solr_delete"/>
</bean>
<!-- 消息监听容器 -->
<bean class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory" />
<property name="destination" ref="queueSolrDeleteDestination" />
<property name="messageListener" ref="itemDeleteListener" />
</bean>
消息监听类:
@Component
public class ItemSearchListener implements MessageListener {
@Autowired
private ItemSearchService itemSearchService;
@Override
public void onMessage(Message message) {
TextMessage textMessage = (javax.jms.TextMessage) message;
try {
String text = textMessage.getText();
System.out.println("监听到消息:"+text);
List<TbItem> itemlist = JSON.parseArray(text,TbItem.class);
itemSearchService.importItemList(itemlist);
System.out.println("导入到solr索引库");
} catch (JMSException e) {
e.printStackTrace();
}
}
}
商品删除(移除solr索引库记录)类似。以及网页静态化,主要是消息模式为发布订阅模式。运营商执行商品审核后,向 activeMQ 发送消息(商品 ID集合),网页生成服务从 activeMQ 接收到消息后执行网页生成操作。
存在的问题
Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name 'dataSource' defined in URL [jar:file:/D:/Program%20Files/Maven/repository/com/pinyougou/pinyougou-dao/0.0.1-SNAPSHOT/pinyougou-dao-0.0.1-SNAPSHOT.jar!/spring/applicationContext-dao.xml]: Could not resolve placeholder 'jdbc.url' in string value "{jdbc.url}"; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'jdbc.url' in string value "{jdbc.url}"
提示找不到配置文件中的jdbc.url配置。是因为在page-service中,在生成静态页面时会用到一个页面生成路径的配置信息。然后在spring中的配置文件中设置<context:property-placeholder location="classpath:config/page.properties" />
。但是该服务依赖dao模块,这个模块中的数据库连接池的配置信息存放在 properties/db.properties 中,然后在 dao 模块中配置了 <context:property-placeholder location="classpath*:properties/*.properties" />
。此时 page-service 模块中的配置会覆盖该配置,就导致了无法访问 properties/db.properties 中数据库连接池的配置信息。解决方法就是使 <context:property-placeholder location="classpath*:*/*.properties" />
包含 dao 模块中的加载配置即可。
Spring Boot入门
Spring 为企业级 Java 开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的 Java 对象(Plain Old Java Object,POJO)实现了 EJB 的功能。
虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的。开始的基于XML配置,Spring2.5引入基于注解的组件扫描,3.0引入基于java的配置。主要是希望简化繁琐的配置。另外项目依赖管理也是一个难题,依赖的版本库会不会起冲突。
而Spring Boot解决了上述问题,它致力于帮助开发者更容易的创建基于 Spring 的应用程序和服务,让更多人的人更快的对 Spring 进行入门体验,为 Spring生态系统提供了一种固定的、约定优于配置风格的框架。
Spring Boot 具有的特性:
- 提供更快的入门体验
- 开箱即用,没有代码生成,也无需XML配置。也可以实现修改默认值。
- 提供大型项目中常见的非功能特性,如嵌入式服务器、安全、指标。
- 并不是Spring功能的增强,而是提供一种快速使用Spring的方式。
Spring Boot Demo
- 添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<dependencies>
<!-- web的启动器, 通过依赖传递引入web项目所需的jar包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 定义引导类 Application
/*
@SpringBootApplication 其实就是以下三个注解的总和
@Configuration: 用于定义一个配置类
@EnableAutoConfiguration :Spring Boot 会自动根据你 jar包的依赖来自动配置项目。
@ComponentScan: 告诉 Spring 哪个 packages 的用注解标识的类会被 spring 自动扫描并且装入 bean容器。*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Spring MVC 实现 hello world输出
@RestController
public class HelloWorldController {
@Autowired
private Environment env; // 用于获取 application.properties 配置中的属性
@RequestMapping("/info")
public String info() {
return "hello world. url:"+env.getProperty("url");
}
}
- 启动引导类即可, http://localhost:8080/info
常用配置:
- 端口号修改(只需要在 application.properties 文件中配置 server.port)
- 读取配置文件信息(注入 Environment 对象,使用getProperty方法)
- 热部署(pom文件中添加 spring-boot-devtools 依赖即可)
Spring Boot与ActiveMQ整合
- 使用内嵌服务 spring-boot-starter-activemq
- 创建消息生产者
@RestController
public class QueueController {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@RequestMapping("/send")
public void sendMessage(String text) {
jmsMessagingTemplate.convertAndSend("spring_boot_text", text);
}
}
- 创建消息消费者
@Component
public class Consumer {
@JmsListener(destination="spring_boot_text") // destination和消息生产者相同
public void readMessage(String text) {
System.out.println("接收到消息:"+text);
}
}
- 启动服务即可。http://localhost:8088/send.do?text=aaaaaa Spring Boot内置了ActiveMQ服务。
常用配置:
注:引入外部的ActiveMQ服务spring.activemq.broker-url=tcp://192.168.25.130:61616
短信解决方案
项目需求
构建一个通用的短信发送服务(独立于品优购的单独工程),接收 activeMQ 的消息(MAP类型) 消息包括手机号(mobile)、短信模板号(template_code)、签名(sign_name)、参数字符串(param )。该微服务通过短信验证码平台的API,实现验证码的发送功能。
验证码发送平台
由于阿里大于注册需要认证,比较繁琐,所以此处先不实现验证码发送模块。腾讯云的短信服务可以个人认证,但是需要域名备案,这个功能先预留,后期继续完成。
用户注册模块
工程搭建
- 用户服务接口层 user-interface
- 用户服务实现层 user-service
- 用户中心控制层 user-web
- 添加web.xml
- 引入依赖 user接口、spring依赖
- 添加 Spring 配置文件
- 静态原型页面
注册判断短信验证码
输入手机号,用户点击“获取验证码”,向后端传递手机号。后端随机生成六位数字作为验证码,同时将其保存在redis中(手机号作为key、验证码作为value), 同时向 ActiveMQ 发送消息。然后短信监听服务接受消息然后向验证码平台发送消息。
用户点击完成注册时,后端根据手机号查询用户输入的验证码与redis中的验证码是否匹配,如果匹配那么就执行注册,向数据库添加一条用户记录,否则提示不能完成注册。
服务层:
@Override
public void createSmsCode(String phone) {
// 1.生成六位随机码
String smsCode = (long)(Math.random()*1000000)+"";
System.out.println("验证码:"+smsCode);
// 2.将验证码存入redis
redisTemplate.boundHashOps("smscode").put(phone, smsCode);
// 3.发送相应的消息给ActiveMQ
// 待完成..... 将消息发送给ActiveMQ即可
}
@Override
public boolean checkSmsCode(String phone, String smsCode) {
// 获取redis中的验证码
String systemCode = (String) redisTemplate.boundHashOps("smscode").get(phone);
if (systemCode == null || !systemCode.equals(smsCode)) {
return false;
}
return true;
}
控制层:
/**
* 注册用户
* @param user
* @return
*/
@RequestMapping("/add")
public Result add(@RequestBody TbUser user,String smsCode){
// 用户注册前进行校验(用户输入的验证码和redis中的验证码进行比较)
if (!userService.checkSmsCode(user.getPhone(), smsCode)) {
return new Result(false, "验证码有误");
}
try {
userService.add(user);
return new Result(true, "注册成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "注册失败");
}
}
/**
* 生成验证码
* @param phone
*/
@RequestMapping("/createSmsCode")
public Result createSmsCode(String phone) {
if (PhoneFormatCheckUtils.isPhoneLegal(phone)) {
userService.createSmsCode(phone);
return new Result(true, "验证码发送成功");
}
return new Result(false, "验证码发送失败");
}
前端控制层:
//控制层
app.controller('userController' ,function($scope,$controller,userService){
// 注册
$scope.register=function(){
// 判断两次输入密码是否一致
if ($scope.entity.password!=$scope.password) {
alert("两次输入的密码不一致,请重新输入");
$scope.entity.password = "";
$scope.password = "";
return ;
}
// 新增
userService.add($scope.entity,$scope.smsCode).success(
function(response){
alert(response.message);
}
);
}
// 生成验证码
$scope.createSmsCode=function(){
userService.createSmsCode($scope.entity.phone).success(
function(response){
alert(response.message);
}
);
}
});
单点登录解决方案
单点登录(Single Sign On),是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。对于分布式的项目,多个子系统分别部署在不同的服务器中,此时采用传统的 session 来记录用户信息是无法实现的。
CAS
CAS 为 Web 应用系统提供一种可靠的单点登录方法。CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
访问流程:
- 访问服务:用户发送请求访问应用系统提供的服务资源(也是cas client)
- 定向认证:cas client 会**重定向(浏览器url会变化)**用户请求到 cas server
- 用户认证:和用户进行身份认证
- 发送票据:cas server生成一个ticket ,先给浏览器用户,然后浏览器将其带入到cas client端
- 验证票据:cas client 向 cas server 请求验证 ticket 的合法性
- 传输用户信息:验证通过,cas server 会将用户的信息传输给cas client
cas 服务端部署
cas 服务端就是一个 war 包,解压对应的压缩包,将cas-server-webapp-4.0.0.war放入tomcat的webapps下,启动tomcat完成解压。
常用配置修改
- 默认用户和密码为 casuser、Mellon。可以在 cas 的 WEB-INF->deployerConfigContext.xml
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
<entry key="admin" value="admin"/>
</map>
</property>
</bean>
- 端口号修改:修改tomcat的默认端口(conf/server.xml),然后 cas 的 WEB-INF/cas.properties 修改
server.name=http://localhost:9100
- 单点退出然后跳转到目标页面 cas 的 WEB-INF/cas-servlet.xml
<bean id="logoutAction" class="org.jasig.cas.web.flow.LogoutAction"
p:servicesManager-ref="servicesManager"
p:followServiceRedirects="${cas.logout.followServiceRedirects:true}"/>
- 去除https认证,cas 默认使用的是 https 协议,该协议需要申请 SSL 证书。一般在开发测试阶段可以使用http协议即可。
- 修改 cas 的 WEB-INF/deployerConfigContext.xml,增加 p:requireSecure="false"
<!-- Required for proxy ticket mechanism. -->
<bean id="proxyAuthenticationHandler" class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" p:requireSecure="false"/>
- 修改 cas 的/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="3600"
p:cookieName="CASTGC"
p:cookiePath="/cas" />
- 修改 cas 的 WEB-INF/spring-configuration/warnCookieGenerator.xml
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="3600"
p:cookieName="CASPRIVACY"
p:cookiePath="/cas" />
注:参数 p:cookieSecure="true",TRUE 为采用 HTTPS 验证,FALSE 为不采用 https 验证。参数 p:cookieMaxAge="-1",是 COOKIE 的最大生命周期,-1 为无生命周期,即只在当前打开的窗口有效,关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于 0 的数字,比如 3600 等,意思是在 3600 秒内,打开任意窗口,都不需要验证。
CAS 客户端Demo
创建 casclient_demo1 工程(war) 引入cas client依赖。
<dependencies>
<!-- cas -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<webResources>
<resource>
<directory>src/main/webapp/WEB-INF</directory>
<filtering>true</filtering>
<targetPath>WEB-INF</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<!-- 指定端口 -->
<port>9001</port>
<!-- 请求路径 -->
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
添加 web.xml 配置
<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://localhost:9100/cas/login</param-value>
<!--这里的 server 是服务端的 IP -->
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:9001</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责对 Ticket 的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://localhost:9100/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://localhost:9001</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器负责实现 HttpServletRequest 请求的包裹, 比如允许开发者通过 HttpServletRequest 的 getRemoteUser() 方法获得 SSO 登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得开发者可以通过 org.jasig.cas.client.util.AssertionHolder 来获取用户的登录名。 比如 AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
主页面 index.jsp <%=request.getRemoteUser()%>
获取远程登录用户名
然后再创建客户端工程2。启动cas服务端和cas客户端,然后http://localhost:9001
和9002端口,都会跳转到cas的登录页面。实现单点登录。单点退出只需访问 http://localhost:9100/cas/logout
即可。
CAS 服务端数据源设置
使用项目中 user 表中的用户信息来实现登录验证。
- 修改 cas 服务端的 WEB-INF/deployerConfigContext.xml
<!-- 数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/pinyougoudb?characterEncoding=utf8"
p:user="root"
p:password="123456" />
<!-- 默认密码解码方式 -->
<bean id="passwordEncoder"
class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder"
c:encodingAlgorithm="MD5"
p:characterEncoding="UTF-8" />
<bean id="dbAuthHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource"
p:sql="select password from tb_user where username = ?"
p:passwordEncoder-ref="passwordEncoder"/>
<!----------------------另外配置认证管理器------------------->
<constructor-arg>
<map>
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<!-- 默认的认证处理方式 <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" /> -->
<entry key-ref="dbAuthHandler" value-ref="primaryPrincipalResolver" />
</map>
</constructor-arg>
- 配置了数据库连接池相关信息,那么就需要把数据库相应的jar包引入
CAS 服务端界面改造
cas server 服务端提供了默认的登录界面,那我们如何修改为我们自己需要的登录页面了。步骤如下:
- 将 login.html 拷贝到 cas 系统下的 WEB-INF\view\jsp\default\ui 目录下
- 将 css、js、img 等静态资源文件夹拷贝到 cas 目录下。web 工程的根目录
- 将原来的 casLoginView.jsp 改名(以做参照模板),将 login.html 改名为 casLoginView.jsp
- 添加 jsp 指令
- 修改 form 标签,保留原页面样式
- 修改用户名输入框,保留原页面样式
- 修改密码框,保留源页面样式
- 修改登录按钮,保留原页面的样式
- 错误提示
<form:errors path="*" id="msg" cssClass="errors" element="div" htmlEscape="false" />
注:错误提示信息默认为英文,使用了国际化标准。在 cas 的 WEB-INF\classes 中的 messages_zh_CN.properties 文件中添加配置。
authenticationFailure.AccountNotFoundException=用户名或密码错误
authenticationFailure.FailedLoginException=用户名或密码错误
第一个是用户名不存在时的错误提示 第二个是密码错误的提示
修改 cas-servlet.xml,设置国际化为 zn_CN(默认为 en ) 。
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"
p:defaultLocale="zh_CN" />
国际化:i18n。英文为:internationalization 。18表示中间的字符数。
用户中心实现单点登录(cas client与Spring Security集成)
- 引入 springSecurity、cas 客户端和 springSecurity Cas 整合包依赖
<!-- spring-security配置 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<!-- spring-security-cas -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
- web.xml 添加 spring-security 过滤器,设置首页
<welcome-file-list>
<welcome-file>home-index.html</welcome-file>
</welcome-file-list>
<!-- 省略post乱码过滤器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-security.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 构建 UserDetailsServiceImpl 认证类,实现UserDetailsService接口
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("经过认证类:"+username);
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 角色固定了,如果存在多种角色的话,那么此处可能会去数据库中查找来实现动态设置用户角色
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(username, "", authorities);
}
}
- 添加 spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 匿名访问资源 -->
<http pattern="/css/**" security="none"></http>
<http pattern="/js/**" security="none"></http>
<http pattern="/img/**" security="none"></http>
<http pattern="/plugins/**" security="none"></http>
<!-- 注册登陆 -->
<http pattern="/register.html" security="none"></http>
<http pattern="/user/add.do" security="none"></http>
<http pattern="/user/createSmsCode.do" security="none"></http>
<!-- entry-point-ref 入口点引用 -->
<http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint">
<intercept-url pattern="/**" access="ROLE_USER"/>
<csrf disabled="true"/>
<!-- custom-filter为过滤器, position 表示将过滤器放在指定的位置上,before表示放在指定位置之前 ,after表示放在指定的位置之后 -->
<custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" />
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</http>
<!-- CAS入口点 开始 -->
<beans:bean id="casProcessingFilterEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<!-- 单点登录服务器登录URL -->
<beans:property name="loginUrl" value="http://localhost:9100/cas/login"/>
<beans:property name="serviceProperties" ref="serviceProperties"/>
</beans:bean>
<beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<!--service 配置自身工程的根地址+/login/cas -->
<beans:property name="service" value="http://localhost:9106/login/cas"/>
</beans:bean>
<!-- CAS入口点 结束 -->
<!-- 认证过滤器 开始 -->
<beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<beans:property name="authenticationManager" ref="authenticationManager"/>
</beans:bean>
<!-- 认证管理器 -->
<authentication-manager alias="authenticationManager">
<authentication-provider ref="casAuthenticationProvider">
</authentication-provider>
</authentication-manager>
<!-- 认证提供者 -->
<beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<beans:property name="authenticationUserDetailsService">
<beans:bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<beans:constructor-arg ref="userDetailsService" />
</beans:bean>
</beans:property>
<beans:property name="serviceProperties" ref="serviceProperties"/>
<!-- ticketValidator 为票据验证器 -->
<beans:property name="ticketValidator">
<beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<beans:constructor-arg index="0" value="http://localhost:9100/cas"/>
</beans:bean>
</beans:property>
<beans:property name="key" value="an_id_for_this_auth_provider_only"/>
</beans:bean>
<!-- 认证类 -->
<beans:bean id="userDetailsService" class="com.pinyougou.user.service.UserDetailServiceImpl"/>
<!-- 认证过滤器 结束 -->
<!-- 单点登出 开始 -->
<beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
<beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<beans:constructor-arg value="http://localhost:9100/cas/logout?service=http://localhost:9103"/> <!-- 退出登陆并跳转到首页 -->
<beans:constructor-arg>
<beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</beans:constructor-arg>
<beans:property name="filterProcessesUrl" value="/logout/cas"/>
<!-- 此时直接请求 logout/cas 即可实现单点退出,相当于上面链接的一个别名 -->
</beans:bean>
<!-- 单点登出 结束 -->
</beans:beans>
获取当前登录用户名,借助Spring Security的方法。SecurityContextHolder.getContext().getAuthentication().getName();
即可得到用户名信息。