• 学成在线(第18天)用户授权


     用户授权业务流程

    用户授权的业务流程如下:

     业务流程说明如下:

    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,并指定此方法所需要的权限
    例如下边是课程管理接口方法的授权配置,它就表示要执行这个方法需要拥有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、当请求没有权限的方法时则拒绝访问

    方法授权实现

    资源服务添加授权控制

    1、要想在资源服务使用方法授权,首先在资源服务配置授权控制
    1)添加spring-cloud-starter-oauth2依赖。
    2)拷贝授权配置类ResourceServerConfig。
    3)拷贝公钥。

    方法上添加注解

    通常情况下,程序员编写在资源服务的controller方法时会使用注解指定此方法的权限标识。
    1、查询课程列表方法
    指定查询课程列表方法需要拥有course_find_list权限。

    @PreAuthorize("hasAuthority('course_find_list')")
    @Override
    public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page,
                                                  @PathVariable("size") int size,
                                                  CourseListRequest courseListRequest)

    2、查看课程基本信息方法
    指定查询课程基本信息方法需要拥有course_get_baseinfo权限。

    @PreAuthorize("hasAuthority('course_get_baseinfo')")
    @Override
    public CourseBase getCourseBaseById(@PathVariable("courseId") String courseId)

    3、在资源服务(这里是课程管理)的ResourceServerConfig类上添加注解,激活方法上添加授权注解

    //激活方法上的PreAuthorize注解
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

    方法授权测试

    Get请求 http://www.xuecheng.com/api/course/coursebase/list/1/2

    由于用户没有查询课程列表方法的权限,所以无法正常访问,其它方法可以正常访问。

    控制台报错:
    org.springframework.security.access.AccessDeniedException: 不允许访问
    说明:如果方法上没有添加授权注解spring security将不进行授权控制,只要jwt令牌合法则可以正常访问。

    控制台报错:
    org.springframework.security.access.AccessDeniedException: 不允许访问
    说明:如果方法上没有添加授权注解spring security将不进行授权控制,只要jwt令牌合法则可以正常访问。

     

    异常处理
    上边当没有权限访问时资源服务应该返回下边的错误代码:

    UNAUTHORISE(false,10002,"权限不足,无权操作!")

    进入资源服务(这里是课程管理),添加异常类AccessDeniedException.class与错误代码 10002 的 对应关系

    @ControllerAdvice
    public class CustomExceptionCatch extends ExceptionCatch {
        static {
         //除了CustomException以外的异常类型及对应的错误代码在这里定义,,如果不定义则统一返回固定的错误信息
            builder.put(AccessDeniedException.class, CommonCode.UNAUTHORISE);
          
        }
    }

    再次测试,结果如下:

      小结

    方法授权步骤:
    1、ResourceServerConfig类上添加注解,如下:

    //激活方法上的PreAuthorize注解
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

    2、在方法添加授权注解

    @PreAuthorize("hasAuthority('???')")

    3、如果方法上不添加授权注解表示此方法不需要权限即可访问。

     动态查询用户权限

     需求分析

    截至目前在测试授权时使用的权限数据是静态数据,正常情况的流程是:
    1、管理员给用户分配权限,权限数据写到数据库中。
    2、认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)
    本节实现动态权限数据。

     权限数据模型

    数据模型结构

    打开xc_user数据库,找到下边的表:

    xc_user:用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等
    xc_role:角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等。
    xc_user_role:用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有
    xc_menu:模块表,记录了菜单及菜单下的权限
    xc_permission:角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有

    数据模型的使用

    本项目教学阶段不再实现权限定义及用户权限分配的功能,但是基于权限数据模型(5张数据表)及现有数据,要
    求学生在数据库中操作完成给用户分配权限、查询用户权限等需求。
    1、查询用户所拥有的权限

    2 、给用户分配权限
    1)向已拥有角色分配权限 步骤:
    确定用户的id
    确定权限的id
    确定用户的角色
    向角色权限表添加记录
    2)添加角色给用户分配权限 步骤:
    确定用户的id
    确定权限的id
    添加角色
    向角色权限表添加记录
    向用户角色关系表添加记录

     用户中心查询用户权限

    需求分析

    认证服务请求用户中心查询用户信息,用户需要将用户基本信息和用户权限一同返回给认证服务。
    本小节实现用户查询查询用户权限,并将用户权限信息添加到的用户信息中返回给认证服务。
    以上需求需要修改如下接口:

    @GetMapping("/getuserext")
    public XcUserExt getUserext(@RequestParam("username") String username);

    DAO

    在用户中心服务中编写dao,实现根据用户id查询权限。
    1、定义XcMenuMapper.java
    在com.xuecheng.ucenter.dao包下定义:

    @Mapper
    public interface XcMenuMapper {
        public List<XcMenu> selectPermissionByUserId(String userid);
    }

    2、XcMenuMapper.xml
    在com.xuecheng.ucenter.dao下定义XcMenuMapper.xml

    <?xml version="1.0" encoding="UTF‐8" ?>
    <!DOCTYPE mapper PUBLIC "‐//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis‐3‐
    mapper.dtd" >
    <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>

    Service

    修改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,查询用户的权限,并拼接权限串,将原来硬编码权限代码删除,代码如
    下:

    ......
           //请求ucenter查询用户
     XcUserExt userext = userClient.getUserext(username);
            if(userext == null){
                //返回NULL表示用户不存在,Spring Security会抛出异常
                return null;
            }
            //从数据库查询用户正确的密码,Spring Security会去比对输入密码的正确性
            String password = userext.getPassword();
            //指定用户的权限,这里暂时硬编码
            List<String> permissionList = new ArrayList<>();
    //        permissionList.add("course_get_baseinfo");
    //        permissionList.add("course_find_pic");
            //取出用户权限
            List<XcMenu> permissions = userext.getPermissions();
            for(XcMenu xcMenu:permissions){
                permissionList.add(xcMenu.getCode());
            }
            ......

     细粒度授权

    需求分析

    什么是细粒度授权?
    细粒度授权也叫数据范围授权,即不同的用户所拥有的操作权限相同,但是能够操作的数据范围是不一样的。一个
    例子:用户A和用户B都是教学机构,他们都拥有“我的课程”权限,但是两个用户所查询到的数据是不一样的。
    本项目有哪些细粒度授权?

    比如:
    我的课程,教学机构只允许查询本教学机构下的课程信息。
    我的选课,学生只允许查询自己所选课。
    如何实现细粒度授权?
    细粒度授权涉及到不同的业务逻辑,通常在service层实现,根据不同的用户进行校验,根据不同的参数查询不同的
    数据或操作不同的数据。

    我的课程细粒度授权

    需求分析

    1、我的课程查询,细粒度授权过程如下:
    1)获取当前登录的用户Id
    2)得到用户所属教育机构的Id
    3)查询该教学机构下的课程信息

    最终实现了用户只允许查询自己机构的课程信息。
    2、修改课程管理服务“我的课程”的功能,根据公司Id查询课程,思路如下:
    1)修改Dao,支持根据公司Id 查询课程。
    2)修改Service,将公司Id传入Dao。
    3)修改Controller,获取当前用户的公司Id,传给Service。

    如何查询某个用户的课程?
    1、确定用户的Id
    2、根据用户的Id查询用户归属的公司。
    3、根据公司Id查询该公司下的课程信息

    一个例子:

    /*确定用户的id:49*/
    /*根据用户Id查找所属公司*/
    SELECT  company_id FROM xc_user.xc_company_user WHERE user_id = '49'
    /*根据公司查询所拥有的课程*/
    SELECT * FROM  xc_course.course_base WHERE company_id = '1'

    Api

    定义我的课程查询接口如下:

    public QueryResponseResult<CourseInfo> findCourseList(int page,
                          int size,
                          CourseListRequest courseListRequest);

    Dao

    修改 CourseMapper.xml的查询课程列表,添加companyId条件。

    <select id="findCourseListPage" 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>

    Service

    修改CourseService的findCourseList方法,添加companyId参数,并且传给dao.

    public QueryResult<CourseInfo> findCourseList(String companyId,int page,int
    size,CourseListRequest courseListRequest) {
        if(courseListRequest == null){
            courseListRequest new CourseListRequest();
        }
        //企业id
        courseListRequest.setCompanyId(companyId);
        //将companyId传给dao
        courseListRequest.setCompanyId(companyId);
        if(page<=0){
            page = 0;
        }
        if(size<=0){
            size = 20;
    }
        PageHelper.startPage(page, size);
        Page<CourseInfo> courseListPage = courseMapper.findCourseListPage(courseListRequest);
        List<CourseInfo> list = courseListPage.getResult();
        long total = courseListPage.getTotal();
        QueryResult<CourseInfo> courseIncfoQueryResult = new QueryResult<CourseInfo>();
        courseIncfoQueryResult.setList(list);
        courseIncfoQueryResult.setTotal(total);
        return courseIncfoQueryResult;
    }
    View Code

    Controller

    修改CourseController的findCourseList,向service传入companyId
    这里先使用静态数据测试使用。

     @Override
       @GetMapping("/coursebase/list/{page}/{size}")
        public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page,
                                                      @PathVariable("size") int size,
                                                      CourseListRequest courseListRequest) {
            //先使用静态数据测试
            String companyId = "1";
            return courseService.findCourseList(companyId,page,size,courseListRequest);
        }

    测试

    1、用户登录
    由于使用了静态数据companyId为1,所以要使用企业编号为1的下边的用户去登录。
    2、进入我的课程,查看数据是否正确。
    观察所查询到的课程是该企业下的课程。

      获取当前用户信息

    需求分析

    要想实现只查询自己的课程信息则需要获取当前用户所属的企业id。
    1、认证服务在用户认证通过将用户所属公司id等信息存储到jwt令牌中。
    2、用户请求到达资源服务后,资源服务需要取出header中的jwt令牌,并解析出用户信息。

    jwt令牌包括企业Id

    资源服务在授权时需要用到用户所属企业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;
    ......

    通过上边代码的分析得知,认证服务调用XcUserExt userext = userClient.getUserext(username);获取用户信息,
    将userext 中的信息存储到jwt令牌中,在userext 对象中已经包括了companyId公司ID等信息。

    获取当前用户

    JWT解析工具类

    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;
        }
    }
    View Code

    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;
        }
    }
    View Code

    获取当前用户

    修改课程管理的CourseController类,将companyId的静态数据改为动态获取:

    @Override
    public QueryResult<CourseInfo> findCourseList(@PathVariable("page") int page,
                                                  @PathVariable("size") int size,
                                                  CourseListRequest courseListRequest) {
        //调用工具类取出用户信息
        XcOauth2Util xcOauth2Util = new XcOauth2Util();
        XcOauth2Util.UserJwt userJwt = xcOauth2Util.getUserJwtFromHeader(request);
        if(userJwt == null){
            ExceptionCast.cast(CommonCode.UNAUTHENTICATED);
     }
        String companyId = userJwt.getCompanyId();
        return courseService.findCourseList(companyId,page,size,courseListRequest);
    }

    测试

    使用不同的用户登录系统,测试细粒度权限控制效果。
    预期结果:每个用户只查询自己所拥有的课程。

  • 相关阅读:
    Django学习笔记之中间件和上下文处理器
    python练手小题(四)
    python练手小题(三)
    python练手小题(二)
    python练手小题(一)
    Django学习笔记之自定义过滤器及标签
    Django学习笔记之模板标签与静态文件
    Django学习笔记之模板变量与模板过滤器
    Django学习笔记之url路由及模板渲染方式
    学习Go语言的9大理由
  • 原文地址:https://www.cnblogs.com/anan-java/p/12321242.html
Copyright © 2020-2023  润新知