项目第七天
ES类型
要答出 type版本前和版本后的改变
ES查询方法
使用ElasticsearchRestTemplate对象来构建
ES的默认规则❤️
- ES能够自动存储未提交创建字段信息的数据 (目的:未指定时ES为了可以更好的支持聚合和查询功能,所以默认创建了两种)
- 对于为提前指定类型的字段,使用以下默认规则
-使用: [字段](text) #分词不聚合
-使用: [字段].keyword(keyword) #聚合不分词
ES聚合 指标聚合 类似于 sum max avg等聚合方法
桶聚合 selectedOne等方法
ES九大查询规则
mathch 模糊查询
term 精确查询
WildcardQuery 通配符查询
regexpQuery 正则表达式查询
prefixQuery 前缀查询
rangeQuery 范围查询
QueryString 按指定字段对关键字先分词再判断是否指定并集
SimpleQueryString 按指定字段对关键字不分词查询
mathchAll 全部查询(不算)
ES索引库创建
//使用前要自动注入elasticsearchRestTemplate对象
public void createMappingAndIndex() {
//创建索引
elasticsearchTemplate.createIndex(SkuInfo.class);
//创建映射
elasticsearchTemplate.putMapping(SkuInfo.class);
}
初始化
此处使用布尔查询
//构建native查询对象
NativeSearchQueryBuilder nativeBuilder = new NativeSearchQueryBuilder();
//创建bool查询对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //构建布尔查询
1.条件筛选
此处有坑。不同版本,可能在使用@Document注解来提前创建索引库时,分词效果不生效!!
手动创建索引库、或使用 @Mapping(mappingPath=“xxx.json”) 在resource下定义conf文件的json字符串创建
Match:先分词,再查询
Term:不分词,精确匹配
//条件筛选,非空判断
//模糊查找,对传入的关键字进行判断
if(StringUtils.isNotEmpty(keyword)){
MatchQueryBuilder val = QueryBuilders.matchQuery("name", keyword);//使用match查询,指定条件
boolQuery.must(val.operator(Operator.AND)); //封装布尔条件
}
//精确查找
TermQueryBuilder val1 = QueryBuilders.termQuery("brandName", brandName);//使用term查询,指定条件
2.规格过滤
//规格过滤
for(String key : strings){
if(key.startsWith("spec_")){
System.out.println(key);
//get请求必须要使用此方式转义 ,但是现在的浏览器已经自动进行转换。故可以不需要
String val = searchMap.get(key).replace("%2B%","+");
//树形结构是 specMap--属性名(key)--关键字keyword
//key.substring(5) 是去除 spec_
MatchQueryBuilder match = QueryBuilders.matchQuery("specMap." + key.substring(5) + ".keyword", val);
//MatchQueryBuilder match = QueryBuilders.matchQuery("specMap." + key.substring(5), key);
boolQuery.filter(match.operator(Operator.AND));
}
}
3.价格区间
//按照价格进行区间过滤查询
if (StringUtils.isNotEmpty(searchMap.get("price"))){
//获取price,并且分割
String[] prices = searchMap.get("price").split("-");
// 0-500 500-1000 3000
//判断是否是区间数
if (prices.length == 2){
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));
}
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));
}
4.封装条件
此处开始,后续所有功能都是在封装之后进行再封装,是与nativeBuilder同级。
//封装布尔的查询条件
nativeBuilder.withQuery(boolQuery);
聚合查询
//设置聚合 按照品牌分组,指定聚合结果的别名,指定聚合结果,执行查询的最大条数(可选)
TermsAggregationBuilder bName = AggregationBuilders.terms("bName").field("brandName.keyword").size(100);;
//存入条件
nativeBuilder.addAggregation(bName);
//===============经过esTemplate.search查询后!!!!=======================
//===============经过esTemplate.search查询后!!!!=======================
//===============经过esTemplate.search查询后!!!!=======================
//通过聚合别名取出查询出的聚合结果
ParsedStringTerms brandTerms = search.getAggregations().get("bName").size(100);
//流转换为集合
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//brandList中就是聚合后的结果了!
分页查询
//分页查询 起始页数从0开始
//nativeBuilder.withPageable()
nativeBuilder.withPageable(PageRequest.of( (int)pageCount-1,(int)pageSize));
商品排序
//商品排序
//按照相关字段进行排序查询
// 1.当前域 2.当前的排序操作(升序ASC,降序DESC)
//判断两个条件是否都满足
if (StringUtils.isNotEmpty(searchMap.get("sortField")) && StringUtils.isNotEmpty(searchMap.get("sortRule"))){
if ("ASC".equals(searchMap.get("sortRule"))){
//升序
//调用排序方法,指定哪个字段来排序,指定高低排序
nativeBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.ASC));
}else{
//降序
nativeBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.DESC));
}
}
高亮显示
//设置高亮域以及高亮的样式
HighlightBuilder.Field field1 = new HighlightBuilder.Field("name")//指定要高亮的域
.preTags("<span style='color:red'>")//高亮样式的前缀
.postTags("</span>");//高亮样式的后缀
nativeBuilder.withHighlightFields(field1);
//===============经过esTemplate.search查询后!!!!=======================
//===============经过esTemplate.search查询后!!!!=======================
//===============经过esTemplate.search查询后!!!!=======================
//替换高亮数据
//获取高亮域的集合
//此处的hit 是esTemplate.search后的集合中的一个对象
SkuInfo content =(SkuInfo) hit.getContent();
Map<String, List<String>> highlight = hit.getHighlightFields();
//判断集合中是否有内容
if (highlight != null && highlight.size() > 0){
//有内容,则替换数据,如果有,那么每一条数据name字段都会有对应的高亮标签
//相对于之前api改变的地方,获取的高亮集合的map的value泛型为List<String>,这个集合中的元素直接就是高亮域的值
content.setName(highlight.get("name").get(0));
}
编译查询结果
//开始查询,传入查询对象实例化、封装实体类
SearchHits<SkuInfo> search = esTemplate.search(nativeBuilder.build(),SkuInfo.class);
创建索引库
GET skuinfo/_search # 查询所有数据
GET skuinfo/_mapping # 查询字段创建信息
DELETE skuinfo #删除所有数据
#创建
PUT skuinfo
{
"mappings": {
"properties": {
"brandName" : {
"type" : "text",
"fields" : {
"keyword" : {
PUT skuinfo
{
"mappings": {
"properties": {
"brandName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"categoryId" : {
"type" : "long"
},
"categoryName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"createTime" : {
"type" : "long"
},
"id" : {
"type" : "long"
},
"image" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"num" : {
"type" : "long"
},
"price" : {
"type" : "long"
},
"spec" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"specMap" : {
"properties" : {
"产品型号" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"克重" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"口味" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"容量" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"尺码" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"手机屏幕尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"机身内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"款式" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"版本" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"电视屏幕尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"网络制式" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"规格" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"选择内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"选择版本" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"选择配置" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"颜色" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
}
}
},
"spuId" : {
"type" : "long"
},
"status" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"updateTime" : {
"type" : "long"
}
}
}
} "type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"categoryId" : {
"type" : "long"
},
"categoryName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"createTime" : {
"type" : "long"
},
"id" : {
"type" : "long"
},
"image" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"num" : {
"type" : "long"
},
"price" : {
"type" : "long"
},
"spec" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart",
"fielddata": true
},
"specMap" : {
"properties" : {
"产品型号" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"克重" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_max_word"
},
"内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_max_word"
},
"口味" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"容量" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_max_word"
},
"尺码" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"手机屏幕尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"机身内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"款式" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"版本" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"电视屏幕尺寸" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"网络制式" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"规格" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"选择内存" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"选择版本" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"选择配置" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"颜色" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
}
}
},
"spuId" : {
"type" : "long"
},
"status" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
},
"analyzer": "ik_smart"
},
"updateTime" : {
"type" : "long"
}
}
}
}
完整代码❤️
@Service
public class SearchServiceImlp implements SearchService {
@Autowired
private ElasticsearchRestTemplate esTemplate; //自动注入
@Override
public Map searchByConditions(Map<String, String> searchMap) throws Exception {
//根据搜索条件关键字查询
double pageSize=10.0;
double pageCount=1.0;
//定义默认页面显示条数
if(searchMap.get("pageSize")!=null){
pageSize=Double.parseDouble(searchMap.get("pageSize"));
}
//定义默认页数
if(searchMap.get("pageCount")!=null){
pageCount=Double.parseDouble(searchMap.get("pageCount"));
}
//构建统一返回的map集合
HashMap<String, Object> result = new HashMap<>();
//构建native关键对象
NativeSearchQueryBuilder nativeBuilder = new NativeSearchQueryBuilder();
//创建bool查询对象,用于封装查询条件进入native
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //选择布尔查询
//=============================================================
//===============条件准备就绪开始组织查询条件===================
//=============================================================
//判断不为空值,则按照条件对应查询。为空直接返回null
if(searchMap!=null && searchMap.size()!=0){
//获取map中的keyword的值
String keyword = searchMap.get("keyword"); //关键字查询
//条件筛选
//非空判断
if(StringUtils.isNotEmpty(keyword)){
MatchQueryBuilder val = QueryBuilders.matchQuery("name", keyword);//使用match查询,指定条件
boolQuery.must(val.operator(Operator.AND)); //封装布尔条件
}
//获取map中的brand的值
String brandName = searchMap.get("brand");
//条件筛选
//非空判断
if(StringUtils.isNotEmpty(brandName)){
TermQueryBuilder val1 = QueryBuilders.termQuery("brandName", brandName);//使用term完全查询,指定条件
boolQuery.filter(val1); //封装布尔条件
}
//获取集合中所有key
Set<String> strings = searchMap.keySet(); //map有两种方式获取值,此处使用的keySet
//规格过滤,判断是否选择规格
for(String key : strings){
//判断是否有以spec_开始的值。
//只要有spec开头的 都当作条件放入布尔查询 形成 mysql中and的查询结构
if(key.startsWith("spec_")){
//get请求必须要使用此方式转义,此方式已经过时。目前的浏览器已经做好了转码
//String val = searchMap.get(key).replace("%2B%","+");
//树形结构是 specMap--属性名(key)--关键字keyword
//MatchQueryBuilder match = QueryBuilders.matchQuery("specMap." + key.substring(5) + ".keyword", val);
MatchQueryBuilder match = QueryBuilders.matchQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key));
//指定分词后以并集 来查询
boolQuery.filter(match.operator(Operator.AND));
}
}
//按照价格进行区间过滤查询
if (StringUtils.isNotEmpty(searchMap.get("price"))){
//获取price,并且分割
String[] prices = searchMap.get("price").split("-");
// 0-500 500-1000 3000
//判断是否是区间数
if (prices.length != 2){
//使用范围查询
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));
}
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));
}
//封装查询条件
nativeBuilder.withQuery(boolQuery);
//=================================================================
//===============封装基本查询条件就绪,开始大封装===================
//=================================================================
//设置聚合 按照品牌分组,指定聚合结果的字段名字
TermsAggregationBuilder bName = AggregationBuilders.terms("bName").field("brandName.keyword").size(3);
//设置聚合 按照规格分组,指定聚合结果的字段名字
TermsAggregationBuilder skuSpec = AggregationBuilders.terms("skuSpec").field("spec.keyword");
//存入聚合查询的条件
nativeBuilder.addAggregation(bName);
nativeBuilder.addAggregation(skuSpec);
//设置分页 起始页数从0开始
nativeBuilder.withPageable(PageRequest.of( (int)pageCount-1,(int)pageSize));
//商品排序
//按照相关字段进行排序查询
// 1.当前域 2.当前的排序操作(升序ASC,降序DESC)
//判断两个条件是否都满足
if (StringUtils.isNotEmpty(searchMap.get("sortField")) && StringUtils.isNotEmpty(searchMap.get("sortRule"))){
//设置排序
if ("ASC".equals(searchMap.get("sortRule"))){
//升序
//调用排序方法,指定哪个字段来排序,指定高低排序
nativeBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.ASC));
}else{
//降序
nativeBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).order(SortOrder.DESC));
}
}
//设置高亮域以及高亮的样式
HighlightBuilder.Field field1 = new HighlightBuilder.Field("name")//指定要高亮的域
.preTags("<span style='color:red'>")//高亮样式的前缀
.postTags("</span>");//高亮样式的后缀
//设置高亮
nativeBuilder.withHighlightFields(field1);
//=============================================
//===============正式开始查询==================
//=============================================
//正式开始查询,传入查询对象实例化、封装实体类
SearchHits<SkuInfo> search = esTemplate.search(nativeBuilder.build(),SkuInfo.class);
//=============================================
//===============查询完成,整理结果==================
//=============================================
//定义集合。存储数据
ArrayList<SkuInfo> skuInfoList = new ArrayList<>();
//通过聚合名取出查询出的聚合结果
//Aggregation brandTerms = search.getAggregations().get("bName"); 坑!
ParsedStringTerms brandTerms = search.getAggregations().get("bName");
ParsedStringTerms specTerms = search.getAggregations().get("skuSpec");
//流转换为集合
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
//遍历查询结果
for(SearchHit hit : search){
//获取每一条skuinfo对象
SkuInfo content =(SkuInfo) hit.getContent();
// Map highlightFields = hit.getHighlightFields(); 坑! 获取结果不一致
// Map<String, List<String>> a = hit.getHighlightFields(); 正确结果
//替换高亮数据
//获取高亮域的集合
Map<String, List<String>> highlight = hit.getHighlightFields();
//判断集合中是否有内容
if (highlight != null && highlight.size() > 0){
//有内容,则替换数据,如果有,那么每一条数据name字段都会有对应的高亮标签
//相对于之前api改变的地方,获取的高亮集合的map的value泛型为List<String>,这个集合中的元素直接就是高亮域的值
content.setName(highlight.get("name").get(0));
}
//将已经重新赋值的数据内容存入集合
skuInfoList.add(content);
}
long total = search.getTotalHits(); //获取所有查询出来的总数
double page = (double)total/pageSize; //由于没有封装页数,故计算页数
double i = Math.ceil(page); //计算结果向上取整
//封装总页数
result.put("total",(int)i);
//封装总条数
result.put("totalPages",total);
//封装数据
result.put("rows",skuInfoList);
//封装品牌
result.put("brandList",brandList);
//封装规格
result.put("specList",specList);
return result;
}
//为空返回null
return null;
}
}
项目第八天
Thymeleaf
介绍
定义:是一个Java后端的一套静态化模板引擎 。
作用: 服务端访问压力降低 客户端体验度提高 开箱即用
默认规则
# 模板文件必须放在该目录下才生效
resource下创建templates文件夹(必须以此名)
# 作为MVC的响应页面
static、public、resources、/META-INF/resources/是他默认的静态资源存放目录(可以理解为根目录)
# templates是默认的模板存储目录。规定themyleaf的属性应用别名
<html xmlns:th="http://www.thymeleaf.org"> 网页中需要配置的头信息,不配置,不生效!
# 是一个页面动态技术 所以每次更新页面信息都要重启
# yml文件中配置是否开启缓存。生产时需开启,调试时建议关闭。
spring:
thymeleaf:
cache: false
使用
使用场景与为什么使用
生成合同、邮件、详情页等
1.网络请求少,不改变、访问量大的网页
2.可以直接访问
3.减小服务器压力
1.导包
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.编码
为了满足用户高体验,在特定情况下才使用get请求方式。因为post请求会出现对话框提示,影响体验。但是大环境依然是要使用post请求的
生成theymeleaf模板❤️
1.两种方式 Model对象 调用方法
2.使用上下文 存储 然后模板引擎来调用 生成模板对象
// 设置的数据放在哪里,才可以被模板锁访问?
@Value("${pagepath}")
private String pagepath; //yml文件定义变量,注入
@Autowired
private TemplateEngine templateEngine; //注入模板引擎
public void generateHTML(String pid) {
//新建上下文对象
Context context = new Context();
//调用方法获取map,获取到要在模板中使用的数据
Map<String, Object> map = getData(pid); //页面需要什么就在里面封装什么。此处是调用getData自定义方法统一获取
//把数据设置给上下文
context.setVariables(map);
//新建file对象确定路径
File path = new File(pagepath); //模板生成路径
//判断生成位置是否存在文件夹
if(!path.exists()){
path.mkdir(); // 不存在就生成一个目录
}
//新建file对象生成html静态文件。以唯一id作用名避免重复
File objPath = new File(path +"/"+ ((Spu) map.get("spu")).getId() + ".html");
//定义输出流
Writer writer = null;
try {
//创建输出对象
writer = new FileWriter(objPath);
//生成html
templateEngine.process("templates文件下模板的名字",context,writer);
} catch (IOException e) {
e.printStackTrace();
}finally {
//非空判断
if(writer!=null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
直接使用
可以直接使用其语法格式 在html中进行定义 来获取对应的值进行渲染
//跳转到自定义登陆页面
@RequestMapping("/toLogin")
//此处定义变量from的获取 是为了在集成oauth2时的访问未授权页面跳转首页登录后 再自动跳转到上一页面
// 1. 在网关中进行拦截 会被默认跳转到首页
// 2. 跳转到一个接口登录接口 该地址使用?FROM=当前访问地址 来拼接
// 3. 下述就是该接口,获取到FROM后就在model中存储 携带到html中去渲染
// 4. 首页html 定义一个变量存储这个from, 在登陆后的成功回调中 就直接跳转到该from变量即可实现返回上一页面
public String toLogin(@RequestParam(value = "FROM",required = false,defaultValue = "") String from, Model model){
model.addAttribute("from",from); //在model添加key val 来渲染页面
return "login";
}
常用表达式
- $:是变量表达式
- @:URI表达式,@{${url}(sortRule='ASC',sortField='price')
- #:工具类的引用表达式
- dates th:text="${#dates.format(now,'yyyy-MM-dd hh:ss:mm')}"
- maps 用于hash对象判断,用法和HashMap类似
- strings 用于字符串的判断,用法类似于String
- numbers 用于数字的处理 #配合循环使用可以遍历出 指定循环数字 2.3.4.5...
常用属性标签
- th:text:插入一个值到html元素内
- th:type:设置元素类型
- th:name:设置元素名称
- th:utext: 带标签渲染
- th:inline 追加样式
#===========================普通文本================================
<input th:type="submit" th:text="${data.price}" th:name="wz"></i>
- th:each:循环集合
- 元素引用对象user
- 上下文引用对象userStat
- •index:当前迭代对象的迭代索引,从0开始,这是索引属性
•count:当前迭代对象的迭代索引,从1开始,这个是统计属性
•size:迭代变量元素的总量,这是被迭代对象的大小属性
•current:当前迭代变量元素
•even/odd:布尔值,当前循环是否是偶数/奇数(从0开始计算)
•first:布尔值,当前循环是否是第一个
•last:布尔值,当前循环是否是最后一个
#===========================循环================================
<tr th:each="user,userStat:${users}"> # user是每一个元素 userStat是集合的别名
<td>
下标:<span th:text="${userStat.index}"></span>,
</td>
<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.address}"></td>
</tr>
#========================条件判断===================================
1.th:if 条件判断
th:if="${(age>=18)}"
2. th:unless 条件判断,是if的取反
<div class="type-wrap" th:unless="${#maps.containsKey(searchMap,'price')}">
#========================引入外部页面===================================
- th:fragment:定义模块
<div id="C" th:fragment="copy" > # 该引用标签的名字是copy
关于我们<br/>
</div>
- th:include:导入模块
th:include="footer::copy" # 使用该格式来引用copy
- th:utext:解析html并插入到元素内
#===========================跳转+网页拼接====================
<li>
<!--拼接条件会默认在get请求网址后面加上& 多个条件用,隔开 -->
<a th:href="@{${url}(sortField='price',sortRule='ASC')}">价格↑</a>
</li>
#===========================================================
静态化页面生成的思路
模板生成后,需要发布到Openrestry的nginx中,因为项目最终的访问地址是在nginx上被用户访问。这一流程是自动化生成的此处为展示。
能否搭建springCloud
docker安装
商品数据 表设计
ES的查询和创建索引库
页面静态化技术生成 freemarker、thymeleaf
swagger
项目第九天
单点登录
一处登录处处登录,在微服务架构中由于各个模块之间是独立存在的,为了提高用户的体验度,不需要在每个微服务中都进行登录所以需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。
java中有很多技术都可以实现单点登录 如:Apache Shiro. 2、CAS 3、Spring security
三方账户登录
第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如 Facebook,Twitter等,国内的比如微博、微信、QQ等。
JWT
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
1.Header 头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
2.第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
3.第三部分是签名,此部分用于防止jwt内容被篡改。
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
基于私钥生成jwt
//基于私钥生成jwt
//1. 创建一个秘钥工厂
//1: 指定私钥的位置
ClassPathResource classPathResource = new ClassPathResource("changgou.jks");
//2: 指定秘钥库的密码
String keyPass = "changgou";
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray());
//2. 基于工厂获取私钥
String alias = "changgou";
String password = "changgou";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
System.out.println("=============="+ Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
//将当前的私钥转换为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
//3.生成jwt
Map<String,String> map = new HashMap();
map.put("company","heima");
map.put("address","beijing");
Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));
String jwtEncoded = jwt.getEncoded();
System.out.println(jwtEncoded);
解析
Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey)); //解析和验证
String claims = token.getClaims();
System.out.println(claims);
公私钥的生成
JWT令牌生成采用非对称加密算法
生成私钥
生成时需要准备一个文件夹,在对应的文件夹下 使用cmd进入黑窗口
执行下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
#注意: 这里生成的是密钥证书!
执行完成后会让输入一系列的生成条件 如 账户密码信息等
# Keytool 是一个java提供的证书管理工具
-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,changgou.jks保存了生成的证书
-storepass:密钥库的访问密码
查询证书信息
keytool -list -keystore changgou.jks
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html
安装完成后配置openssl的path环境变量
cmd进入changgou.jks文件所在目录执行如下命令,可以获取到公钥:
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
-----BEGIN PUBLIC KEY----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/ iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv 9QIDAQAB
-----END PUBLIC KEY----
# 最终得到如上效果,该效果要整理为一行进行存储
# 将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
加密测试
public class CreateJWTTest {
@Test
public void createJWT(){
//基于私钥生成jwt
//1. 创建一个秘钥工厂
//1: 指定私钥的位置
ClassPathResource classPathResource = new ClassPathResource("changgou.jks");
//2: 指定秘钥库的密码
String keyPass = "changgou"; //此处的密码是什么密码
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray());
//2. 基于工厂获取私钥
String alias = "changgou"; //这里的两个数据是不是配置文件中的值 什么作用
String password = "changgou";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
//将当前的私钥转换为rsa私钥
//为什么需要转为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
//3.生成jwt
Map<String,String> map = new HashMap();
map.put("company","heima");
map.put("address","beijing");
Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));
String jwtEncoded = jwt.getEncoded(); //转换jwt格式 编码为字符串
System.out.println(jwtEncoded);
}
}
解密测试
public class ParseJwtTest {
@Test
public void parseJwt(){
//基于公钥去解析jwt
String jwt ="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiYmVpamluZyIsImNvbXBhbnkiOiJoZWltYSJ9.dj7yzURaPipxpo0B0ErOtYcJedBRu-ivGttSnYooxGLgyW4x5f0EoiUfJfO04XvLusEnZKQ1EU5u4CR2boxoC-eCRKruQ9I6vmDHPFNRO4S53OQiyE6JSm_G-rqemNZvAoTs9TgZno1-LiIVwVNlKKjejSm3H9GiX4N-REb9cwsSnaZyyUSRYAN4Wo4gL0cA4paFM9eC6Dw6Xg5ACH9WrpUSMRR5m4wRzjQglDeF20Yma7c2kLI7HXEyFgfMhMgb0rVjELlX9d8nK9m7_CeuViKAc1DrRx5NmfCvvvp2uZMtzX-69sLkqIM6gu2USqaAepFfmAFc-1pxwEdu-lzf2A";
String publicKey ="----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAieiStUexKt/XzXX7Of5TGB5XgrRbLweJDb5q57znepsPoh2oZq4kOnH3kvgPsYKXH3Ia3xwy0k9oiYXT1+1eV/36+KkqJmYkr4yRpJVVOezwp6JBZhl0eCdezaaRDh4jX+fJD3b6b8liACnqVV0wvPC9Io93rfUTMonxCD0T/5K33cgkstM+hSaAbiLyZUEAOadBmaAEBZBsMJG74Ejs8LjVHEzA9CeZHuRA6JIOCQQ+G1/wQHy+BShYpb7zCzv0ZkihGsnGEdPO+oLUe8gDtGsZqxAxsg/Be16sq1LulRErbOmyOP8SE0POTEVYLRawEw+aokEQhiXLdO5qTiEX4QIDAQAB-----END PUBLIC KEY-----";
//解析并且验证
Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));
//获取
String claims = token.getClaims();
System.out.println(claims);
}
}
oauth2认证
1.什么oauth2
是一个用户系统之间进行用户授权认证的协议
因为要实现跨系统认证,各系统之间要遵循一定的接口协议。OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
2.四种角色
- 组成:
- 资源服务:是校验access_token的合法性,并提供资源接口
- 授权服务:是生成授权码、Access_token等信息的服务器
- 客户端:获取受限资源的请求方
- 用户
3.数据库表
- oauth_client_details:# 客户端详情信息
- oauth_access_token:# 给客户端生成的令牌信息存储表
- oauth_client_token:# 客户端授权模式时,在服务器存储的客户端生成的令牌信息
- oauth_code: # 给客户端生成的授权存储信息表(授权码只能被使用一次)
- oauth_refresh_token:# 刷新令牌的存储信息
4.四种模式
1.授权码模式(Authorization Code) # 常用 一般用于公司之间的信息交互 如:微信、qq进行交互
2.隐式授权模式(Implicit)
3.密码模式(Resource Owner Password Credentials) #会用 一般用户内部系统之间,进行授权访问
4.客户端模式(Client Credentials)
授权码模式
# 一般用于公司之间的信息交互
都在postman中进行测试
密码模式
一般用户内部系统之间,进行授权访问
# 密码模式与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。
Post请求:
http://localhost:9200/oauth/token
【header】 # oauth数据库表的密码账户
clinet_id:客户端名
client_secret:客户端密码
【body】 # 配置类型 账户名、密码
携带参数:
grant_type:密码模式授权填写password
username:账号
password:密码
携带 http Basic认证 账户密码是生成的密钥证书的密码账户并且在表中要有一份
5.怎么搭建两个服务器
每个类的具体操作在下面的搭建与使用中有详细代码
授权服务器
1. 导包
由于oauth2没有对security进行封装 所以需要导入security依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
2.在oauth2的核心配置类上定义注解 将本模块定义为授权服务 @EnableAuthorizationServer
写oauth2的核心配置类继承AuthorizationServerConfigurerAdapter
在resources下放入 公钥私钥
//数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
@Autowired
private DataSource dataSource;
//jwt令牌转换器
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//SpringSecurity security提供的用户自定义授权认证类
@Autowired
UserDetailsService userDetailsService;
//授权认证管理器
@Autowired
AuthenticationManager authenticationManager;
//令牌持久化存储接口
@Autowired
TokenStore tokenStore;
3. //在此类中最终生成 转码后的jwt令牌(1.头:存放算法和转码方式 2.载荷:存放用户信息以授权等 3.签名:存放解析)
定义实现类UserDetailsService
定义实现类WebSecurityConfigurerAdapter // 指定整个oauth体系下的放行路径
定义实现类DefaultUserAuthenticationConverter //对调用UserDetailsService 来返回响应对象给restTemplate.exchange调用方
4. 定义业务层授权方法和注入RestTemplate到IOC中 //使用此类开启认证,要先设置对应的oauth必要参数
指定 http://ip:端口/oauth/token
指定 请求体的账户、密码 、oauth模式 指定 请求头 oauth2详细表中的账户、密码 进行 basic认证格式转码
开始oauth认证 ResponseEntity<Map> token = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
// 经过定义的四个实现类 最终得到结果 该结果中包含access_token、refresh_token、jti
资源服务器
1.导包 由于是资源服务 所以只需要导入oauth2依赖即可
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
2.定义核心配置文件 继承ResourceServerConfigurerAdapter
3.将本模块的核心配置文件定义为资源服务 @EnableResourceServer
4.启动权限认证@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
6.前后端两种网关的区别
前端网关 拦截公网用户的请求 由于要提供系统安全性 所以使用oauth2来进行鉴权
后端网关 用于后台系统的整体操作 由于使用人数少且是后台内部人员, 故仅使用jjwt来生成获取令牌 作为鉴权依据
7.请求令牌需要哪些网址
# 授权码模式 进入到登录网页:
http://地址:端口/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
# 请求令牌 授权模式和密码模式都要通过此网址进行获取令牌
# 请求的时候要指定模式 携带oauth_client_details客户端表的账户密码 携带登录用户的账户密码
http://ip:端口/oauth/token
8.代码访问流程
1.请求到达网关判断权限
2.登录相关页面放行 (携带账户名和密码) 在oauth2模块中生成令牌 存入redis 或 账户密码错误
3.如果不是相关页面 则从请求头取出权限信息头 与redis做对比
4.存在则放行 让后续对应服务器来做请求头校验是否与redis数据值一致
三方授权码模式流程图❤️
整体访问流程图❤️
令牌获取方式
1.登录权限页面
Get请求: #进入该网址是一个oauth2提供的登录网页,输入的账户密码要与默认表oauth_client_details中的账户密码匹配才可以登录
http://localhost:9200/oauth/authorizeclient_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
2.获取授权码
# 账户密码正确后可以进入到授权页面,点击授权即可获得授权码
点击Authorize,接下来返回授权码:认证服务携带授权码跳转redirect_uri,code=k45iLY就是返回的授权码, 每一个授权码只能使用一次
3.申请令牌
# 拿到授权码后,申请令牌。
Post请求:
http://ip:端口/oauth/token
【header】
clinet_id:客户端名
client_secret:客户端密码
【body】
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
响应值:
- access_token:长令牌
- refresh_token:刷新礼品
- jti:短令牌
4.刷新令牌
Post:http://localhost:9200/oauth/token
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
503 找不到服务 由于服务还没有注册到注册中心中 导致在调用的时候不能够获取,或者灭有开启服务!
Http的用户认证信息传递的方式❤️
# 格式中都有一个空格
- Basic认证 #在授权码模式中进行交互
- 在Header中的Authorization中传递用户名和密码
- 格式:Authorization: Basic Base64(用户名:密码)
- Bearer认证 #在密码模式中进行交互,用于服务与服务之间的访问时所携带的请求头信息(在网关中定义)
- 在Header中的Authorization中传递用户名和密码
- 格式:Authorization: Bearer AccessToken
认证失败服务端返回 401 Unauthorized。
# 什么是http Basic认证?
http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编码,放在header中请求服务端,一个例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。
认证失败服务端返回 401 Unauthorized。
搭建与使用
# 构建时的注意事项 这里是对密码模式 即服务与服务之间绑定使用
1.feign最终 底层也是http网络协议 所以 oauth2 可以拦截到feign的远程调用 要对feign的对应接口进行放行!!(此处有问题,放行后的接口会变得不安全,应该如何处理)
2.定义好客户端和服务端的注解,否则oauth2不能识别
3.其中有使用到网关,不做赘述。网关只判断令牌是否存在的操作,具体的令牌校验是要交给对应的微服务来校验
网关作用:放行登录页面,拦截其余网页进行鉴权操作。有令牌则放行,无令牌则判定401
4. 获取到的令牌,长短令牌存在redis中,作为val。短令牌进行传输,作为key。 两点好处:1.安全性高 2.短令牌更便于传输
网关
@Component
public class GatewayFilter1 implements GlobalFilter,Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private AuthService authService; //业务层 定义的方法 来生成和获取cookie 未展示
//公钥
private static final String PUBLIC_KEY = "public.key";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求/响应对象
ServerHttpResponse response = exchange.getResponse();
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if ("/oauth/login".equals(path) || "/oauth/toLogin".equals(path)){
//直接放行
return chain.filter(exchange);
}
//获取头
HttpHeaders headers = request.getHeaders();
//从头中获取uid
// String uid = headers.getFirst("uid"); //使用key的方式
String uid = authService.getJtiFromCookie(request); //使用cookie的方式
//空值判断
if(uid==null || "".equals(uid)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//从redis中获取令牌
String jwt = stringRedisTemplate.boundValueOps(uid).get();
//空值判断,只要取出东西就表示认证过
if(jwt==null || "".equals(jwt)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//对请求进行增强,避免在放行后,后续的服务端还要执行一次从redis中获取令牌的操作!
request.mutate().header("Authorization","Bearer "+jwt);
//读取公钥内容
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
//转换为输入流
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
//变为高速缓冲流
BufferedReader br = new BufferedReader(inputStreamReader);
//读取内容
String s = br.readLine();
//存入请求中增强,该方式会被拦截到吗
request.mutate().header("signer",s);
//int a = 1/0; //造个异常
} catch (Exception e) {
e.printStackTrace();
//报错请求超时 最终要显示友好页面 不会直接拒绝401
response.setStatusCode(HttpStatus.REQUEST_TIMEOUT);
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
//======================================================================
//========================不放行的方式===================================
//======================================================================
//不放行的两种方式
if (StringUtils.isEmpty(jwt)){
//拒绝访问 直接拒绝
/*response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();*/
//拒绝访问 设置重新跳转页面
return this.toLoginPage(LOGIN_URL,exchange);
}
//跳转登录页面
private Mono<Void> toLoginPage(String loginUrl, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.SEE_OTHER);
response.getHeaders().set("Location",loginUrl);
return response.setComplete();
}
授权服务
在核心配置类上使用@EnableAuthorizationServer来定义该工程为授权服务端
导包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
核心代码❤️
自定义页面后,不设置http.formLogin()等匹配网页的方法一样生效???
大致步骤与前面在postman中的测试是一致!
1.定义访问地址 http://ip:端口/oauth/token
2.封装必要的 basic认证进请求头和请求体信息
3.开始oauth2的认证
4.结果令牌存入redis后返回前端
@Service
public class AuthServiceImpl implements AuthService {
//注入核心模板对象
@Autowired
private RestTemplate restTemplate; //该类要存入IOC中才可以被加载 new RestTemplate即可
//注入加载客户端 目的:远程获取访问路径
@Autowired
private LoadBalancerClient loadBalancerClient;
//注入redis模板 使用String 对String类型存入缓存数据库更加友好
@Autowired
private StringRedisTemplate stringRedisTemplate;
//定义过期时间
@Value("${auth.ttl}")
private long ttl;
//登录方法
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//1.封装请求条件
//从注册中心里获取到自己的请求实例对象
ServiceInstance choose = loadBalancerClient.choose("nacos-provider-oauth");
//从注册中心获取到对应的ip和port
URI uri = choose.getUri();
//拼接访问地址 由oauth2提供
String url = uri+"/oauth/token";
//设置指定错误信息 不进行处理
//如果账户密码不匹配则出现400/401
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//判定不对400和401的异常进行处理
if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
super.handleError(response); //处理异常
}
}
});
//添加请求体
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password"); //设置模式 密码模式
body.add("username",username); //设置账户,为前端传递的值
body.add("password",password); //设置密码,为前端传递的值
//添加请求头base64编码信息
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization",this.getHttpBasic(clientId,clientSecret)); //携带证书密码进行转码,规定
//封装条件
HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);
//2.对oauth2发送请求 获取到令牌 如果报错 目前的使用,多半原因是密码账户不正确导致
ResponseEntity<Map> token = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
//获取到map格式
Map map = token.getBody();
//非空怕判断 判断必要参数是否都存在 不存在说明oauth2生成有问题
if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){
//申请令牌失败
throw new RuntimeException("申请令牌失败。错误原因:账户密码可能错误!");
}
//3.封装令牌
AuthToken authToken = new AuthToken(); //自定义的类来封装令牌
authToken.setAccessToken((String) map.get("access_token")); //长令牌
authToken.setRefreshToken((String) map.get("refresh_token")); //刷新令牌
authToken.setJti((String)map.get("jti")); //短令牌
//4.令牌存放在redis中 短为key 长为val
BoundValueOperations<String, String> s = stringRedisTemplate.boundValueOps(authToken.getJti());
//设置
s.set(authToken.getAccessToken(),ttl,TimeUnit.SECONDS);
return authToken;
}
//转为basic认证格式
private String getHttpBasic(String clientId, String clientSecret) {
//按照 Basic 空格 账户:密码 的规定格式进行转码
String value = clientId+":"+clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic "+new String(encode);
}
}
四个核心配置类
1.告知SpringSecurity系统自定义用户的密码、权限等详细
/*****
* 自定义授权认证类
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
@Autowired
private UserFeign userFeign;
/****
* 自定义授权认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if(authentication==null){
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if(clientDetails!=null){
//秘钥
String clientSecret = clientDetails.getClientSecret();
//静态方式
//return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
//数据库查找方式
return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
//因为是远程调用 但是对方已经被oauth2保护 所以需要对该接口地址进行放行
com.wz.user.pojo.User un = userFeign.findUserInfo(username);
//根据用户名查询用户信息
//将密码进行转码,以和数据库的明文进行匹配。如果数据库是密文 那么此处不进行转换
// String pwd = new BCryptPasswordEncoder().encode(un.getPassword());
//创建User对象
String permissions = "goods_list,seckill_list";
UserJwt userDetails = new UserJwt(username,un.getPassword(),AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
return userDetails;
}
}
2.把Authentication信息转成自定用户信息的类
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Autowired
UserDetailsService userDetailsService;
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap response = new LinkedHashMap();
String name = authentication.getName();
response.put("username", name);
Object principal = authentication.getPrincipal();
UserJwt userJwt = null;
if(principal instanceof UserJwt){
userJwt = (UserJwt) principal;
}else{
//refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
userJwt = (UserJwt) userDetails;
}
response.put("name", userJwt.getName());
response.put("id", userJwt.getId());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
配置OAuth2,告诉框架OAuth2的客户端信息存储在哪里
是@EnableAuthorizationServer 来定义为服务器
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
@Autowired
private DataSource dataSource;
//jwt令牌转换器
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//SpringSecurity 用户自定义授权认证类
@Autowired
UserDetailsService userDetailsService;
//授权认证管理器
@Autowired
AuthenticationManager authenticationManager;
//令牌持久化存储接口
@Autowired
TokenStore tokenStore;
@Autowired
private CustomUserAuthenticationConverter customUserAuthenticationConverter;
/***
* 客户端信息配置
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).clients(clientDetails());
}
/***
* 授权服务器端点配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)//认证管理器
.tokenStore(tokenStore) //令牌存储
.userDetailsService(userDetailsService); //用户信息service
}
/***
* 授权服务器的安全配置
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients()
.passwordEncoder(new BCryptPasswordEncoder())
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
//读取密钥的配置
@Bean("keyProp")
public KeyProperties keyProperties(){
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
//客户端配置
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
@Autowired
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/****
* JWT令牌转换器
* @param customUserAuthenticationConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
keyProperties.getKeyStore().getLocation(), //证书路径 wz.jks
keyProperties.getKeyStore().getSecret().toCharArray()) //证书秘钥 changgouapp
.getKeyPair(
keyProperties.getKeyStore().getAlias(), //证书别名 wz
keyProperties.getKeyStore().getPassword().toCharArray()); //证书密码 wz
converter.setKeyPair(keyPair);
//配置自定义的CustomUserAuthenticationConverter
DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
return converter;
}
}
告诉OAuth2使用的密码方式
@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/***
* 忽略安全拦截的URL
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
//放行
"/oauth/login", //放行登录方法
"/oauth/toLogin",
"/user/load/**",
"/oauth/logout"
);
}
/***
* 创建授权管理认证对象
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
/***
* 采用BCryptPasswordEncoder对密码进行编码
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/****
*
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic() //启用Http基本身份验证
.and()
.formLogin() //启用表单身份验证
.and()
.authorizeRequests() //限制基于Request请求访问
.anyRequest()
.authenticated(); //其他请求都需要经过验证
//自定义页面后的放行操作 指定处理页面
http.formLogin()
.loginPage("/oauth/toLogin")
.loginProcessingUrl("/oauth/toLogin");
// .failureForwardUrl("https://www.baidu.com/index.php?tn=monline_3_dg");
}
}
资源服务
在核心配置类上使用@EnableResourceServer来定义该工程为资源服务端,则表明变为受保护的服务。如果没有鉴权是不可以进行访问的
导包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
核心配置类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) //激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
自定义登录页面 ❤️
-
准备登录页面
在oauth模块中定义
-
提供登录页面的访问地址 在配置文件中配置文件
// 在继承WebSecurityConfigurerAdapter的配置类中定义 public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .httpBasic() //启用Http基本身份验证 .and() .formLogin() //启用表单身份验证 .and() .authorizeRequests() //限制基于Request请求访问 .anyRequest() .authenticated(); //其他请求都需要经过验证 //自定义页面后的放行操作 指定处理页面 是在微服务与微服务之间进行的放行操作 http.formLogin() .loginPage("/oauth/toLogin") //设置访问登录页面的路径 .loginProcessingUrl("/oauth/toLogin"); //设置执行登录操作的路径 // .failureForwardUrl("https://www.baidu.com/index.php?tn=monline_3_dg"); }
-
放行自定义的登录地址
在继承WebSecurityConfigurerAdapter的配置类中定义的类中做放行操作
web.ignoring().antMatchers(
//放行
"/oauth/login", //放行登录方法
"/oauth/toLogin",
"/user/load/**",
"/oauth/logout"
);
项目第十天
资源服务权限访问显示
1.资源服务中 使用@EnableGlobalMethodSecurity 在对应的模块的核心配置类上开启
2.在对应的controller方法上方使用,那么该方法必须要有指定的权限。权限在oauth模块的userDetail核心配置文件中定义
@PreAuthorize("hasAnyAuthority('admin','user')")
feign拦截器
服务与服务之间调用的时候 由于继承过oauth所以需要令牌认证时使用
前提是两边服务器都集成过oauth2需要权限验证访问 如果没有则不需要设置拦截器
拦截器应该要在发起方存入IOC
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//传递令牌,登录后的上下文信息存储位置
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null){
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
if (request != null){
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()){
String headerName = headerNames.nextElement();
if ("authorization".equals(headerName)){
String headerValue = request.getHeader(headerName); // Bearer jwt
System.out.println();
//传递令牌
requestTemplate.header(headerName,headerValue);
}
}
}
}
未登录时页面跳转
# 由于有oauth2的存在且需要使用到该用户的信息,那么使用该工具类getUserInfo方法可以直接获取到jwt的载荷部分信息
用户登录后,用户的信息会封装到 SecurityContextHolder.getContext().getAuthentication() 里面, 我们可以将数据从这里面取出,然后转换成 OAuth2AuthenticationDetails ,在这里面可以获取到令牌信息、令牌类型等。
//工具类 getUserInfo方法会调用下面的两个方法来级联使用 最终返回已经解析过jwt的map对象
@Component
public class TokenDecode {
//公钥
private static final String PUBLIC_KEY = "public.key";
private static String publickey="";
/***
* 获取用户信息
* @return
*/
public Map<String,String> getUserInfo(){
//获取授权信息
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
//令牌解码
return dcodeToken(details.getTokenValue());
}
/***
* 读取令牌数据
*/
public Map<String,String> dcodeToken(String token){
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(getPubKey()));
//获取Jwt原始内容
String claims = jwt.getClaims();
return JSON.parseObject(claims,Map.class);
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
public String getPubKey() {
if(!StringUtils.isEmpty(publickey)){
return publickey;
}
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
publickey = br.lines().collect(Collectors.joining("\n"));
return publickey;
} catch (IOException ioe) {
return null;
}
}
}
使用该工具类来获取用户名
因为 在工具类中获取到的其实是jwt 经过rsa解密之后就可以获取到 载荷部分的用户信息 最终返回一个map对象
所以可以在get(“username”)来获取到对应的用户名
//购物车添加商品
@GetMapping("/addCart")
public Result addCart(@RequestParam("skuId") String skuId, @RequestParam("num") Integer num){
//动态获取当前人信息,暂时静态
String username = tokenDecode.getUserInfo().get("username");
cartService.addCart(skuId,num,username);
return new Result(true, StatusCode.OK,"加入购物车成功");
}
页面跳转到上一网页
//跳转到自定义登陆页面
@RequestMapping("/toLogin")
//此处定义变量from的获取 是为了在集成oauth2时的访问未授权页面跳转首页登录后 再自动跳转到上一页面
// 1. 在网关中进行拦截 由于放行登录,会被默认跳转到首页下属方法就是通往首页的接口 跳转过程中拼接首页地址?FROM=当前访问地址
// 2. 跳转到一个登录接口 该地址使用?FROM=当前访问地址 来拼接
// 3. 下述就是该接口,获取到FROM后就在model中存储 携带到html中去渲染
// 4. 首页html 定义一个变量存储这个from, 在登陆后的成功回调中 就直接跳转到该from变量即可实现返回上一页面
public String toLogin(@RequestParam(value = "FROM",required = false,defaultValue = "") String from, Model model){
model.addAttribute("from",from); //在model添加key val 来渲染页面
return "login";
}
RunDashboard的使用
是使用在SpringBoot中使用, 在.idea的文件夹的workspace.xml中找到 RunDashboard 加入
加入以下
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<component name="RunDashboard"> ←找到他
<option name="configurationTypes"> 👈加入他
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
<option name="contentProportion" value="0.17659138" />
</component>
String claims = jwt.getClaims();
return JSON.parseObject(claims,Map.class);
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
public String getPubKey() {
if(!StringUtils.isEmpty(publickey)){
return publickey;
}
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
publickey = br.lines().collect(Collectors.joining("\n"));
return publickey;
} catch (IOException ioe) {
return null;
}
}
}
使用该工具类来获取用户名
因为 在工具类中获取到的其实是jwt 经过rsa解密之后就可以获取到 载荷部分的用户信息 最终返回一个map对象
所以可以在get(“username”)来获取到对应的用户名
//购物车添加商品
@GetMapping("/addCart")
public Result addCart(@RequestParam("skuId") String skuId, @RequestParam("num") Integer num){
//动态获取当前人信息,暂时静态
String username = tokenDecode.getUserInfo().get("username");
cartService.addCart(skuId,num,username);
return new Result(true, StatusCode.OK,"加入购物车成功");
}
页面跳转到上一网页
//跳转到自定义登陆页面
@RequestMapping("/toLogin")
//此处定义变量from的获取 是为了在集成oauth2时的访问未授权页面跳转首页登录后 再自动跳转到上一页面
// 1. 在网关中进行拦截 由于放行登录,会被默认跳转到首页下属方法就是通往首页的接口 跳转过程中拼接首页地址?FROM=当前访问地址
// 2. 跳转到一个登录接口 该地址使用?FROM=当前访问地址 来拼接
// 3. 下述就是该接口,获取到FROM后就在model中存储 携带到html中去渲染
// 4. 首页html 定义一个变量存储这个from, 在登陆后的成功回调中 就直接跳转到该from变量即可实现返回上一页面
public String toLogin(@RequestParam(value = "FROM",required = false,defaultValue = "") String from, Model model){
model.addAttribute("from",from); //在model添加key val 来渲染页面
return "login";
}
RunDashboard的使用
是使用在SpringBoot中使用, 在.idea的文件夹的workspace.xml中找到 RunDashboard 加入
加入以下
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<component name="RunDashboard"> ←找到他
<option name="configurationTypes"> 👈加入他
<set>
<option value="SpringBootApplicationConfigurationType" />
</set>
</option>
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
<option name="contentProportion" value="0.17659138" />
</component>