搭建后台springboot框架
框架说明
这是以前搭建的一套单springboot的后台接口框架,没有用到微服务。这里介绍一下里面使用的模块,后面有时间再完善搭建过程。
技术选型
技术 | 版本 | 备注 |
---|---|---|
Spring Boot | 2.4.2 | 最新发布稳定版 |
springboot-jdbc | 用jdbc实现自定义SQL | |
Mybatis Plus | 3.4.2 | mybatis增强框架 |
HikariCP | 数据源 | |
Knife4j | 2.0.8 | api文档生成工具 |
MySQL | MySQL数据库连接驱动 | |
ehcache | 缓存(有需要可以换成redis) | |
JWT | 3.4.0 | JSON WEB TOKEN |
hutool-all | 5.5.8 | 常用工具集 |
lombok | 注解生成Java Bean等工具 | |
commons-lang3 | 3.9 | 常用工具包 |
commons-io | 2.8.0 | IO工具包 |
jsoup | 1.13.1 | 用于防css攻击 |
p6spy | 3.8.7 | SQL日志分析打印 |
jsoup | 1.13.1 | jsoup,用于防css攻击 |
自定义对象
- 自定义返回结果
- 自定义分页对象
- 自定义异常
mybatis-plus
- 配置
JDBC
使用springboot封装过的jdbc,还挺好用的
- 封装自定义SQL查询
xss注入解决
统一异常返回
LocalDateTime 日期适配器
统一处理LocalDateTime的格式
支持swagger
- 使用knife4j
权限校验
之前弄的权限校验用的是shiro、JWT、Redis,后来觉得还不如自己写,就用JWT和ehcache写了一个权限校验的,自己定义比较灵活。不用Redis是因为有时候项目比较小用不到,用ehcache不用部署,直接用就可以,后续有需要换redis也比较简单。
-
定义HandlerInterceptor拦截器
@Slf4j public class AuthenticationInterceptor implements HandlerInterceptor { // jwt的私钥 @Value("${custom.jwt.secret}") private String secret; @Autowired private UserTokenEhcaheService userTokenEhcaheService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { String token = request.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if (!(object instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } 执行认证 // token是否传入 if (StringUtils.isBlank(token)) { throw new LoginResultException("请传入token"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build(); try { // 校验token jwtVerifier.verify(token); // 获取token中的userId String userId = JWT.decode(token).getClaim("userId").asString(); // 获取token中的登录类型 String loginType = JWT.decode(token).getClaim("loginType").asString(); UserTokenReturn userToken = userTokenEhcaheService.findByToken(userId, loginType); if (userToken == null) { throw new LoginResultException("token已过期"); } else { // 验证与缓存中的是否是同一个token(保证单点登录) if (userToken.getToken().equals(token)) { log.info("token可用,用户信息是{}",userToken.toString()); // 在当前线程中放入用户id,在其他地方使用 RequestDetailThreadLocal.getRequestDetail().setUserId(userId); return true; } else { throw new LoginResultException("token已过期"); } } } catch (Exception e) { throw new LoginResultException("token校验失败"); } } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
-
定义一个service存储token在缓存中
@Slf4j @Service @CacheConfig(cacheNames = "user_token_cache") public class UserTokenEhcaheService { @CachePut(key = "#user.userId+'-'+#user.loginType") public UserTokenReturn saveToken(UserTokenReturn user){ log.info("将token存入缓存"); return user; } @Cacheable(key = "#userId+'-'+#loginType") public UserTokenReturn findByToken(String userId,String loginType){ log.info("获取用户信息失败,没有找到缓存"); return null; } @CacheEvict(key = "#userId+'-'+#loginType") public void deleteByToken(String userId,String loginType) { log.info("删除缓存中的token"); } }
-
添加拦截器
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry // 鉴权拦截器 .addInterceptor(authenticationInterceptor()) // 添加需要拦截的路径 .addPathPatterns("/**") // 添加需要忽略的路径 .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/error","/swagger-ui.html/**"); // 可以继续添加拦截器 } /** * 鉴权拦截器 * @return */ @Bean public AuthenticationInterceptor authenticationInterceptor(){ return new AuthenticationInterceptor(); } }
-
增加用户权限的接口类,实现登录和注销
@RestController @RequestMapping("/authentication") @Api(tags = "Test001-用户权限") public class AuthenticationController { // jwt的私钥 @Value("${custom.jwt.secret}") private String secret; @Autowired private UserTokenService userTokenService; @Autowired private UserTokenEhcaheService userTokenEhcaheService; @Autowired private UserService userService; @PassToken @PostMapping("/login") @ApiOperation("使用账号密码登录") public UserTokenReturn login(@RequestBody UserTokenReq userTokenReq) { User user = userTokenService.findUserByLoginName(userTokenReq.getLoginName()); if (user != null && user.getPassword().equals(userTokenReq.getPassword())) {//验证用户名密码,如果登录成功 // 用户信息 UserTokenReturn userTokenReturn = new UserTokenReturn(); userTokenReturn.setUserId(user.getId()); userTokenReturn.setName(user.getName()); userTokenReturn.setLoginName(user.getLoginName()); //使用随机数可以多地登录,需要单一登录需要传入登录类型 userTokenReturn.setLoginType(String.valueOf(new Random().nextInt(1000))); // 生成token,token不过期,过期时间由缓存控制(也可生成一个随机字符串也是一样的) String token = JWT.create() // 用户id .withClaim("userId", userTokenReturn.getUserId()) .withClaim("loginType", userTokenReturn.getLoginType()) // 使用随机数,使生成的token不一致 .withClaim("random", new Random().nextInt(1000)) .sign(Algorithm.HMAC256(secret)); userTokenReturn.setToken(token); // 放入缓存,过期时间由ehcahe的timeToIdleSeconds闲置时间控制,一直访问可以一直不过期 userTokenEhcaheService.saveToken(userTokenReturn); return userTokenReturn; }else { // 登录失败 throw new BaseResultException("登录失败,账号或密码错误"); } } @PostMapping("/logout") @ApiOperation("用户注销登录,删除缓存中的token") public String logout(@RequestHeader(required = false) String token){ if(StringUtils.isEmpty(token)) throw new BaseResultException("token不存在"); // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build(); try { // 校验token jwtVerifier.verify(token); // 获取token中的userId String userId = JWT.decode(token).getClaim("userId").asString(); // 获取token中的登录类型 String loginType = JWT.decode(token).getClaim("loginType").asString(); userTokenEhcaheService.deleteByToken(userId,loginType); } catch (Exception e) { throw new BaseResultException("token校验失败"); } return "注销登录成功"; } @GetMapping("/check") @ApiOperation("获取用户信息,测试是否已经登录") public User check() { RequestDetail requestDetail = RequestDetailThreadLocal.getRequestDetail(); User user = userService.findById(requestDetail.getUserId()); return user; } }
-
启动类加上注解
@EnableCaching
-
将用户信息放在线程中,方便取用,获取用其他方法,比如注解
目录格式
目录格式按照模块分类,每个模块中都有controller、service等,好处是方便复用,坏处是接口类存放位置不统一;这次尝试用这种方式。
----config
----modules
--------example
------------controller
----------------ExampleController
------------dao
----------------mapper
--------------------ExampleMapper
----------------xml
--------------------ExampleMapper.xml
----------------ExampleDao
------------entity
----------------ExampleEntity
------------model
----------------ExampleModel
------------service
----------------ExampleService
----util
因为controller放到模块中,@RestControllerAdvice(basePackages = "xx.xx.xx")
定义的基础路径要包含全部接口
因为将xml和Mapper类都放到没模块中,入口函数上的注解@MapperScan
不能在用,需要在每个Mapper类上使用注解@Mapper
,xml的位置也需要在配置文件中定义,建议配置两个,包含原来的路径
mapper-locations: classpath:mapper/*.xml,classpath:xx/xx/xx/modules/**/dao/xml/*.xml
由于xml放在模块中,打包会导致xml没打进去,需要修改打包的配置
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/resources/lib</directory>
<targetPath>/BOOT-INF/lib/</targetPath>
<includes>
<include>**/*.jar</include>
</includes>
</resource>
</resources>
</build>
文件上传下载
- 编写一个简单的文件上传下载功能,存放在本地
- 有需要可以使用minio,也比较简单