效果展示:

YAPI接口文档:


代码展示:
首先使用mybatis-plus代码生成器,自动生成模板


controller层:

虽然这样写对于这个功能来说没有问题,但是我每个Controller都需要获取token进行验证是否登录。就会出现一下问题:

统一Token处理(重点)
解决思路:
我们可以使用网关,进行token的获取和解析,并判断登录状态,提取用户信息,将其写入请求头在转发到springMVC的拦截器,并将数据绑定到ThreadLocal线程池中,以后可以在线程中获取信息,示意图如下:


首先在网关模块,搭建全局过滤器

package com.xuecheng.gate.filter;
import com.alibaba.fastjson.JSON;
import com.xuecheng.commons.model.vo.AuthInfo;
import com.xuecheng.commons.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.SneakyThrows;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
//网关校验
@Component
public class AuthFilter implements GlobalFilter,Ordered {
@Override
@SneakyThrows
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
//判断不要验证的,直接放行
if(path.contains("coursePub/preview") ||
path.contains("auth/login") ||
path.contains("basic/dictionary") ||
path.contains("category/tree-nodes")||
path.contains("course/upload")||
path.contains("search/")){
return chain.filter(exchange);
}
String token = request.getHeaders().getFirst("Authorization");
Boolean aBoolean = JwtUtils.verifyToken(token);
if (!aBoolean){
//4、校验token,对于token异常的,返回401状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
Claims claims = JwtUtils.parserToken(token).getBody();
Long userId = claims.get("userId", Long.class);
Long companyId = claims.get("companyId",Long.class);
String companyName = claims.get("companyName",String.class);
AuthInfo authInfo = new AuthInfo();
authInfo.setUid(userId);
authInfo.setCompanyId(companyId);
authInfo.setCompanyName(companyName);
//解决中文乱码
String json = URLEncoder.encode(JSON.toJSONString(authInfo), "UTF-8");
//7、向下传递,到其他微服务
//向请求头对象,设置用户信息
ServerHttpRequest httpRequest = request.mutate().headers(httpHeaders -> {
httpHeaders.add("payload", json);
}).build();
//设置到请求体里
exchange.mutate().request(httpRequest);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
然后在统一web处理模块,配置拦截器

package com.xuecheng.web.filter;
import com.alibaba.fastjson.JSON;
import com.xuecheng.commons.model.vo.AuthInfo;
import com.xuecheng.commons.utils.AuthInfoHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
/**
* 自定义拦截器
*/
public class AuthInfoInInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String payload = request.getHeader("payload");
if(StringUtils.isEmpty(payload)) {
return true;
}
String json = URLDecoder.decode(payload, "utf-8");
AuthInfo authInfo = JSON.parseObject(json, AuthInfo.class);
AuthInfoHolder.setAuthInfo(authInfo);
return true;
}
}
package com.xuecheng.web.filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.annotation.WebFilter;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInfoInInterceptor())
.addPathPatterns("/**");
}
}
最后设置ThreadLocal工具类:

package com.xuecheng.commons.utils;
import com.xuecheng.commons.model.vo.AuthInfo;
/**
* 存放用户id的容器
*/
public class AuthInfoHolder {
//定义ThreadLocal
private final static ThreadLocal<AuthInfo> threadLocal = new ThreadLocal<>();
/**
* 获取线程中的用户
*/
public static AuthInfo getAuthInfo() {
return threadLocal.get();
}
/**
* 设置当前线程中的用户
*/
public static void setAuthInfo(AuthInfo info) {
threadLocal.set(info);
}
public static Long getUserId() {
return threadLocal.get().getUid();
}
public static Long getCompanyId() {
if(threadLocal.get() != null) {
return threadLocal.get().getCompanyId();
}else {
return null;
}
}
public static void remove(){threadLocal.remove();}
}
引入依赖,并修改启动类:
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xc-framework-web</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
package com.xuecheng.system;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@MapperScan("com.xuecheng.system.mappers")
@ComponentScan(basePackages = {
"com.xuecheng.web",
"com.xuecheng.system"
})
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
}
}
修改后的controller层:
package com.xuecheng.system.controller;
import com.xuecheng.commons.enums.ErrorCode;
import com.xuecheng.commons.model.vo.AuthInfo;
import com.xuecheng.commons.model.vo.CompanyVo;
import com.xuecheng.commons.model.vo.ResponseResult;
import com.xuecheng.commons.utils.AuthInfoHolder;
import com.xuecheng.commons.utils.JwtUtils;
import com.xuecheng.web.exception.BusinessException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.xuecheng.system.service.CompanyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import sun.net.httpserver.AuthFilter;
/**
*
* 教育机构前端控制器
*
*
* @author itheima
*/
@Slf4j
@RestController
@RequestMapping("/company")
public class CompanyController {
@Autowired
private CompanyService companyService;
//查询我的机构信息
@GetMapping("/mine")
public ResponseResult mine(@RequestHeader("payload") String payload){
AuthInfo authInfo = AuthInfoHolder.getAuthInfo();
Long companyId = authInfo.getCompanyId();
CompanyVo vo =companyService.mine(companyId);
return ResponseResult.okResult(vo);
}
}
serviceimpl层:
package com.xuecheng.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.xuecheng.commons.enums.ErrorCode;
import com.xuecheng.commons.model.vo.CompanyVo;
import com.xuecheng.commons.utils.BeanHelper;
import com.xuecheng.system.domain.Company;
import com.xuecheng.system.mappers.CompanyMapper;
import com.xuecheng.system.service.CompanyService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.web.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* 教育机构 服务实现类
*
*
* @author itheima
* @since 2022-07-24
*/
@Service
public class CompanyServiceImpl extends ServiceImpl<CompanyMapper, Company> implements CompanyService {
@Autowired
private CompanyMapper companyMapper;
@Override
public CompanyVo mine(Long companyId) {
Company company = companyMapper.selectById(companyId);
if (BeanUtil.isEmpty(company)){
throw new BusinessException(ErrorCode.COMPANYERROR);
}
CompanyVo vo = BeanHelper.copyProperties(company, CompanyVo.class);
return vo;
}
}
效果展示

