@TOC
后台管理系统
登录和退出和检查未登录
第一次登录:
退出登录:
@RequestMapping("/admin/do/logout.html")
public String doLogout(HttpSession session) {
// 强制 Session 失效
session.invalidate();
return "redirect:/admin/to/login/page.html";
}
检查是否已经登录:
用户CURD(后端渲染)
分页查询:
单条删除:
增加:
更新:
角色CURD(前端渲染)
分页查询:
新增:
更新:
单条删除与批量删除:
- 前端的“单条删除”和“批量删除”在后端合并为同一套操作。合并的依据是:单 条删除时 id 也放在数组中,后端完全根据 id的数组进行删除。
菜单管理
树形结构基础知识介绍:
创建菜单的数据库表:
create table t_menu (
id int(11) not null auto_increment,
pid int(11), name varchar(200),
url varchar(200),
icon varchar(200),
primary key (id)
);
在 Java 类中表示树形结构:
- 基本方式 在 Menu 类中使用 List<Menu> children 属性存储当前节点的子节点。
- 为了配合 zTree 所需要添加的属性:
①pid 属性:找到父节点
②name 属性:作为节点名称
③ icon 属性:当前节点使用的图标
④
open 属性:控制节点是否默认打开⑤url 属性:点击节点时跳转的位置
页面显示树形结构:
- 将数据在 Java 代码中组装成树形结构
- 再返回根节点
添加子节点:
更新子节点:
删除子节点:
权限分配
权限控制:
- 权限控制机制的本质就是“用钥匙开锁”。
给 Admin 分配 Role(需要用户与角色的中间表):
- 查询需要查询用户所分配的角色和未分配的角色。
保存分配的角色的方法(暴力解决方法):①先删除所拥有的的角色 ②添加分配的角色。
创建权限数据库:
CREATE TABLE `t_auth` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) DEFAULT NULL,
`title` varchar(200) DEFAULT NULL,
`category_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
);
- name 字段:给资源分配权限或给角色分配权限时使用的具体值,将来做权限验证 也是使用 name 字段的值来进行比对。建议使用英文。
- title 字段:在页面上显示,让用户便于查看的值。建议使用中文。
- category_id 字段:关联到当前权限所属的分类。这个关联不是到其他表关联,而是就在当前表内部进行关联,关联其他记录。所以说,t_auth 表中是依靠 category_id 字 段建立了“节点”之间的父子关系。
- name 字段中值的格式:中间的“:”没有任何特殊含义。不论是我们自己写的代码 还是将来使用的框架都不会解析“:”。如果不用“:”,用“%、@、&、*、-”等等这样 的符号也都是可以的。 ①模块:操作名 <1>user:delete <2>user:get <3>role:delete <4>……
给 Role 分配 Auth(需要角色与权限的中间表):
- 查询需要查询所有的权限
- 再查询角色所拥有的权限
保存分配的权限的方法(暴力解决方法):①先删除所拥有的的权限 ②添加分配的权限。
给 Menu 分配 Auth((需要菜单与权限的中间表)):
-
一个菜单对应多个权限
-
例如一个用户管理菜单对应用户的CURD。
SpringSecurity使用
简介:
springsecurity:对于处理登录,退出,未登录拦截,授权都做好了处理(过滤器和拦截器),我们要做的就是指定url和指定账号,密码,角色,权限添加处理器:对应handler就是替换对应的url映射,也就是替换springsecurity底层做好的处理器
使用:
-
密码加密:要么使用装配好的,要么实现passwordencode实现类
-
数据库认证:实现userdetail servicre接口,返回账号,密码,角色,权限
-
认证:指定登录地址,指定登录处理请求地址(设置参数),指定登录失败地址,指定登录成功地址
-
退出:指定退出成功后跳转url,指定处理退出的请求url
-
授权: 指定地址是否可以直接访问 ①
放行首页地址和静态资源。 ②登录请求地址springsecurity通过loginUrl方法设置。 -
登录后的角色,权限控制:指定访问拥有的权限,指定访问拥有的角色,指定访问异常处理的页面,指定访问异常处理的处理器 ①
通过写方法和标签分配太麻烦,一般在指定方法上通过springsecurity注解分配。
总结:
- springsecurity分为两个步骤:认证,授权。
认证:指定登录登出的请求地址和成功后地址等等,以及如何获取用户信息,权限,角色以及如何加密密码授权:根据用户角色,权限进行配置,以及无权限的异常处理
注意:
- 谁来把 WebAppSecurityConfig 扫描到 IOC 里?
①
结论:为了让 SpringSecurity 能够针对浏览器请求进行权限控制,需要让 SpringMVC 来扫描 WebAppSecurityConfig 类。②衍生问题:DelegatingFilterProxy 初始化时需要到 IOC 容器查找一个 bean, 这个 bean 所在的 IOC 容器要看是谁扫描了 WebAppSecurityConfig。 如果是 Spring 扫描了WebAppSecurityConfig,那么 Filter 需要的 bean 就在 Spring 的 IOC 容器。 如果是 SpringMVC 扫描了 WebAppSecurityConfig,那么 Filter 需要的 bean 就在 SpringMVC 的 IOC 容器。 - 明确三大组件启动顺序: ① 首先:ContextLoaderListener 初始化,创建 Spring 的 IOC 容器 ② 其次:DelegatingFilterProxy 初始化,查找 IOC 容器、查找 bean ③ 最后:DispatcherServlet 初始化,创建 SpringMVC 的 IOC 容器
- DelegatingFilterProxy 查找 IOC 容器然后查找 bean 的工作机制:
- 解决方案一:把两个 IOC 容器合二为一 不使用 ContextLoaderListener,让 DispatcherServlet 加载所有 Spring 配置文件。
①DelegatingFilterProxy 在初始化时查找 IOC 容器,找不到,放弃。
②第一次请求时再次查找。 找到 SpringMVC 的 IOC 容器。
③从这个 IOC 容器中找到所需要的 bean。
④
遗憾:会破坏现有程序的结构。原本是 ContextLoaderListener 和 DispatcherServlet 两个组件创建两个 IOC 容器,现在改成只有一个。 解决方案二:改源码- 这个结果为什么没有经过异常映射机制?
密码加密
java中实现MD5加密通常有两种方法:
通过java自带的java.security.MessageDigest实现加密①MessageDigest的digest()返回的是byte[],而我们期望的到的MD5加密后的值应该是32位的16进制数,所以还要将byte[]转换为16进制数组成的字符串。 ②如果你看过别人的代码,有的人直接调用的MessageDigest的digest(byte[] input)而没有调用update(向MessageDigest传送要计算的数据;传入的数据需要转化为指定编码的字节数组)方法,那是因为digest()已经将update()封装了,所以不要感到奇怪。通过DigestUtils工具类(需要导入commons-codec)①可以发现使用DigestUtils返回的直接就是32位的16进制数组成的字符串,使用它就简单多了(推荐使用).
应用:
- 链接:java与MD5加密以及两次MD5加密
- 使用两次MD5对用户密码进行加密
第一次MD5+固定Salt加密:第一次MD5是在浏览器端,通过js文件中进行MD5加密,由于http报文的明文传输,如果传输的数据被截获(例如使用fiddler等抓包工具),那用户密码将暴露无疑,所以我们使用MD5和固定Salt将表单的关键数据(例如密码)进行加密.第二次MD5+随机Salt加密:第二次MD5是在服务器端完成的,它将用户的关键信息进行加密后存入到数据库中,这样如果数据库的被盗后也得不到用户原始的密码信息.- 注意:
①
MD5加密是不可逆的,但是你可能会发现有一些md5在线解密的网站能够通过密文获得你原始的数据,但它并不是反推出来的,而是通过反向查询,穷举字符组合的方式,创建了明文密文对应查询数据库,这样获得的你的原始数据.(例如它查询出123456的MD5加密后的密文为abcdef,然后将这一对数据存入数据库,当你查询输入密文为abcdef的时候,它就返回了123456的原文) ②登录的时候,要去取数据库里面对应用户的密码和salt值,后台接收到前端做了一次MD5的密码formPass,然后将这个formPass和数据库里面的salt一起再做一次MD5加密,然后看是否与数据库里面存的那个密码一致,如果一致,则登录成功,否则登录失败。(因此随机盐值需要明文保存起来)
后台管理系统总结
简介:
- 步骤:
①用户登录
②查询用户对应的角色
③查询角色对应的权限
④
根据权限反去查权限对应的菜单再去显示对应的菜单或者直接显示全部菜单等到用户操作时再判断并选择跳到相应页面。 - 每个用户对应的权限和菜单不同。
在系统中一个操作对应一个权限吗?①根据具体业务要求,可以将增删查改某个表的数据定义为一个权限,也可以分开为四个权限。 ②另外有些权限不一定对应着系统里的一个操作,而是返回给前端,前端根据是否有权限决定是否显示某些页面或菜单或按钮。
订单秒杀系统
简介:
- 链接: ①实现订单(包括支付,二维码) ②实现订单 ③订单并发问题 ④电商项目难点详细分析 ⑤订单系统难点简单分析 ⑥Java电商项目面试--订单模块 ⑦秒杀订单
实现订单思路:①查询商品消息。 ②计算订单总价:已经查询出了每件商品的信息,通过每件商品的价格乘以订单中该件商品的数量就可以得到订单的总价。 ③订单详情入库:现在是买家买东西,我们为买家创建订单,包括订单号,商品号,商品名称,商品价格,商品图片等,买家选择好以后要将这些生成一个订单写入数据库。(订单号在业务层生成出来)④买家订单入库将买家创建的所有订单写入数据库:包括订单总价,订单号等等。 ⑤减库存:当买家订单创建完以后要扣库存。订单号生成规则:①订单号无重复性。②如果方便客服的话,最好是“日期+自增数”样式的订单号,客服一看便知道订单是否在退货保障期限内容。 ③订单号长度尽量保持短(10位以内),方便用户,尤其电话投诉时,长的号码报错几率高,影响客服效率。订单状态分析:①常见订单状态主要有待付款、待发货、待收货、待评价、售后/退款。 <1>待付款:代表买家下单了但是还没有付款。 <2>待发货(同待接单):代表买家付款了卖家还没有发货。 <3>已发货(同待收货):代表卖家已经发货并寄出商品了。 <4>已完成(同待评价):代表买家已经确认收到货了。 <5>已关闭(同已取消):代表订单过期了买家也没付款、或者卖家关闭了订单。 <6>售后/退款:代表订单出现了问题,如商品坏掉了,以及其他的问题"扯皮". ②状态之间可以如何转换: <1>从无到待付款:1、触发条件:下单 2、用户浏览完商品,觉得符合心理预期,决定购买该商品,点击购买,APP会进入付款页面,此时这笔订单的状态就是“代付款”,该状态为开始状态,即每个订单的第一个状态都会是该状态。 <2>从待付款到待发货:1、触发条件:付款 2、用户进入付款页面确认信息无误,点击提交订单,输入密码,APP提示支付成功后,该订单的状态由“待付款”变为“待发货”。 <3>从待发货到已发货:1、触发条件:发货 2、卖家核实用户信息后,通过后台发货系统,输入物流单号,点击发货,平台提示发货成功,该订单的状态由“待发货”变为“已发货”,虚拟产品直接点击发货,订单状态变为“已发货”。 <4>从已发货到已完成1、触发条件:确认收货 2、目前有两种方式确认收货,一种是用户取完快递,平台即确认收货,订单状态变为“已完成”或者“确认收货”,另一种是买家在APP上点击确认收货,订单状态变为“已完成”或者“确认收货”。 <5>从待付款到已关闭1、触发条件:关闭订单 2、当用户超时不付款,订单就会自动从待付款变成已关闭。当然某些电商平台也允许卖家手动关闭订单,或者买家手动关闭订单。 <6>从待发货到部分发货1、触发条件:选择部分商品发货 2、当卖家选择了订单中部分商品进行发货,此时订单状态有个子状态叫做部分发货,当卖家把剩余的商品都发货成功,此时订单状态才变成已发货。 <7>从待发货到退款中1、触发条件:申请退款 2、当买家在待发货的时候,选择了订单中某个商品进行退款,此时订单进入状态“退款中”。 <8>从已发货到退款中1、触发条件:申请退款 2、当买家在已发货的时候,选择了订单中某个商品进行退货/退款,此时订单进入状“退款中”。 <9>从已完成到退款中1、触发条件:申请退款 2、买家在收到商品并且确认收货之后,发现并不符合自己的心理预期,或者是质量等问题,选择申请退款,在平台审核后符合退款规则,订单状态变为“退款中”。 <1>从已发货到派送中1、触发条件:终端物流公司录入订单信息 2、当商品到达当地物流点,物流公司确认之后,会把信息录入平台系统,一般会有专人打电话通知买家取货,此时的订单状态变为“派送中”。 <11>从派送中到未能签收1、触发条件:买家未成功签收 2、终端物流点通知买家取货,买家拒收,或者联系不到买家,一段时间后订单状态变为“未能成功签收”。订单并发问题分析:①悲观锁:在事务中查询数据的时候尝试对数据进行加锁(互斥锁), 获取到锁的事务可以对数据进行操作,获取不到锁的事务会阻塞,直到锁被释放。 <1>悲观锁有死锁问题,不推荐使用 ②乐观"锁":乐观锁本质上不是加锁,查询数据的时候不加锁,对数据进行修改的时候需要进行判断,修改失败需要重新进行尝试。 ③任务队列:将下单的代码封装成celery任务函数,在下单API中只是发出下单的任务消息,同时celery的worker工作进程进行只创建一个。秒杀订单:①redis缓存: <1>尝试对前端页面进行相应的优化,比较典型的是缓存,一些东西可以存在浏览器身上或者redis中,提高相应速度,降低后端压力。1 、页面缓存:商品列表页面,商品详情页面 2、对象缓存:更新登录的用户的密码 <2>解决超卖:1、超卖问题是用redis+rabbitmq解决的,先将mysql中商品库存信息写入redis;然后每次下单都从redis中做库存预减1,如果库存值减1后大于0,则判断该用户秒杀成功,进入消息队列,做异步减库存,下订单操作。因为redis是单线程的,所以不会存在超卖并发问题。 2、mysql方面第一点可以以用户id和商品id建立唯一索引(防止一个用户发送多个请求,秒杀到多件同类商品),第二点在减库存的sql语句中加上and stock_count > 0,防止多个用户同时秒杀到最后一件商品出现负,本质是利用数据库锁。
高并发下防止库存超卖的解决方案:
- 超卖:
库存没了还卖出去 如何产生:在高并发场景下,如果同时有两个线程a和b,同时查询到商品库存为1,他们都认为存库充足,于是开始下单减库存。如果线程a先完成减库存操作,库存为0,接着线程b也是减库存,于是库存就变成了-1,商品被超卖了。
//1.查询出商品库存信息
select stock from t_goodswhere id=1;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id)values (null,1);
//3.修改商品库存
update t_goodsset stock=stock-1where id=1;
悲观锁:①所谓悲观锁,即悲观的认为自己在操作数据库时,会大几率出现并发,于是在操作前会先进行加锁,操作完成后再释放锁。如果加锁失败说明该记录正在被修改,那么当前操作可以等待后尝试。 ②以我们常用的MySQL为例,行锁、表锁、排他锁等都是悲观锁,为避免冲突,会在操作时先加锁,其他线程必须等待它的完成。 ③这里我们通过使用select...for update语句,在查询商品表库存时将该条记录加锁,待下单减库存完成后,再释放锁。 ④这样可以解决并发时库存超卖的问题,然而高并发时,所有的操作都被串行化了,效率很低,将严重影响系统的吞吐量。而且使用悲观锁还有可能造成死锁问题。
//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select stock from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品stock减一
update t_goods set stock=stock-1 where id=1;
//4.提交事务
commit;
乐观锁:①现在我们尝试下使用乐观锁,所谓乐观锁,是相对于悲观锁而言的,它假设数据一般情况下不会发生并发,因此不会对数据进行加锁,操作完成提交时才对数据是否冲突进行检测,如果发现冲突则返回错误。 ②比较常见的实现方式是,在表中增加一个version字段,操作前先查询version信息,在数据提交时检查version字段是否被修改,如果没有被修改则进行提交,否则认为是过期数据。 ③这样在并发时,如果线程a尝试修改商品库存时,发现版本号已经被线程b修改了,线程a执行update语句条件不满足便不再执行了,库存也不会被超卖。 ④但是这种乐观锁的方式,在高并发时,只有一个线程能执行成功,会造成大量的失败,这给用户的体验显然是很不好的。 ⑤这里我们可以减小锁的颗粒度,最大程度提升系统的吞吐量,提高并发能力:下面的update语句通过stock>0进行乐观锁的控制,在执行时,会在一次原子操作中查询stock的值,并扣减一。
//1.查询出商品信息
select stock, version from t_goods where id=1;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品库存
update t_goods set stock=stock-1, version = version+1 where id=1, version=version;
//减小锁的颗粒度:修改商品库存时判断库存是否大于0
update t_goods set stock=stock-1 where id=1 and stock>0;
分布式锁:①除了在数据库层面加锁,我们还可以通过在内存中加锁,实现分布式锁。例如我们可以在Redis中设置一个锁,拿到锁的线程抢购成功,拿不到锁的抢购失败。 ②Redis的setnx方法可以实现锁机制,key不存在时创建,并设置value,返回值为1;key存在时直接返回0。线程调用setnx方法成功返回1认为加锁成功,其他线程要等到当前线程业务操作完成释放锁后,才能再次调用setnx加锁成功。
Long TIMEOUT_SECOUND = 120000L;
Jedis client =jedisPool.getResource();
//线程设置lock锁成功
while(client.setnx("lock",String.valueOf(System.currentTimeMillis())) == 1){
Long lockTime = Long.valueOf(client.get("lock"));
//持有锁超时后自动释放锁
if(lockTime!=null&& System.currentTimeMillis() > lockTime+TIMEOUT_SECOUND){
client.del("lock");
}
Thread.sleep(10000);
}
......
......
client.del("lock");
Redis原子操作:①虽然通过以上方按可以防止库存超卖,但是高并发情况下对数据库进行频繁操作,会造成严重的性能问题。因此我们必须在前端对请求进行限制。 ②我们可以在Redis中设置一个队列key为商品的id,队列的长度为商品库存量。每次请求到达时pop出一个元素,这样拿到元素的请求即认为秒杀成功,后续通过MQ发送消息异步完成数据库减库存操作。没有拿到元素的请求即认为秒杀失败。 ③由于Redis是工作线程是单线程的,而list的pop操作是原子性的,因此并发的请求都被串行化了,库存就不会超卖了。
//获取商品库存
Stringtoken= redisTemplate.opsForList().leftPop(goodsStock);
if(token==null){
log.info(">>>商品已售空");
return setResultError("亲,该秒杀已经售空,请下次再来!");
}
//异步发送MQ消息,执行数据库操作
sendSecondKillMsg(goodsId, userId);
- 当然除此之外还有很多其他解决方案,也有很多可以优化的地方。
- 链接:mysql超卖问题处理_mysql 解决超卖问题的锁分析