学成在线笔记十四:用户授权

用户授权

需求分析

  1. 管理员给用户分配权限,权限数据写到数据库中。
  2. 认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)

权限数据模型

五张表,标准的权限模型设计。

比较简单易懂。

https://imxushuai-01.coding.net/p/pic/d/pic/git/raw/298dc9640cd17b5a0ac744515594de8bac3af0b8/%E5%AD%A6%E6%88%90%E5%9C%A8%E7%BA%BF-%E7%94%A8%E6%88%B7%E6%9D%83%E9%99%90%E6%A8%A1%E5%9E%8B.jpg

  • xc_user

    用户表,存储了系统用户信息,用户类型包括:学生、老师、管理员等

  • xc_role

    角色表,存储了系统的角色信息,学生、老师、教学管理员、系统管理员等

  • xc_user_role

    用户角色表,一个用户可拥有多个角色,一个角色可被多个用户所拥有

  • xc_menu

    模块表,记录了菜单及菜单下的权限

  • xc_permission

    角色权限表,一个角色可拥有多个权限,一个权限可被多个角色所拥有

用户中心查询权限列表

Dao

  • XcMenuMapper

    package com.xuecheng.ucenter.dao;
    
    import com.xuecheng.framework.domain.ucenter.XcMenu;
    import org.apache.ibatis.annotations.Mapper;
    
    import java.util.List;
    
    @Mapper
    public interface XcMenuMapper {
        List<XcMenu> selectPermissionByUserId(String userid);
    
    }
    
  • 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 updateTime
            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#findByUsername方法,查询用户权限

    @Autowired
    private XcMenuMapper xcMenuMapper;


    /**
     * 按用户名查询用户信息
     *
     * @param username 用户名
     * @return 用户信息
     */
    public XcUserExt findByUsername(String username) {
        XcUserExt result = new XcUserExt();

        // 查询用户信息
        XcUser userInfo = xcUserRepository.findByUsername(username);
        if (userInfo == null) {
            return null;
        }

        BeanUtils.copyProperties(userInfo, result);

        // 查询用户公司信息
        XcCompanyUser companyUser = xcCompanyUserRepository.findByUserId(userInfo.getId());
        if (companyUser != null) {
            result.setCompanyId(companyUser.getCompanyId());
        }

        // 查询用户权限
        List<XcMenu> xcMenus = xcMenuMapper.selectPermissionByUserId(userInfo.getId());
        result.setPermissions(xcMenus);

        return result;
    }

认证服务设置用户权限

修改认证微服务中授权部分的代码,使用查询到的用户权限进行用户授权。

https://imxushuai-01.coding.net/p/pic/d/pic/git/raw/369f6242f49a1d7a631e7cdf92c87e948bf537fa/QQ%E6%88%AA%E5%9B%BE20200623232453.png

        List<String> stringList = userext.getPermissions()
                .stream()
                .map(XcMenu::getCode)
                .collect(Collectors.toList());
        String[] permissionList = new String[stringList.size()];
        UserJwt userDetails = new UserJwt(username,
                password,
                AuthorityUtils.createAuthorityList(stringList.toArray(permissionList)));

异常处理

权限不足为特殊的异常,可以使用统一异常处理器,捕捉异常然后抛出。

修改xc-framework-common中的ExceptionCatch中的静态代码块

    static {
        OPTIONS = builder
                .put(HttpMessageNotReadableException.class, CommonCode.PARAMS_ERROR)
                .put(MissingServletRequestPartException.class, CommonCode.PARAMS_ERROR)
                .put(AccessDeniedException.class, CommonCode.UNAUTHORISE)
                .build();

    }

新增上AccessDeniedException的错误码即可。

控制接口权限

控制接口权限需要下列步骤:

  1. 需要控制接口的微服务,需要先引入Spring Security的依赖
  2. 拷贝ResourceServerConfig
  3. 拷贝公钥
  4. ResourceServerConfig需要注解@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
  5. 在需要控制权限的接口上添加注解@PreAuthorize("hasAuthority('{permission_str}')")

测试

登录后获取token,解析token,查看是否包含permiss列表

https://imxushuai-01.coding.net/p/pic/d/pic/git/raw/aeb1eef44c145ea687d8630bc1cae8b125615ee3/QQ%E6%88%AA%E5%9B%BE20200623232748.png

我这里是在JWT官网解析的,网址:👉https://jwt.io/👈

前端集成认证授权

这里以教学管理中心模块为例

需求分析

前端集成认证授权功能需要作如下工作:

  1. 前端页面校验用户的身份,如果用户没有登录则跳转到登录页面
  2. 前端请求资源服务需要在http header中添加jwt令牌,资源服务根据jwt令牌完成授权。

