为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 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
令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
方法授权要完成的是 资源服务
根据 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、当请求没有权限的方法时则拒绝访问
修改认证服务的 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
属性中为用户的权限。
1、资源服务添加权限控制
要想在资源服务使用方法授权,首先在 资源服务
配置授权控制,流程如下
1)添加 spring-cloud-starter-oauth2
依赖。
2)拷贝授权配置类 ResourceServerConfig
。
3)拷贝公钥。
2、方法上添加注解
通常情况下,程序员编写在资源服务的 controller
方法时会使用注解指定此方法的权限标识。
为什么不在 Service
或者 Dao
上定义?因为 Service 和 Dao的方法有可能是公用的,而 Controller
通常都是最外层的,所以不会涉及到被其他服务依赖的情况。
下面我们在 获取课程的图片
和 删除课程图
的接口中使用 @PreAuthorize
注解进行权限的设置,试下以下功能
而我们要注意的是,我们在前面的认证当中,只为用分配了 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)
重启课程管理服务,测试上边两个方法。
使用 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);
}
}
再次测试,结果如下:
基于方法授权步骤:
1、ResourceServerConfig
类上添加注解,如下:
//激活方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
2、在 Controller
为需要校验权限的方法上添加授权注解
@PreAuthorize("hasAuthority('权限名称')")
3、如果方法上不添加授权注解则表示此方法不需要权限即可访问。
截至目前在测试授权时使用的权限数据是静态数据,正常情况的流程是:
1、管理员给用户分配权限,权限数据写到数据库中。
2、认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)
本节实现动态权限数据。
打开 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",
"课程图片查询"
);
2)将该用户所属的某个角色与权限 A
关联起来
INSERT INTO xc_permission (id,role_id,menu_id) VALUES(
"8947692177635409931",
20,
"903459378655395851" # 权限A的ID
)
3)再次查询该用户对有的权限
从上图中可以看到,我们为用户所属的角色下添加了一个课程图片查询的权限,那么用户也会同时拥有了该权限。
认证服务请求用户中心查询用户信息,用户需要将用户基本信息和用户权限一同返回给认证服务。
本小节实现查询用户所拥有的权限,并将用户权限信息添加到的指定对象中返回给认证服务。
以上需求需要修改如下接口:
@GetMapping("/getuserext")
public XcUserExt getUserext(@RequestParam("username") String username);
在用户中心服务中编写 dao
,实现根据用户id查询权限。
1、定义 XcMenuMapper.java
在 com.xuecheng.ucenter.dao
包下定义:
@Mapper
public interface XcMenuMapper {
public List<XcMenu> selectPermissionByUserId(String userid);
}
2、XcMenuMapper.xml
在 resources
下创建 com.xuecheng.ucenter.dao
然后定义 XcMenuMapper.xml
这里要注意的一个点就是,在r esources 创建 com.xuecheng.ucenter.dao 时需要逐个包来创建,如果直接复制整串来创建,idea会将他识别为同一个目录,导致mapperxml文件无法被扫描到,例如下图
这里mapper.xml文件也可以存放的位置也可以是自定义的包名,但是要在启动类配置mapperscan
<mapper namespace="com.xuecheng.ucenter.dao.XcMenuMapper" >
<select id="selectPermissionByUserId"
resultType="com.xuecheng.framework.domain.ucenter.XcMenu" parameterType="java.lang.String" >
SELECT
id,
CODE,
p_id pId,
menu_name menuName,
url,
is_menu isMenu,
LEVEL,
sort,
STATUS,
icon,
create_time createTime,
update_time updateTiem
FROM
xc_menu
WHERE id IN(
SELECT menu_id FROM xc_permission WHERE role_id IN(
SELECT role_id FROM xc_user_role WHERE user_id = #{id}
)
)
select>
mapper>
其它 Dao 采用 spring data jpa
编写如下:
修改 UserService
的 getUserExt
方法,查询用户权限。
//根据账号查询用户的信息,返回用户扩展信息
public XcUserExt getUserExt(String username){
XcUser xcUser = this.findXcUserByUsername(username);
if(xcUser == null){
return null;
}
//根据用户id查询用户权限
String userId = xcUser.getId();
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(userId);
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
//用户的权限
xcUserExt.setPermissions(xcMenus);
return xcUserExt;
}
修改认证服务的 UserDetailServiceImpl
,查询用户的权限,并拼接权限串,将原来硬编码权限代码删除,代码如
下:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ClientDetailsService clientDetailsService;
//用户中心服务客户端
@Autowired
UserClient userClient;
@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则会抛出异常
//从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
String password = userext.getPassword();
//指定用户的权限,从数据库中获取
List<XcMenu> permissions = userext.getPermissions();
if(permissions == null) {
permissions = new ArrayList<>();
}
List<String> permissionsCode = new ArrayList<>();
//遍历权限对象中的code字段
permissions.forEach(item -> permissionsCode.add(item.getCode()));
//将权限串中间以逗号分隔
String user_permission_string = StringUtils.join(permissionsCode.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;
}
}
这里要注意的是,设置到 userDetails
对象的权限信息为权限的代码标识,也就是 UserJwt
对象的 code
字段,需要将所有的权限代码遍历出来然后拼接成字符串,如下代码
//指定用户的权限,从数据库中获取
List<XcMenu> permissions = userext.getPermissions();
List<String> permissionsCode = new ArrayList<>();
for (XcMenu xcMenu : permissions ){
String code = xcMenu.getCode();
permissionsCode.add(code);
}
//将权限串中间以逗号分隔
String user_permission_string = StringUtils.join(permissionsCode.toArray(), ",");
1、执行登录,在 redis
中查看 jwt
令牌,使用 jwt
测试程序解析 jwt
令牌中是否包括用户的权限 。
测试登录,拿到令牌
根据令牌,我们到redis里找到该令牌对应的 access_token
2、使用新的jwt令牌测试方法授权
成功的访问课程图片的接口
截至目前认证授权服务端的功能已基本完成,本章实现前端集成认证授权功能。
前端集成认证授权功能需要作如下工作:
1、前端页面校验用户的身份,如果用户没有登录则跳转到登录页面
2、前端请求资源服务需要在 http header
中添加 jwt
令牌,资源服务根据 jwt
令牌完成授权。
哪些功能需要前端请求时携带 JWT
?
用户登录成功请求资源服务都需要携带 jwt
令牌,因为资源服务已经实现了 jwt
认证,如果校验头部没有 jwt
则会认为身份不合法。
本节实现教学管理中心(xc-ui-pc-teach)实现身份校验,其它前端参考教学管理中心实现
教学管理前端访问微服务统一在访问地址前添加 /api
前缀并经过网关转发到微服务。
配置 teacher.xuecheng.com 虚拟主机。
#前端教学管理
upstream teacher_server_pool{
server 127.0.0.1:12000 weight=10;
}
#文件服务
upstream filesystem_server_pool{
server 127.0.0.1:22100 weight=10;
}
#媒资服务
upstream media_server_pool{
server 127.0.0.1:31400 weight=10;
}
# 学成网教学管理中心
server {
listen 80;
server_name teacher.xuecheng.com;
#个人中心
location / {
proxy_pass http://teacher_server_pool;
}
location /api {
proxy_pass http://api_server_pool;
}
location /filesystem {
proxy_pass http://filesystem_server_pool;
}
#媒资管理
location ^~ /api/media/ {
proxy_pass http://media_server_pool/media/;
}
#认证
location ^~ /openapi/auth/ {
proxy_pass http://auth_server_pool/auth/;
}
}
修改 hosts
文件 C:\Windows\System32\drivers\etc\hosts
127.0.0.1 teacher.xuecheng.com
教学管理中心(xc-ui-pc-teach)是单页面应用,我们在路由变化时校验用户的身份,校验失败将跳转到登录页面。
校验方法如下:
如果成功从 sessionStorage 和 cookie
获取当前用户则继续访问
如果 sessionStorage 中无当前用户,cookie 中有当前用户则请求服务端获取 jwt
,如果成功则继续访问。
以上两种情况都不满足则跳转到登录页面。
1、在 main.js
中添加路由监控代码,如下
router.beforeEach((to, from, next) => {
if(openAuthenticate){
// console.log(to)
// console.log(from)
//***********身份校验***************
let activeUser
let uid
try{
activeUser = utilApi.getActiveUser()
uid = utilApi.getCookie("uid")
}catch(e){
//alert(e)
}
if(activeUser && uid && uid == activeUser.uid) {
next();
}else if(to.path =='/login' || to.path =='/logout'){
next();
}else if(uid){
//请求获取jwt
systemApi.getjwt().then((res)=>{
if(res.success){
let jwt = res.jwt;
let activeUser = utilApi.getUserInfoFromJwt(jwt)
if(activeUser){
utilApi.setUserSession("activeUser",JSON.stringify(activeUser))
}
next();
}else{
//跳转到统一登陆
window.location = "http://ucenter.xuecheng.com/#/login?returnUrl="+Base64.encode(window.location)
}
})
}else{
//跳转到统一登陆
window.location = "http://ucenter.xuecheng.com/#/login?returnUrl="+Base64.encode(window.location)
}
}else{
next();
}
});
配置 xc-ui-pc-teach/config/sysConfig.js 开启认证授权的配置
var sysConfig = {
openAuthenticate: true,
openAuthorize: true
}
module.exports = sysConfig
2、在 base/api/system.js 中添加 getjwt
方法
/*获取jwt令牌*/
export const getjwt= () => {
return http.requestQuickGet('/openapi/auth/userjwt')
}
3、在utils.js中添加 如下方法
getActiveUser: function(){
let uid = this.getCookie("uid")
if(uid){
let activeUserStr = this.getUserSession("activeUser");
return JSON.parse(activeUserStr);
}else{
this.delUserSession("activeUser")
}
},
//获取jwt令牌
getJwt : function(){
let activeUser = this.getActiveUser()
if(activeUser){
return activeUser.jwt
}
},
//解析jwt令牌,获取用户信息
getUserInfoFromJwt : function (jwt) {
if(!jwt){
return ;
}
var jwtDecodeVal = jwtDecode(jwt);
if (!jwtDecodeVal) {
return ;
}
let activeUser={}
//console.log(jwtDecodeVal)
activeUser.utype = jwtDecodeVal.utype || '';
activeUser.username = jwtDecodeVal.name || '';
activeUser.userpic = jwtDecodeVal.userpic || '';
activeUser.userid = jwtDecodeVal.userid || '';
activeUser.authorities = jwtDecodeVal.authorities || '';
activeUser.uid = jwtDecodeVal.jti || '';
activeUser.jwt = jwt;
return activeUser;
},
4、测试
1)启动学习中心前端、教学管理前端、认证服务、用户中心服务、网关、Eureka
进入首页
点击“教学提供方”,此时由于没有登录自动跳转到登录页面
2)输入账号和密码登录
登录成功,跳转到教学管理页面
1、前端携带JWT请求
根据需求,在使用 axios
进行 http 请求前向 header
中加入 jwt
令牌
在 main.js
中添加
import axios from 'axios'
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求向header添加jwt
let jwt = utilApi.getJwt()
if(jwt){
config.headers['Authorization'] = 'Bearer '+jwt
}
return config;
}, function (error) {
return Promise.reject(error);
});
2、测试 http
请求是否携带jwt
进入教学管理中心,点击我的课程,观察 request header
中是否有 Authorization
信息
3、测试授权效果
当访问一个没有权限的方法时是否报错?
测试方法:
在课程计划查询方法上添加授权注解,表示当前用户需要拥有course_teachplan_list权限方可正常访问。
@PreAuthorize("hasAuthority('course_teachplan_list')")
@Override
public TeachplanNode findTeachplanList(@PathVariable("courseId") String courseId) {
return courseService.findTeachplanList(courseId);
}
进入我的课程,点击课程计划,观察响应结果为 10002错误。
4、提示权限不足
当权限不足首页要给出提示,实现思路是使用axios的拦截,在执行后校验响应结果,如果是10002
代码的错误则提示用户 ```“权限不足”,如果是
10001` 代码则强制登录。
在 main.js
中添加
// 响应拦截
axios.interceptors.response.use(data => {
console.log("data=",data)
if(data && data.data){
if(data.data.code && data.data.code =='10001'){
//需要登录
// router.push({
// path: '/login',
// query: {returnUrl: Base64.encode(window.location)}
// })
window.location = "http://ucenter.xuecheng.com/#/login?returnUrl="+ Base64.encode(window.location)
}else if(data.data.code && data.data.code =='10002'){
Message.error('您没有权限操作该选项');
return
}else if(data.data.code && data.data.code =='10003'){
Message.error('认证被拒绝,请重新登录重试!');
return
}
}
return data
})
测试:
执行一个没有权限的操作,提示如下
用户前端是如何解密JWT令牌的? 公钥是否会暴露在前端?
什么是细粒度授权?
细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。
一个例子:
用户 A
和 用户 B
都是教学机构,他们都拥有 “我的课程” 权限,但是两个用户所查询到的数据是不一样的。
本项目有哪些细粒度授权?
比如:
我的课程,教学机构只允许查询本教学机构下的课程信息。
我的选课,学生只允许查询自己所选课。
如何实现细粒度授权?
细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的数据或操作不同的数据。
1、我的课程查询,细粒度授权过程如下:
获取当前登录的用户Id
得到用户所属教育机构的Id
查询该教学机构下的课程信息
最终实现了用户只允许查询自己机构的课程信息。
2、修改课程管理服务 “我的课程
” 的功能,根据 公司 Id
查询课程,思路如下:
修改Dao,支持根据公司Id 查询课程。
修改Service,将公司Id传入Dao。
修改Controller,获取当前用户的公司Id,传给Service。
3、数据模型分析如下:
1)课程表
在 xc_course
数据库的 course_base
表中添加 company_id
字段,来表示此课程的归属
2)用户企业表
在 xc_user
数据库的 xc_company_user
表中记录了用户的归属公司信息
通过 xc_company_user 表可得到 用户 的所属公司 Id。
如何查询某个用户的课程?
1、确定用户的 Id
2、根据用户的 Id
查询用户归属的公司。
3、根据 公司Id
查询该公司下的课程信息
一个例子:
/*确定用户的id:49*/
/*根据用户Id查找所属公司*/
/*根据公司查询所拥有的课程*/
SELECT * FROM xc_course.course_base WHERE company_id IN (
SELECT company_id FROM xc_user.xc_company_user WHERE user_id = '49'
)
定义我的课程查询接口如下:
@ApiOperation("查询指定公司下的所有课程")
public QueryResponseResult<CourseInfo> findCourseListByCompany(
int page,
int size,
CourseListRequest courseListRequest
);
在 CourseMapper 下新增一个 findCourseListByCompany
方法
@Mapper
public interface CourseMapper {
//根据课程id查询课程信息
CourseBase findCourseBaseById(String id);
/**
* 分页查询课程数据
* @param courseListRequest 查询条件
* @return
*/
Page<CourseBase> findCourseList(CourseListRequest courseListRequest);
/**
* 分页查询指定公司下的课程数据
* @param courseListRequest 查询条件
* @return
*/
Page<CourseInfo> findCourseListByCompany(CourseListRequest courseListRequest);
}
修改 CourseMapper.xml
的查询课程列表,添加 companyId
条件。
<select id="findCourseListByCompany" resultType="com.xuecheng.framework.domain.course.ext.CourseInfo"
parameterType="com.xuecheng.framework.domain.course.request.CourseListRequest">
SELECT
course_base.*,
(SELECT pic FROM course_pic WHERE courseid = course_base.id) pic
FROM
course_base
where 1=1
<if test="companyId!=null and companyId!=''">
and course_base.company_id = #{companyId}
if>
select>
修改 CourseService 的 findCourseList
方法,添加 companyId
参数,并且传给 dao.
/**
* 分页查询指定公司下的课程信息
*/
public QueryResponseResult findCourseListByCompany(String companyId ,int pageNum, int size, CourseListRequest courseListRequest){
if(pageNum<=0){
pageNum = 0;
}
if(size<=0){
size = 20;
}
PageHelper.startPage(pageNum,size); //设置分页参数
//设置公司信息到查询条件内
courseListRequest.setCompanyId(companyId);
Page<CourseInfo> courseList = courseMapper.findCourseListByCompany(courseListRequest);
QueryResult queryResult = new QueryResult();
queryResult.setList(courseList.getResult());
queryResult.setTotal(courseList.getTotal());
return new QueryResponseResult(CommonCode.SUCCESS,queryResult);
}
修改 CourseController 的 findCourseList
,向 service 传入 companyId
这里先使用静态数据测试使用。
/**
* 查询指定公司下的所有课程
* @param page 页码
* @param size 数量
* @param courseListRequest 查询参数
* @return QueryResponseResult
*/
@GetMapping("/coursebase/company/list/{page}/{size}")
@Override
public QueryResponseResult findCourseListByCompany(
@PathVariable("page") int page,
@PathVariable("size") int size,
CourseListRequest courseListRequest
){
String companyId = "1";
return courseService.findCourseListByCompany(companyId, page, size, courseListRequest);
}
修改 xc-ui-pc-teach/src/module/course/api/course.js
中查询课程的API 与我们前面构建的 controller 对应
//我的课程列表
export const findCourseList = (page,size,params) => {
//使用工具类将json对象转成key/value
let queries = querystring.stringify(params)
return http.requestQuickGet(apiUrl+"/course/coursebase/company/list/"+page+"/"+size+"?"+queries)
}
进入我的课程,查看数据是否正确。
由于我们在 controller
中暂时将 company
的固定的写为了1,所以预期的结果应该是查询到 company 为1的所有课程,测试结果如下。
测试结果如下,得到了预期的结果
要想实现只查询自己的课程信息,则需要获取当前用户所属的企业id。
1、认证服务在用户认证通过将用户所属公司id等信息存储到jwt令牌中。
2、用户请求到达资源服务后,资源服务需要取出header中的jwt令牌,并解析出用户信息。
资源服务在授权时需要用到用户所属企业 ID
,需要实现认证服务生成的JWT令牌中包括用户所属公司 id
信息。
查看认证服务 UserDetailServiceImpl
代码如下:
......
//用户id
userDetails.setId(userext.getId());
//用户名称
userDetails.setName(userext.getName());
//用户头像
userDetails.setUserpic(userext.getUserpic());
//用户类型
userDetails.setUtype(userext.getUtype());
//用户所属企业id
userDetails.setCompanyId(userext.getCompanyId());
return userDetails;
......
通过上边代码的分析得知,认证服务调用远程调用 ucenter 服务的 getUserext
接口获取用户信息,并将 userext
中的信息存储到jwt令牌中,所以在在 getUserext 接口中返回的 userext
对象中需要包括了 companyId
公司ID等信息 。
getUserExt 的代码如下
/**
* 根据用户名获取用户权限的实现
* @param username 用户名
* @return
*/
@Override
public XcUserExt getUserExt(String username) {
//查询用户信息
XcUser xcUser = this.findXcUserByUsername(username);
if(xcUser ==null) return null;
XcUserExt xcUserExt = new XcUserExt();
BeanUtils.copyProperties(xcUser,xcUserExt);
//根据用户id查询用所属公司
String xcUserId = xcUser.getId();
XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findByUserId(xcUserId);
if(xcCompanyUser!=null){
String companyId = xcCompanyUser.getCompanyId();
xcUserExt.setCompanyId(companyId);
}
//获取用户的所有权限
List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(xcUserId);
xcUserExt.setPermissions(xcMenus);
//返回XcUserExt对象
return xcUserExt;
}
getUserExt
是在用户进行认证时被调用的,由 ucenter-auth
服务向 ucenter
服务调用该接口,获取用户的信息,然后将得到的这些信息 打包为 UserJwt
对象交由 Spring Security 进行校验,校验通过后会将该JWT令牌到认证服务中,校验 Spring Security 返回的 JWT
令牌完整性后写入到 redis。
1、在 Oauth2Util
工具类中,从 header 中取出JWT令牌,并解析 JWT
令牌的内容。
public class Oauth2Util {
public static Map<String,String> getJwtClaimsFromHeader(HttpServletRequest request) {
if (request == null) {
return null;
}
//取出头信息
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization) || authorization.indexOf("Bearer") < 0) {
return null;
}
//从Bearer后边开始取出token
String token = authorization.substring(7);
Map<String,String> map = null;
try {
//解析jwt
Jwt decode = JwtHelper.decode(token);
//得到 jwt中的用户信息
String claims = decode.getClaims();
//将jwt转为Map
map = JSON.parseObject(claims, Map.class);
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
2、在 XcOauth2Util
工具类中,将解析的 JWT
内容封装成 UserJwt
对象返回。
public class XcOauth2Util {
public UserJwt getUserJwtFromHeader(HttpServletRequest request){
Map<String, String> jwtClaims = Oauth2Util.getJwtClaimsFromHeader(request);
if(jwtClaims == null || StringUtils.isEmpty(jwtClaims.get("id"))){
return null;
}
UserJwt userJwt = new UserJwt();
userJwt.setId(jwtClaims.get("id"));
userJwt.setName(jwtClaims.get("name"));
userJwt.setCompanyId(jwtClaims.get("companyId"));
userJwt.setUtype(jwtClaims.get("utype"));
userJwt.setUserpic(jwtClaims.get("userpic"));
return userJwt;
}
@Data
public class UserJwt{
private String id;
private String name;
private String userpic;
private String utype;
private String companyId;
}
}
修改课程管理的 CourseController 类,将 companyId
的静态数据改为动态获取:
配置 CourseController
继承 BaseController
,这样我们就可以拿到一个用户请求时的 request 对象
public class CourseContorller extends BaseController implements CourseControllerApi {
...
}
在 findCourseListByCompany
中调用前面我们定义的工具类,从用户的 header
信息中取出 JWT 令牌并且解析出用户的信息,拿到用户所属的公司 ID
,实现细粒度的课程信息获取。
/**
* 查询指定公司下的所有课程
* @param page 页码
* @param size 数量
* @param courseListRequest 查询参数
* @return QueryResponseResult
*/
@GetMapping("/coursebase/company/list/{page}/{size}")
@Override
public QueryResponseResult findCourseListByCompany(
@PathVariable("page") int page,
@PathVariable("size") int size,
CourseListRequest courseListRequest
){
//调用工具类取出用户信息
XcOauth2Util xcOauth2Util = new XcOauth2Util();
//从用户header中附带的jwt令牌取出用户信息
XcOauth2Util.UserJwt userJwt = xcOauth2Util.getUserJwtFromHeader(request);
//从用户信息获取该用户所属的公司id,根据该公司id查询该公司下的所有课程
String companyId = userJwt.getCompanyId();
return courseService.findCourseListByCompany(companyId, page, size, courseListRequest);
}
使用不同的用户登录系统,测试细粒度权限控制效果。
预期结果:每个用户只查询自己所拥有的课程。
公司1的用户
公司2的用户
前边章节已经实现了用户携带身份令牌(JTI) 和 JWT
(access_token) 令牌访问微服务,微服务获取 jwt
并完成授权。当微服务访问微服务,此时如果没有携带 JWT
则微服务会在授权时报错。
测试课程预览:
1、将课程管理服务和CMS全部添加授权配置
2、用户登录教学管理前端,进入课程发布界面,点击课程发布,观察课程管理服务端报错如下:
feign.FeignException: status 401 reading CmsPageClient#save(CmsPage); content:
{"error":"unauthorized","error_description":"Full authentication is required to access thisresource"}
分析原因:
由于课程管理访问 CMS
时没有携带 JWT
令牌导致。
解决方案:
微服务之间进行调用时需携带 JWT
。
微服务之间使用 feign
进行远程调用,采用 feign
拦截器实现远程调用携带 JWT。
在 common 工程添加依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring‐cloud‐starter‐openfeignartifactId>
dependency>
在 Common
工程定义拦截器如下:
package com.xuecheng.framework.interceptor;
public class FeignClientInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
try {
//使用RequestContextHolder工具获取request相关变量
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if(attributes!=null){
//取出request
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
if(name.equals("authorization")){
//System.out.println("name="+name+"values="+values);
requestTemplate.header(name, values);
}
}
}
}
}catch (Exception e) {
e.printStackTace();
}
}
}
本例子中课程管理调用 cms
需要携带 jwt
,所以需要在课程管理中定义 Feign
拦截器 bean
,在启动类中定义 bean
如下:
@Bean
public FeignClientInterceptor feignClientInterceptor(){
return new FeignClientInterceptor();
}
在发布课程时,cms服务使用 restTeamlate 向数据模型URL发送请求获取数据,该操作涉及到调用课程管理服务的接口,由于课程管理服务开启了接口认证,所有没附带 JWT 令牌的请求都会被拒绝,如下图所示
在课程管理服务中放行该URL的认证拦截,通常该接口是可以不需要认证的。
在课程管理服务的 ResourceServerConfig
中配置 configure
方法,放行 /course/preview/model
@Value("${oauth2.urlMatchers}")
String urlMatchers;
//Http安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
if(urlMatchers.equals("")){
//如果urlMatchers未指定,则所有url都需要授权后才能被访问
http.authorizeRequests().anyRequest().authenticated();
}else{
//放行 urlMatchers 中指定的url条目, 未指定的url仍需授权后才能访问
if(urlMatchers != null){
String[] split = urlMatchers.split(",");
http.authorizeRequests()
//下边的路径放行
.antMatchers(split).permitAll()
.anyRequest().authenticated();
}
}
}
我这里需要放行的URL是从 appliaction.yml 中将 oauth2.urlMatchers
注入到变量 urlMatchers
内,内容如下
oauth2:
urlMatchers: /v2/api-docs,/swagger-resources/configuration/ui,/swagger-resources,/swagger-resources/configuration/security,/swagger-ui.html,/webjars/**,/course/coursepic/get/*
获取当前请求的 request
对象,从该对象中取出当前请求中 header
信息里面包含的 authorization
字段,该字段内带有了我们认证需要的 JWT
令牌信息。
//从dataUrl中获取页面模型数据
private Map getModelByPageId(String pageId){
//查询页面信息
CmsPageResult cmsPageResult = this.cmsPageQueryById(pageId);
CmsPage cmsPage = cmsPageResult.getCmsPage();
//页面不存在
if(cmsPage == null){
ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
}
//取出dataUrl
String dataUrl = cmsPage.getDataUrl();
if(StringUtils.isEmpty(dataUrl)){
ExceptionCast.cast(CmsCode.CMS_GENRATEHTML_DATAURL_IS_NULL);
}
//通过获取当前请求的request对象来取出jwt认证信息,并且传递到下一个请求中
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String jwt = request.getHeader("authorization");
//使用LinkedMultiValueMap储存多个header信息
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("authorization",jwt);
HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(headers);
//发送请求获取模型数据
ResponseEntity<Map> forEntity = restTemplate.exchange(dataUrl, HttpMethod.GET,httpEntity,Map.class);
Map body = forEntity.getBody();
return body;
}
将 CMS
服务中调用 课程管理服务
的数据模型接口更改为使用 Feign
来进行远程调用,再结合前面配置的 Feign
拦截器实现 Header 信息的向下传递。
由于数据模型接口需要的参数是课程ID,但CmsPage对象中没有明确的给出该页面对应的课程ID,但是我们可以看到,页面的名称其实就是以课程ID所命名的,如下图
所以我们可以从 pageName
中取出课程ID进行远程调用,代码如下
String[] pageNameSplit = cmsPageName.split("\\.");
String courseId = pageNameSplit[0];
这里要注意如果以
.
对字符串进行分割,需要在点号前面加两个斜杠,如\\.
在之前的代码中,使用 RestTemplate
访问的数据模型接口返回的是一个 map
类型的数据,而如果采用远程调用的方式,拿到的是一个 CourseView
对象,这里我们可以使用 JSONObject.toJSONString
将对象转为字符串,再使用 parseObject
将JSON形式的字符串转换为 Map
对象,代码如下
JSONObject.parseObject(JSONObject.toJSONString(courseView), Map.class)
修改后的全部代码如下
//从dataUrl中获取页面模型数据
private Map getModelByPageId(String pageId){
//查询页面信息
CmsPageResult cmsPageResult = this.cmsPageQueryById(pageId);
CmsPage cmsPage = cmsPageResult.getCmsPage();
//页面不存在
if(cmsPage == null){
ExceptionCast.cast(CmsCode.CMS_PAGE_NOT_EXISTS);
}
String cmsPageName = cmsPage.getPageName();
if(cmsPageName == null){
ExceptionCast.cast(CmsCode.CMS_PAGE_NAME_NOT_EXISTS);
}
String[] pageNameSplit = cmsPageName.split("\\.");
String courseId = pageNameSplit[0];
CourseView courseView = courseManageClient.courseView(courseId);
Map body = JSONObject.parseObject(JSONObject.toJSONString(courseView), Map.class);
// ResponseEntity
// Map body = forEntity.getBody();
// courseManageClient.courseView(cmsPage.)
return body;
}
执行课程发布,提示发布成功。
1、JWT时间目前是由 redis
来进行控制,那么 jwt
令牌的实际过期时间是多久? 如何获取或者设置?
2、生成JWT的公钥和私钥都有哪些作用?
公钥:用于校验JWT令牌是否完整,以及解密JWT令牌中的用户信息
私钥:生成加密后的JWT令牌
作者: LCyee ,全干型代码
自建博客:https://www.codeyee.com
记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。
CSDN 博客:https://blog.csdn.net/codeyee
记录和分享一些开发过程中遇到的问题以及解决的思路。
欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。