第一章 完善用户相关信息
用户注册与登录
- 数据库表设计:用户表、用户信息表
- 相关接口(API):获取
RSA公钥
、用户注册、用户登录
数据库表设计及相关实体类设计
用户表
用户信息表
- 根据这两张数据库表创建对应的实体类
基于JWT的用户token验证
先来看看基于session的用户身份验证的特点
- 验证过程:服务端验证浏览器携带的用户名和密码,验证通过后生成用户凭证保存在服务端(session),浏览器再次访问时,服务端查询session,实现登录状态保持
- 缺点:随着用户的增多,服务端压力增大;若浏览器cookie被攻击者拦截,容易受到跨站请求伪造攻击;分布式系统下扩展性不强
基于token的用户身份验证
- 验证过程:服务端验证浏览器携带的用户名和密码,验证通过后生成用户令牌(token)并返回给浏览器,浏览器再次访问时携带token,服务端校验token并返回相关数据
- 优点:token不储存在服务器,不会造成服务器压力;token可以存储在非cookie中,安全性高;分布式系统下扩展性强
什么是 JWT ?
- JWT:全称是JSONWeb Token,JWT是一个规范,用于在空间受限环境下安全传递“声明”。
- JWT的组成:JWT分成三部分,第一部分是头部(header),第二部分是载荷(payload),第三部分是签名(signature)
- JWT优点:跨语言支持、便于传输、易于扩展
JWT 的内部有什么 ?
- JWT头部:声明的类型、声明的加密算法(通常使用SHA256)
- JWT载荷:存放有效信息,一般包含签发者、所面向的用户、接受方、
过期时间
、签发时间以及唯一身份标识
- JWT签名:主要由头部、载荷以及秘钥组合加密而成
用户关注与动态提醒
用户关注
- 数据库表设计:用户关注表、用户关注分组表
- 相关接口(API):关注用户、关注列表、粉丝列表、分页查询用户
数据库表设计及相关实体类设计
用户关注分组表
- 先新增3条数据
用户关注表
动态提醒(重点)
- 数据库表设计:用户动态表
- 相关接口(API):用户发布动态、用户查询订阅内容的动态
- 设计模式:订阅发布模式
数据库表设计及相关实体类设计
用户动态表
订阅发布模式
订阅发布模式 与 观察者模式的区别
实现动态提醒的工具
- RocketMQ:纯java编写的开源消息中间件,特点是:高性能、低延迟、分布式事务
- Redis:高性能缓存工具,数据存储在内存中,读写速度非常快
- RocketMQ 相关工具类及配置实现
用户权限控制(重点)
- 权限控制是什么:控制用户对系统资源(URI)的操作
- 前端的权限控制:对页面或页面元素的权限控制
- 前端的权限控制:对页面或页面元素的权限控制
B站会员等级权限
- 访问权限:哪些页面可以访问、哪些页面元素可见等等
- 操作权限:如页面按钮是否可点击、是否可以增删改查等等
- 接口与数据权限:接口是否可以调用、接口具体字段范围等等
RBAC权限控制模型
- RBAC权限控制模型(Role-Based Access Control):基于角色的权限控制
- RBAC模型的层级:RBAC0、RBAC1、RBAC2、RBAC3
- 关键词:用户、角色、资源、权限、操作
- 通过权限对
资源
和操作
的相关绑定,再通过角色绑定相关的资源,就能变相地让角色拥有资源和操作的相关权限 - 最后通过用户和角色的相关绑定,来达到用户可以对资源和操作进行权限控制的目的
- 通过权限对
项目实现 RBAC权限控制模型
- 用户:注册用户
- 角色:Lv0~Lv6 会员
- 权限:视频投稿、发布动态、各种弹幕功能等等
- 资源:页面、页面元素
- 操作:点击、跳转、增删改查等等
相关数据库表设计
- 数据库表设计:
角色表
、用户角色关联表
、元素操作权限表
、角色元素操作权限关联表
、页面菜单权限表
、角色页面菜单权限关联表
Spring AOP
- 概念:Spring AOP是一种约定流程的编程
- 关键词:约定(AOP的核心)
- 典型的例子:数据库事务包括打开数据库,设置属性,执行sql语句,没有异常则提交事务,有异常则回滚事务,最后关闭数据库连接
- 打开打开数据库和设置属性,再到异常回滚没有异常则提交,这些都是固定(约定)的流程,而执行sql语句包含的功能有很多种,不是一个固定的操作
- Spring AOP就类似于把这些固定的流程抽出来,做成一个约定的流程,以及在我们约定的流程当中,插入我们个性化的操作的一种工具或一种框架
SpringAOP术语
- 连接点(join point):对应的被拦截的对象
- 切点(point cut):通过正则或指示器的规则来适配连接点
- 切面(aspect):可以定义切点、各类通知和引入的内容
- 通知(advice):,分为前置通知(before)、后置通知(after)、事后返回通知(afterReturning)、异常通知(afterThrowing)
- 织入(weaving):为原有服务(service)对象生成代理对象,然后将与切点匹配的连接点拦截,并将各类通知加入约定流程
- 目标对象(target):被代理对象
基于接口的权限控制
ApiLimitedRole.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
@Component
public @interface ApiLimitedRole {
/**
* 角色权限代码列表
*
* @return
*/
String[] limitedRoleCodeList() default {};
}
ApiLimitedRoleAspect.java
/**
* 切面类
*
* @author xiexu
* @create 2022-06-17 07:37
*/
@Aspect
@Order(1)
@Component
public class ApiLimitedRoleAspect {
@Autowired
private UserSupport userSupport;
@Autowired
private UserRoleService userRoleService;
/**
* 切入点
*/
@Pointcut("@annotation(com.imooc.bilibili.annotation.ApiLimitedRole)")
public void check() {
}
/**
* 前置通知
*
* @param joinPoint
* @param apiLimitedRole
*/
@Before("check() && @annotation(apiLimitedRole)")
public void doBefore(JoinPoint joinPoint, ApiLimitedRole apiLimitedRole) {
Long userId = userSupport.getCurrentUserId();
// 用户具有哪些角色
List<UserRole> userRoleList = userRoleService.getUseRoleByUserId(userId);
// 角色权限代码列表
String[] limitedRoleCodeList = apiLimitedRole.limitedRoleCodeList();
// 将角色权限代码列表转换成set集合
Set<String> limitedRoleCodeSet = new HashSet<>();
for (String s : limitedRoleCodeList) {
limitedRoleCodeSet.add(s);
}
// 用户角色列表转换成set集合
Set<String> roleCodeSet = new HashSet<>();
for (UserRole userRole : userRoleList) {
roleCodeSet.add(userRole.getRoleCode());
}
// 对用户角色列表 和 角色权限代码列表 取交集,roleCodeSet存放的就是他们的交集
roleCodeSet.retainAll(limitedRoleCodeSet);
// 交集指的就是用户角色列表里面有包含,角色权限代码列表里面也有包含,说明用户有某个角色是不允许被访问的
if (roleCodeSet.size() > 0) {
throw new ConditionException("权限不足!");
}
}
}
UserMomentsApi.java
@RestController
public class UserMomentsApi {
@Autowired
private UserMomentsService userMomentsService;
@Autowired
private UserSupport userSupport;
/**
* 用户发布动态
* AuthRoleConstant.ROLE_LV0表示等级0是没有权限进行发布动态的
*
* @param userMoment
* @return
*/
@ApiLimitedRole(limitedRoleCodeList = {AuthRoleConstant.ROLE_LV0})
@PostMapping("/user-moments")
public JsonResponse<String> addUserMoments(@RequestBody UserMoment userMoment) throws Exception {
Long userId = userSupport.getCurrentUserId();
userMoment.setUserId(userId);
userMomentsService.addUserMoments(userMoment);
return JsonResponse.success();
}
}
基于数据的权限控制
DataLimited.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
@Component
public @interface DataLimited {
}
DataLimitedAspect.java
/**
* 切面类
*
* @author xiexu
* @create 2022-06-17 07:37
*/
@Aspect
@Order(1)
@Component
public class DataLimitedAspect {
@Autowired
private UserSupport userSupport;
@Autowired
private UserRoleService userRoleService;
/**
* 切入点
*/
@Pointcut("@annotation(com.imooc.bilibili.annotation.DataLimited)")
public void check() {
}
/**
* 前置通知
*
* @param joinPoint
*/
@Before("check()")
public void doBefore(JoinPoint joinPoint) {
Long userId = userSupport.getCurrentUserId();
// 用户具有哪些角色
List<UserRole> userRoleList = userRoleService.getUseRoleByUserId(userId);
// 用户角色列表转换成set集合
Set<String> roleCodeSet = new HashSet<>();
for (UserRole userRole : userRoleList) {
roleCodeSet.add(userRole.getRoleCode());
}
// 把切到的方法里面的参数获取到,也就是addUserMoments(@RequestBody UserMoment userMoment)方法的userMoment参数
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof UserMoment) {
UserMoment userMoment = (UserMoment) arg;
String type = userMoment.getType();
// 用户角色为lv0的情况下,只有传参"0"给type字段,其他数字参数都报错
if (roleCodeSet.contains(AuthRoleConstant.ROLE_LV0) && !"0".equals(type)) {
throw new ConditionException("参数异常!");
}
// 用户角色为lv1的情况下,只有传参"0"或者"1"给type字段,其他数字参数都报错
if (roleCodeSet.contains(AuthRoleConstant.ROLE_LV1) && (!"1".equals(type) || !"0".equals(type))) {
throw new ConditionException("参数异常!");
}
}
}
}
}
UserMomentsApi.java
@RestController
public class UserMomentsApi {
@Autowired
private UserMomentsService userMomentsService;
@Autowired
private UserSupport userSupport;
/**
* 用户发布动态
* AuthRoleConstant.ROLE_LV0表示等级0是没有权限进行发布动态的
*
* @param userMoment
* @return
* @ApiLimitedRole 接口权限控制
* @DataLimited 数据权限控制
*/
@ApiLimitedRole(limitedRoleCodeList = {AuthRoleConstant.ROLE_LV0})
@DataLimited
@PostMapping("/user-moments")
public JsonResponse<String> addUserMoments(@RequestBody UserMoment userMoment) throws Exception {
Long userId = userSupport.getCurrentUserId();
userMoment.setUserId(userId);
userMomentsService.addUserMoments(userMoment);
return JsonResponse.success();
}
}
存在的问题
之前我们做的登录token模块,用户在退出登录之后,如果token还没有过期失效,那么这个时候用户还是可以拿着token去访问系统的资源,这样是不合理的。
用户退出之后,应该不能再访问系统的资源了。基于这种情况,我们引入双token机制。