😎 知识点概览
为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day18
的内容
- 基于方法的权限校验
- 基于
RBAC
进行用户权限配置以及动态查询。 - 根据教师所属的公司来实现课程信息查询的细粒度授权。也就是
A
公司的老师只能查询到A
公司下的课程。 - 使用
Feign
拦截器实现获取前端请求中的 header 信息,并将header
中带有的 jwt 令牌向下传递,实现微服务间的远程调用的认证授权。
目录
内容会比较多,可以根据目录进行按需查阅。
文章目录
一、用户授权业务流程
用户授权的业务流程如下:
业务流程说明如下:
1、用户认证通过,认证服务向浏览器 cookie
写入 token
( 身份令牌)
2、前端携带 token
请求用户中心服务获取jwt令牌,前端获取到jwt令牌解析,并存储在sessionStorage
3、前端携带 cookie
中的身份令牌及jwt令牌访问资源服务
前端请求资源服务需要携带两个 token
,一个是 cookie
中的身份令牌,一个是 http header
中的 jwt
,前端请求资源服务前在 http header
上添加 jwt
请求资源
4、网关校验 token
的合法性
用户请求必须携带身份令牌和jwt令牌。
网关校验 redis
中 user_token
的有效期,已过期则要求用户重新登录。
5、资源服务校验 jwt
的合法性并进行授权
资源服务校验 jwt
令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
二、基于方法授权
0x01 需求分析
方法授权要完成的是 资源服务
根据 jwt 令牌完成对方法的授权,具体流程如下:
1、生成 Jwt
令牌时在令牌中写入用户所拥有的权限
我们给每个权限起个名字,例如某个用户拥有如下权限:
course_find_list:课程查询
course_pic_list:课程图片查询
2、在资源服务方法上添加注解 @PreAuthorize
,并指定此方法所需要的权限
@PreAuthorize 注解是由Spring Security 提供的一个权限校验注解
例如下边是课程管理接口方法的授权配置,它就表示要执行这个方法需要拥有 course_find_list
权限。
@PreAuthorize("hasAuthority('course_find_list')")
@Override
public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page,
@PathVariable("size") int size,
CourseListRequest courseListRequest)
3、当请求有权限的方法时正常访问
4、当请求没有权限的方法时则拒绝访问
0x02 jwt令牌包含权限
修改认证服务的 UserDetailServiceImpl
类,下边的代码中 permissionList
列表中存放了用户的权限,并且将权限标识按照中间使用逗号分隔的语法组成一个字符串,最终提供给 Spring security
核心的代码如下
//指定用户的权限,这里暂时硬编码
List<String> permissionList = new ArrayList<>();
permissionList.add("course_base_list");
permissionList.add("course_pic_list");
//将权限串中间以逗号分隔
String user_permission_string = StringUtils.join(permissionList.toArray(), ",");
//设置用户信息到userDetails对象
UserJwt userDetails = new UserJwt(
username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
在上述的代码当中,通过向 permissionList
添加标识来对用户的进行授权,这里我们暂时对用户的权限的内容进行硬编码,后面的章节中用户的权限信息会从数据库中获取。
全部代码如下
@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,clientSecret,AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
if (StringUtils.isEmpty(username)) {
return null;
}
//请求ucenter查询用户
XcUserExt userext = userClient.getUserext(username);
if(userext == null) return null; //如果获取到的用信息为空,则返回null,spring security则会抛出异常
//设置用户的认证和权限信息
userext.setUsername("itcast");
userext.setPassword(new BCryptPasswordEncoder().encode("123"));
userext.setPermissions(new ArrayList<XcMenu>()); //这里授权部分还没完成,所以先填写静态的
if(userext == null){
return null;
}
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
//指定用户的权限,这里暂时硬编码
List<String> permissionList = new ArrayList<>();
permissionList.add("course_base_list");
permissionList.add("course_pic_list");
//将权限串中间以逗号分隔
String user_permission_string = StringUtils.join(permissionList.toArray(), ",");
//设置用户信息到userDetails对象
UserJwt userDetails = new UserJwt(
username,
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
//用户id
userDetails.setId(userext.getId());
//用户名称
userDetails.setName(userext.getName());
//用户头像
userDetails.setUserpic(userext.getUserpic());
//用户所属企业id
userDetails.setCompanyId(userext.getCompanyId());
//返回用信息给到Spring Security进行处理
return userDetails;
}
重启认证服务工程,使用 postman
完成登录,获取到用户令牌
从 redis
中找到该用户令牌对应的 jwt
令牌。
使用 jwt
的测试程序查看 此令牌的内容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ToOi66ex-1595567460983)(https://qnoss.codeyee.com/20200704_MTg=/image4.png)]
可以看到 authorities
属性中为用户的权限。
0x03 方法授权实现
1、资源服务添加权限控制
要想在资源服务使用方法授权,首先在 资源服务
配置授权控制,流程如下
1)添加 spring-cloud-starter-oauth2
依赖。
2)拷贝授权配置类 ResourceServerConfig
。
3)拷贝公钥。
2、方法上添加注解
通常情况下,程序员编写在资源服务的 controller
方法时会使用注解指定此方法的权限标识。
为什么不在 Service
或者 Dao
上定义?因为 Service 和 Dao的方法有可能是公用的,而 Controller
通常都是最外层的,所以不会涉及到被其他服务依赖的情况。
下面我们在 获取课程的图片
和 删除课程图
的接口中使用 @PreAuthorize
注解进行权限的设置,试下以下功能
- 访问 getCoursePic 需要授权 course_pic_list 权限
- 访问 deleteCoursePic 需要授权 course_pic_delete 权限
而我们要注意的是,我们在前面的认证当中,只为用分配了 course_pic_list 的权限,配置完后我们来进行测试。
/**
* 根据课程id获取该课程的课程图片信息
* @param courseId
* @return 由于这里每个课程只有一个图片,所以只返回一个 CoursePic 对象
*/
@PreAuthorize("hasAuthority('course_pic_list')")
@Override
@GetMapping("/coursepic/get/{courseId}")
public CoursePic getCoursePic(@PathVariable("courseId") String courseId) {
return courseService.getCoursePic(courseId);
}
/**
* 删除课程图片信息
* @param courseId
* @return
*/
@PreAuthorize("hasAuthority('course_pic_delete')")
@Override
@DeleteMapping("/coursepic/delete")
public ResponseResult deleteCoursePic(@RequestParam("courseId") String courseId) {
return courseService.deleteCoursePic(courseId);
}
3、在资源服务(这里是课程管理)的 ResourceServerConfig
类上添加注解,激活方法上添加授权注解
//激活方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
0x04 方法授权测试
重启课程管理服务,测试上边两个方法。
使用 postman
测试,测试前执行登录,并且将 jwt
令牌(access_token)添加到 header
。
发送GET请求到以下链接
http://www.xuecheng.com/api/course/coursepic/get/4028e58161bd22e60161bd23672a0001
1、用户拥有 course_pic_list
权限,可以正常访问获取课程图片的接口 。
2、下面我们来测试删除课程图片的接口
发送给 DELETE
请求到 http://www.xuecheng.com/api/course/coursepic/delete?courseId=4028e58161bd22e60161bd23672a0001
由于用户没有查询课程列表方法的权限,所以无法正常访问,其它方法可以正常访问。
控制台报错:
org.springframework.security.access.AccessDeniedException: 不允许访问
说明:如果方法上没有添加授权注解 spring security
将不进行授权控制,只要 jwt
令牌合法则可以正常访问
3、异常处理
上边当没有权限访问时资源服务,应该返回下边的错误代码:
UNAUTHORISE(false,10002,"权限不足,无权操作!")
进入资源服务(我们测试的是课程管理),新建一个 exception
包,在包下创建一个 CustomExceptionCatch ,并继承 common
工程中的 ExceptionCatch 。
添加异常类 AccessDeniedException.class
与错误代码 10002 的 对应关系,使用 @ControllerAdvice
注解添加一个全局的异常处理,并继承我们在 common
工程中定义的 ExceptionCatch
,使用 static {} 向 builder
里面添加自定义的异常处理代码。
package com.xuecheng.manage_course.exception;
import com.xuecheng.framework.exception.ExceptionCatch;
import com.xuecheng.framework.model.response.CommonCode;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ControllerAdvice
public class CustomExceptionCatch extends ExceptionCatch {
static {
//除了CustomException以外的异常类型及对应的错误代码在这里定义,,如果不定义则统一返回固定的错误信息
builder.put(AccessDeniedException.class, CommonCode.UNAUTHORISE);
}
}
再次测试,结果如下:
0x05 小结
基于方法授权步骤:
1、ResourceServerConfig
类上添加注解,如下:
//激活方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
2、在 Controller
为需要校验权限的方法上添加授权注解
@PreAuthorize("hasAuthority('权限名称')")
3、如果方法上不添加授权注解则表示此方法不需要权限即可访问。
三、动态查询用户权限
0x01 需求分析
截至目前在测试授权时使用的权限数据是静态数据,正常情况的流程是:
1、管理员给用户分配权限,权限数据写到数据库中。
2、认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)
本节实现动态权限数据。
0x02 权限数据模型
数据模型结构
打开 xc_user
数据库,找到下边的表:
xc_user:用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等
xc_role:角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等。
xc_user_role:用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有
xc_menu: 模块表,记录了菜单及菜单下的权限
xc_permission: 角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有
xc_permission 表可以更名为 xc_permission_role 或者 xc_menu_role 会容易理解
数据模型的使用
本项目教学阶段不再实现权限定义及用户权限分配的功能,但是基于权限数据模型(5张数据表)及现有数据,要求学生在数据库中操作完成给用户分配权限、查询用户权限等需求。
1、查询用户所拥有的权限
步骤:
-
确定用户的id
-
查询用户所拥有的角色
-
查询用户所属的 角色 所拥有的权限
例子:
# 根据查到的权限ID(menu_id)查询所对应的权限的详细信息
SELECT * FROM xc_menu WHERE id IN(
# 根据用户角色ID取出该角色所拥有的权限
SELECT menu_id FROM xc_permission WHERE role_id IN(
# 获取指定用户所拥有角色的id
SELECT role_id FROM xc_user_role where user_id = 49
)
)
2、向已拥有角色分配权限
1)新增一个 权限 A
INSERT INTO xc_menu (id,code,p_id,menu_name) VALUES (
"903459378655395851", # 权限A的ID
"course_pic_list",
"903459378655395841",
"课程图片查询"