YAPI文档接口:

需要的表:

代码展示:
使用mybatis-plus代码生成器生成基本架构,带入相关依赖和yml配置文件,太多了这里我就不显示了
配置mybatis-plus分页插件

package com.xuecheng.mybatis.config;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.xuecheng.commons.utils.AuthInfoHolder;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class MybatisPlusConfig {
private static String [] tables = new String[]{"tb_dictionary","tb_category",
"undo_log","tb_application","tb_company","tb_resource","tb_role_resource","tb_user_role"};
//注册mybatis的插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//多租户 mybatisPlusInterceptor.addInnerInterceptor(tenantEnterpriseInterceptor());
//分页
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor());
return mybatisPlusInterceptor;
}
//分页的插件配置
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor(){
return new PaginationInnerInterceptor(DbType.MYSQL);
}
}
web层:
package com.xuecheng.content.controller;
import com.xuecheng.commons.model.dto.CourseDto;
import com.xuecheng.commons.model.dto.PageRequest;
import com.xuecheng.commons.model.vo.ResponseResult;
import javafx.geometry.Pos;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.xuecheng.content.service.CourseBaseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
/**
*
* 课程基本信息前端控制器
*
*
* @author itheima
*/
@Slf4j
@RestController
@RequestMapping("/course")
public class CourseBaseController {
@Autowired
private CourseBaseService courseBaseService;
//分页查询课程列表
@PostMapping("/list")
public ResponseResult courseList(PageRequest pageRequest, @RequestBody CourseDto courseDto){
return courseBaseService.courseList(pageRequest,courseDto);
}
}
serviceIml层:
package com.xuecheng.content.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xuecheng.commons.model.dto.CourseDto;
import com.xuecheng.commons.model.dto.PageRequest;
import com.xuecheng.commons.model.vo.PageResponseResult;
import com.xuecheng.commons.model.vo.ResponseResult;
import com.xuecheng.commons.utils.AuthInfoHolder;
import com.xuecheng.content.domain.CourseBase;
import com.xuecheng.content.mappers.CourseBaseMapper;
import com.xuecheng.content.service.CourseBaseService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
* 课程基本信息 服务实现类
*
*
* @author itheima
* @since 2022-07-27
*/
@Service
public class CourseBaseServiceImpl extends ServiceImpl<CourseBaseMapper, CourseBase> implements CourseBaseService {
@Override
public ResponseResult courseList(PageRequest pageRequest, CourseDto courseDto) {
//分页
Page<CourseBase> page = new Page<>(pageRequest.getPageNo(), pageRequest.getPageSize());
//根据条件插叙
LambdaQueryWrapper<CourseBase> wrapper = new LambdaQueryWrapper<>();
//hutool工具包
if (StrUtil.isNotEmpty(courseDto.getCourseName())){
wrapper.like(CourseBase::getName,courseDto.getCourseName());
}
if (StrUtil.isNotEmpty(courseDto.getAuditStatus())){
wrapper.eq(CourseBase::getAuditStatus,courseDto.getAuditStatus());
}
if (StrUtil.isNotEmpty(courseDto.getTeachmode())){
wrapper.eq(CourseBase::getTeachmode,courseDto.getTeachmode());
}
//设置机构ID,要不然就全查出来了
wrapper.eq(CourseBase::getCompanyId, AuthInfoHolder.getCompanyId());
//数据分页
IPage<CourseBase> courseBasePage = this.page(page, wrapper);
return PageResponseResult.okResult(courseBasePage.getTotal(), courseBasePage.getRecords());
}
}
代码优化:

这里其实有点小问题,为了确保数据的安全性和隔离性,我们在执行SQL时,往往都会设置上租户id(即企业ID),针对大面积的操作就比较繁琐和枯燥。查询,删除,更新,插入操作的时候都得加上 企业ID这个条件

那mybatis-plus提供了多租户插件,借助SQL拦截技术,可以自动对SQL语句添加租户ID
具体如下:
首先注解掉以前的代码

其次在mybatis-plus的配置在添加多租户sql拦截器

然后加上

/**
* 多租户的插件配置
* getTenantId: 租户数据的来源
* getTenantIdColumn: 拼接的SQL语句的租户字段
* ignoreTable: 判断表时候需要租户配置
*/
@Bean
public TenantLineInnerInterceptor tenantEnterpriseInterceptor(){
return new TenantLineInnerInterceptor(new TenantLineHandler() {
//获取线程中的企业id
public Expression getTenantId() {
Long companyId = AuthInfoHolder.getCompanyId();
if(ObjectUtil.isEmpty(companyId)) {
return null;
}else {
return new StringValue(companyId.toString());
}
}
//租户的字段
public String getTenantIdColumn() {
return "company_id";
}
//设置忽略的表,true忽略,false-拼接
@Override
public boolean ignoreTable(String tableName) {
// 是否需要需要过滤某一张表
if (ArrayUtil.contains(tables,tableName)){
return true;
}
//拼接的sql多租户字段标示对应的值不能为空
Expression tenantId = this.getTenantId();
if (ObjectUtil.isEmpty((tenantId))){
return true;
}
return false;
}
});
}
效果展示:

第二天任务完成