哪些功能需要前端请求时携带JWT

用户登录成功请求资源服务都需要携带jwt令牌,因为资源服务已经实现了jwt认证,如果校验头部没有jwt则会认为身份不合法。

配置Nginx

教学管理前端访问微服务统一在访问地址前添加/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/;  
		} 
	 
	}

前端代码

其他的前端代码基本都是实现了的,只需要在main.js把注释符去掉就行了。

前端代码就不做多的解析了,有兴趣的可以自己研究。

细粒度授权

感觉叫细粒度授权有点怪怪的,这更像是业务逻辑。

需求分析

  1. 我的课程查询,细粒度授权过程如下
    • 获取当前登录的用户Id
    • 得到用户所属教育机构的Id
    • 查询该教学机构下的课程信息
  2. 修改课程管理服务“我的课程”的功能,根据公司Id查询课程
    • 修改Dao,支持根据公司Id 查询课程
    • 修改Service,将公司Id传入Dao
    • 修改Controller,获取当前用户的公司Id,传给Service

获取当前登录用户

JWT令牌解析

因为之前我们已经在登录的时候,将用户的company_id放在了令牌中,我们现在只需要解析令牌就可以获取到当前登录用户的company_id

  • 工具类代码

    package com.xuecheng.framework.utils;
    
    import com.alibaba.fastjson.JSON;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.security.jwt.Jwt;
    import org.springframework.security.jwt.JwtHelper;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Map;
    
    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;
        }
    }
    
  • 将工具解析到的数据封装为想要的数据结构

    package com.xuecheng.framework.utils;
    
    import lombok.Data;
    import org.apache.commons.lang3.StringUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Map;
    
    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;
        }
    
    }
    

课程管理改造

课程管理新增API

CourseBaseControllerApi中新增方法

QueryResponseResult 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 course_base.company_id = #{companyId}
    </select>

CourseBaseService

    /**
     * 查询当前登录用户的课程列表
     *
     * @param companyId         当前用户的所属公司
     * @param page              当前页
     * @param size              每页记录数
     * @param courseListRequest 查询条件
     * @return 课程列表
     */
    public QueryResponseResult findCourseList(String companyId, int page, int size, CourseListRequest courseListRequest) {
        if(courseListRequest == null){
            courseListRequest = new CourseListRequest();
        }
        // 设置companyid
        courseListRequest.setCompanyId(companyId);
        // 分页
        PageHelper.startPage(page, size);
        // 调用dao
        com.github.pagehelper.Page<CourseInfo> courseListPage = courseMapper.findCourseListPage(courseListRequest);
        List<CourseInfo> list = courseListPage.getResult();
        long total = courseListPage.getTotal();
        QueryResult<CourseInfo> courseInfoQueryResult = new QueryResult<>();
        courseInfoQueryResult.setList(list);
        courseInfoQueryResult.setTotal(total);
        return new QueryResponseResult(CommonCode.SUCCESS,courseInfoQueryResult);
    }

需要引入PageHelper的依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>

CourseBaseController

实现findCourseList方法

    @Override
    @GetMapping("/list/{page}/{size}")
    public QueryResponseResult findCourseList(@PathVariable int page, @PathVariable int size,
                                              CourseListRequest courseListRequest) {
        // 获取当前用户信息
        XcOauth2Util xcOauth2Util = new XcOauth2Util();
        XcOauth2Util.UserJwt userJwt = xcOauth2Util.getUserJwtFromHeader(request);
        // 使用companyId查询数据
        return courseBaseService.findCourseList(userJwt.getCompanyId(), page, size, courseListRequest);
    }

测试

我从postman能够成功获取到数据。

用前端去,我发现teacher.xucheng.com这边的sessionStorage里面根本没有存令牌不知道为啥,是跨域吗?还是其他问题,搞了一会儿没解决,算了,懒得搞了。

微服务之间认证

微服务之间的调用,也是需要携带jwt令牌的,但是现在是没有的,所以当我们调用的某些需要调用其他微服务的接口的时候,就会出现401认证失败的错误。

Feign拦截器

定义Feign拦截器

package com.xuecheng.framework.intercepter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

/**
 * feign拦截器, 调用前传递header
 */
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.printStackTrace();
        }

    }
}

使用Feign拦截器

只需要将编写的拦截器注册到需要使用该拦截器的微服务的spring容器中即可.

不想单拎出来可以直接在启动类中注册.

@Bean
public FeignClientInterceptor feignClientInterceptor(){
return new FeignClientInterceptor();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值