学成在线笔记十四:用户授权
用户授权、数据细粒度及微服务之间调用携带 JWT 令牌

用户授权

需求分析

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

权限数据模型

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

比较简单易懂。

  • xc_user

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

  • xc_role

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

  • xc_user_role

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

  • xc_menu

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

  • xc_permission

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

用户中心查询权限列表

Dao

  • XcMenuMapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <?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方法,查询用户权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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;
}

认证服务设置用户权限

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

1
2
3
4
5
6
7
8
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中的静态代码块

1
2
3
4
5
6
7
8
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列表

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

前端集成认证授权

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

需求分析

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

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

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

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

配置Nginx

教学管理前端访问微服务统一在访问地址前添加/api前缀并经过网关转发到微服务。

配置teacher.xuecheng.com的代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#前端教学管理
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

  • 工具类代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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;
    }
    }
  • 将工具解析到的数据封装为想要的数据结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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中新增方法

1
QueryResponseResult findCourseList(int page, int size, CourseListRequest courseListRequest);

Dao

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

1
2
3
4
5
6
7
8
9
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 查询当前登录用户的课程列表
*
* @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的依赖

1
2
3
4
5
> <dependency>
> <groupId>com.github.pagehelper</groupId>
> <artifactId>pagehelper-spring-boot-starter</artifactId>
> </dependency>
>

CourseBaseController

实现findCourseList方法

1
2
3
4
5
6
7
8
9
10
@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拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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容器中即可.

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

1
2
3
4
@Bean
public FeignClientInterceptor feignClientInterceptor(){
return new FeignClientInterceptor();
}

代码获取

代码获取

文章作者: imxushuai
文章链接: https://www.imxushuai.com/2020/06/26/33.学成在线笔记十四:用户授权/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 imxushuai
支付宝打赏
微信打赏