用户授权
需求分析
- 管理员给用户分配权限,权限数据写到数据库中。
- 认证服务在进行用户认证时从数据库读取用户的权限数据(动态数据)
权限数据模型
五张表,标准的权限模型设计。
比较简单易懂。
-
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;
}
认证服务设置用户权限
修改认证微服务中授权部分的代码,使用查询到的用户权限进行用户授权。
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
的错误码即可。
控制接口权限
控制接口权限需要下列步骤:
- 需要控制接口的微服务,需要先引入
Spring Security
的依赖 - 拷贝
ResourceServerConfig
类 - 拷贝公钥
ResourceServerConfig
需要注解@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
- 在需要控制权限的接口上添加注解
@PreAuthorize("hasAuthority('{permission_str}')")
测试
登录后获取token
,解析token
,查看是否包含permiss
列表
我这里是在JWT
官网解析的,网址:👉https://jwt.io/👈
前端集成认证授权
这里以教学管理中心
模块为例
需求分析
前端集成认证授权功能需要作如下工作:
- 前端页面校验用户的身份,如果用户没有登录则跳转到登录页面
- 前端请求资源服务需要在
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
把注释符去掉就行了。
前端代码就不做多的解析了,有兴趣的可以自己研究。
细粒度授权
感觉叫细粒度授权有点怪怪的,这更像是业务逻辑。
需求分析
- 我的课程查询,细粒度授权过程如下
- 获取当前登录的用户Id
- 得到用户所属教育机构的Id
- 查询该教学机构下的课程信息
- 修改课程管理服务“我的课程”的功能,根据公司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();
}