阅读 3219

【建议收藏】毕设/私活/大佬必备,开源一个SpringBoot标准化框架

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

小伙伴们,大家好,我是花Gie,一个正经的程序猿。

今天和大家分享一个项目,这个项目花Gie不眠不休足足肝了两天两夜,这是个什么东东呢,它是集SpringBoot+Mybatis+redis+shiro+jwt+vue于一体的标准项目框架。

整个项目不涉及任何具体业务场景,只配置了一些常用功能,如:权限管理,用户管理,菜单管理等,外加redis中间件,以及很多好用的工具类(RedisUtil,Id生成器,JWt等),可以说是即拿即用,扩展性也非常强,下面就就进入主题,看看怎么使用吧。

image.png

一、SpringBoot + Mybatis介绍与搭建

不知道大家在开发过程中,有没有从0开始搭建一个项目,反正花Gie刚开始学Java的时候,被SSM折腾的死去活来,各种复杂的配置文件搞的我一直在崩溃的边缘试探,而近年来随着SpringBoot的普遍使用,新一批程序猿大军要舒服多了,因为SpringBoot简化了配置,让开发变得极其简单而快速。

1.1 SpringBoot是什么

SpringBoot 是 Pivotal 团队开发的一套全新框架,设计目的是为了简化项目初始搭建以及开发过程,去除了大量的 xml 配置文件,简化了复杂的依赖管理,配合各种 starter 使用,基本上可以做到自动化配置。如果你使用的是IDEA开发工具,你只要点击几下 [下一步] 就可以完成所有配置,实现轻松启动。

1.2 框架搭建介绍

搭建基础框架有多种方式,由于不是我们本文的重点,这里介绍两种最常用的方式,小伙伴们可以根据自己习惯来选取,花Gie一般使用IDEA来搭建,相当方便。

  • 1.通过Spring Initializr创建

打开浏览器,输入地址start.spring.io,正常情况可以看到下面这个界面。

image.png

我们可以看到有很多配置项,这里简单介绍一下主要配置项的含义:

  • Project: 选择使用Maven或Gradle来创建项目;

  • Language: 开发语言;

  • Spring Boot: Spring Boot版本选择,默认最新版本(非里程碑和快照版本);

  • Project Metadata: 指定项目的一些基本信息:

    1. Group: 一般分为多个段,如com.basic.business,其中第一段为域,第二段为公司名称。域又分为org、com、cn等,其中org为非营利组织,com为商业组织。举个apache公司的tomcat项目例子:这个项目的groupId是,它的域是org(tomcat是非营利项目),公司名称是apache,artigactId是tomcat。
    2. Artfact: 一般是项目名或者模块名,和Group一起保证项目唯一性
    3. name:项目名称
    4. Description:项目描述
    5. Package Name:包名,如com.huage.base
    6. Packaging:打包方式
    7. Java :JDK版本号
  • Dependencies: 选择需要的依赖,它会在创建项目时自动在生成的pom.xml(Maven)或者build.gradle(Gradle)引入依赖。

填好所有信息后,点击Generate the project 按钮,Spring Initializr就会生成一个项目,这个项目会以zip文件的形式下载。解压到本地后,可以通过IDEA导入项目。

2.通过IDEA创建

依次点击IDEA菜单栏:File -> New -> Project,打开如下窗口:

image.png

选择好JDK版本后,点击下一步会看到如下界面,是不是很熟悉,这个和上面说的网页端操作是一样的,这里就不再赘述,继续点击【下一步】

image.png

下面这里是我们初始化依赖的地方,我们可以根据项目需要合理选择,也可以后续在pom文件中添加,此外这里还可以选择SpringBoot版本,一般使用最新稳定版本。

image.png

1.3 数据库设计

因为本项目不涉及任何具体业务,所以只需要一些基础表即可,如:sys_user、sys_role、sys_menu等。

image.png

1.4【标准版本】基础信息配置

基础项目搭建完成后,此时整个项目只有空的文件夹和一个空的application.properties,这时我们需要对数据库和依赖包以及mybatis进行配置。

  • 1. 配置文件
server.port=18082
spring.application.name=first-program
​
# mysql db
spring.datasource.url=jdbc:mysql://localhost:3306/firstProgram?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
​
# mybatis
mybatis.type-aliases-package=com.basic.business
mybatis.mapper-locations=classpath:mapper/*.xml
​
pagehelper.helper-dialect= mysql
pagehelper.reasonable= false
pagehelper.support-methods-arguments= true
pagehelper.params= count=countsql
复制代码
  • 2. pom文件
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter</artifactId>
</dependency>
<!--mysql 驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.40</version>
    <scope>runtime</scope>
</dependency>
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.2</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper-spring-boot-starter.version}</version>
</dependency>
复制代码

1.5 使用EasyCode快捷生成基础代码

配置好以上信息后,我们使用EasyCode代码生成器,来生成Entity、Dao、Service、Controller等文件,花Gie在一个小时肝了一周的需求,看我如何使用EasyCode完成封神 中已经详细介绍过用法,这里就不再重复造轮子啦。

二、【标准版本】 集成Swagger

EasyCode代码生成后,会自动带有Swagger注解,我们需要两步配置即可完成Swagger注解引入

  • pom依赖
 <!--swagger配置-->
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.5.21</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
复制代码
  • 新建配置类
@Configuration
public class Swagger2Configurate {
    //是否开启swagger
    @Value("${swagger.enable}")
    private boolean enableSwagger;
​
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(enableSwagger)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }
​
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("标准版本API文档")
                .version("1.0")
                .build();
    }
}
复制代码

接下来我们启动一下项目,在浏览器输入地址http://127.0.0.1:18082/swagger-ui.html,看看效果吧,如下图,我们可以正常看到swagger界面,尝试调用了一下接口,也都可以正常请求:

image.png

三、【标准版本】集成 redis

redis想必小伙伴们即使没有用过,也是经常听到的,在工作中,redis用到的频率非常高,所以【标准版本】项目中,花Gie也集成了redis还有一些相关工具类,相当贴心。

3.1 redis是什么

用通俗点的话解释,redis就是一个数据库,直接运行在内存中,因此其运行速度相当快,同时其并发能力也非常强。redis是以key-value键值对的形式存在(如:"name":huage),它的key有五种常见类型

  • String:字符串
  • Hash:字典
  • List:列表
  • Set:集合
  • SortSet:有序集合

除此之外,redis还有一些高级数据结构,如HyperLogLog、Geo、Pub/Sub以及BloomFilter、RedisSearch等,这个后面花Gie会有专门的系列来讲解,这里不再展开啦(不然肝不完了)。

3.2 集成redis步骤

  • pom文件配置
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
复制代码
  • 配置文件
#redis配置开始
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=1024
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=10000
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=200
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000
#redis配置结束
spring.redis.block-when-exhausted=true
复制代码
  • 初始化配置文件
//初始化jedis
public JedisPool redisPoolFactory() throws Exception {
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxIdle(maxIdle);
    jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
    // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
    jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted);
    // 是否启用pool的jmx管理功能, 默认true
    jedisPoolConfig.setJmxEnabled(true);
    JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
    return jedisPool;
}
复制代码

3.3 代码演示

完成上面的配置后,我们只需要使用@Autowired引入RedisTemplate,就可以很方便的存取redis了,此外花Gie在项目中增加了一个RedisUtil工具类,囊括了redis大部分命令,足够平时开发使用。

//引入redis
@Autowired
private RedisTemplate redisTemplate;
​
//将【name:花哥】 存入redis
redisTemplate.opsForValue().set("name","花哥");
//取出redis中key为name的数据
redisTemplate.opsForValue().get("name");
复制代码

四、【标准版本】引入 JWT

标准版本采用的是前后端分离架构,因此要想保持前后端正常通信,需要前端请求时,带上一个身份识别字段token,来确认接口请求的合法性,所以就引入了jwt。

4.1jwt是什么

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

4.2 jwt适用场景

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

4.3 教程简介

这里花Gie看到知乎的一篇文章,写的也是相当好,链接奉上五分钟带你了解啥是JWT

五、【标准版本】集成 Shiro

基线版本采用了shiro进行鉴权,一是因为花Gie工作中用的比较多,对它相对比较熟悉;二来呢,shiro确实相较于另一种类似功能的spring-security,更加容易理解,代码更加易于阅读。

5.1 Shiro简介

Apache Shiro是一个强大且易用的Java安全框架,我们可以通过shiro完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。这些简直不要太嗨,一个人就可以肝这么多事情,堪称肝王。而且shiro是Apache下的项目,给人一种很靠谱的赶脚,并且它不跟任何框架或容器绑定。

5.2 集成shiro步骤

标准版本采用shiro+jwt来进行身份验证,抛开原理不说,我们先来看一下如何将shiro集成到我们标准版本中。

这里需要提醒一下工作经验不多的小司机,遇到不懂的技术切勿抓破头皮读概念,有时候亲自动手实现一遍代码,等到代码运行一遍,看完结果之后,再回过头来,重新看概念,捋一捋代码实现,会有不同的感受,如果在学习上还是感觉很难学下去,可以在评论区发出来,社区有很多大神可以解答,切勿闭门造车。

image.png

  • 配置核心安全事务管理器
@Bean
public SecurityManager securityManager() {
    /** 1. 引入两种身份验证realm */
    /**loginRealm用于登录认证,customRealm用于其他接口认证 */
    securityManager.setRealms(Lists.newArrayList(customRealm(), loginRealm()));
​
     DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    /** 2.设置认证策略*/
    securityManager.setAuthenticator(authenticator());
    
    /** 3.关闭shiro自带的session。让每次请求都得到过滤*/
    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
    securityManager.setSubjectDAO(subjectDAO);
    return securityManager;
}
复制代码

先来看 【第一步】 ,我们引入了两种身份验证(loginRealmcustomRealm),令人头大,这个Realm到底是啥呢,求求花Gie说说人话吧。

image.png

简单的说,后台会接收各种场景发来的请求,比如登录、注册、新增用户等等,我们不可能一棒子打死,全部用一种策略来校验所有请求,比如登录请求需要调用数据库查询登录信息是否正确,而一旦登录成功后,此后再发送其他接口请求,每次再连接数据库查询用户登录信息是否正确,显然是不合适的,所以需要其他策略。

image.png

因此,标准版本使用了两种Realm,其中用于处理登录验证的loginRealm,该realm只处理鉴权登录请求,校验登录是否正确;而对于其他所有的接口请求,都会被customRealm进行拦截处理。

接下来我们看 【第二步】setAuthenticator()方法是用来设置认证策略,那什么是认证策略呢,这听起来也不怎么像人话,但其实比较好理解,先来看一下下面这段代码:

/**
 *多个Realm时,设置认证策略
*/
@Bean
public ModularRealmAuthenticator authenticator() {
    ModularRealmAuthenticator authenticator = new MultiRealmAuthenticator();
    // 多个Realm的认证策略,默认 AtLeastOneSuccessfulStrategy
    AuthenticationStrategy strategy = new FirstSuccessfulStrategy();
    authenticator.setAuthenticationStrategy(strategy);
    return authenticator;
}
复制代码

是不是一脸懵逼,不要急,这里举个具体场景:我们标准版本里有两个用于认证的Realm,默认情况下,我们接口每次请求都会被这两个Realm分别校验,那如果有一个校验成功,另一个校验失败,那这次请求到底成功还是失败呢,这时候就需要一种策略来规范,官方给出三种策略:

  • FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;(注:这里"第一个"指的是认证成功得那一个realm)
  • AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulSTRATEGY不同,将返回所有Realm身份验证成功的认证信息;
  • AllSuccessfulStrategy:所有Realm验证成功过才算成功,且返回所有Realm身份验证的认证信息,如果有一个失败就失败。

最后看一下 【第三步】 ,这里我们禁用session, 不保存用户登录状态,保证每次请求都重新认证。

/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
    DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    sessionStorageEvaluator.setSessionStorageEnabled(false);
    return sessionStorageEvaluator;
}
复制代码
  • 自定义LoginRealm

LoginRealm 只负责处理登录请求,它的实现逻辑就是获取请求接口中账号密码,通过数据库查询,并将查询结果保存到redis,供后续接口请求时验证。

/**
* 查询数据库,将获取到的用户安全数据封装返回(伪代码)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    LoginToken token = (LoginToken) authenticationToken;
    String username = token.getUsername();
    String password = new String((char[]) authenticationToken.getCredentials());
​
    //查询是否存在当前用户,有效用户查询结果只能为1
    SysUser user = loginService.getUserByNameAndPwd(username, password);
    if(user == null){
        throw new AuthException(LOGIN_PWD_ERROR.getCode(), "登录验证失败, 用户名或密码错误");
    }
    // 缓存当前登录用户信息
    redisUtil.setex(key, value);
    return new SimpleAuthenticationInfo(jwtEntity, password, getName());
}
复制代码
  • 自定义CustomRealm

上面的LoginReaml会将登陆成功的用户信息缓存,此后接口每次调用后台时,都会在header请求头中携带token字段,用于鉴权,而鉴权就是在CustomRealm中进行,伪代码如下:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.获取请求token
    CustomerToken jwtToken = (CustomerToken) authenticationToken;
    
    ....省略
    //2.从jwt中解析出用户名、密码
    String username = "";
​
    //3.取出redis中用户信息
    String jwtCache = redisUtil.get(StringUtils.joinWith(SYMBOL_UNDERLINE, REDIS_KEY_PREFIX_LOGIN_ACCOUNT, username), 0);
    
    //4.对缓存信息进行处理
    if (jwtCache == null) {
        log.warn("[获取密码缓存失败, 查询数据库 account = {}]", username);
    } else {
        log.error("[当前账号在其他地方登录, account = {}]", username);
    } else if (!StringUtils.equals(password, cacheJwtObj.getPassword())) {
        log.error("缓存密码校验失败");
    }
}
return new SimpleAuthenticationInfo(jwtObject, jwtToken.getCredentials(), getName());
}
​
复制代码
  • 配置拦截内容

有些请求是不需要被拦截的,像注册这类接口及一些静态资源(css、图片、js等),这时我们就需要在shiro中提前设置,示例代码如下:

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//登录地址
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功后要跳转的连接
shiroFilterFactoryBean.setSuccessUrl("/authorized");
//未授权跳转地址
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
​
//增加自定义过滤器(JwtFilter)
Map<String, Filter> filterMap = Maps.newHashMapWithExpectedSize(1);
filterMap.put(JwtFilter.class.getName(), new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
​
//配置资源访问权限(anon 表示资源都可以匿名访问)
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", JwtFilter.class.getName());
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
复制代码
  • 登录测试

这里我们获取到前端传入的用户名、密码,封装到LoginToken中调用subject.login()方法,这时就会触发LoginRealm进行鉴权,为什么会触发LoginRealm而不是CustomRealm,我们上面已经详细介绍了。

LoginToken loginToken = new LoginToken(username,password);
Subject subject = SecurityUtils.getSubject();
//登录操作(前往LoginRealm鉴权)
subject.login(usernamePasswordToken);
//返回前端token信息
JwtEntity jwtEntity = (JwtEntity) subject.getPrincipal();
return new SysUserLoginDto(JwtUtils.createJwtToken(JSON.toJSONString(jwtEntity)), jwtEntity.getLoginId());
复制代码

测试结果,可以拿到 jwt生成的token值,后续再次请求时,需要携带该token进行鉴权:

{"token":"eyJhbGciOiJIUzI1NiJ9.eyJqd3RLZXkiOiJ7XCJhY2NvdW50XCI6XCIxXCIsXCJsb2dpbklkXCI6MSxcInBhc3N3b3JkXCI6XCIweEMwbWZJVGR4MjJ3ejFKMVU1c3pnPT1cIixcInNlc3Npb25JZFwiOlwiODg2MjcyNDkyOTE0NjA2MTJcIn0iLCJleHAiOjE2MjY5MjI5MDJ9.VF8R9X3hZb6SDvShZbRdSRgwaAUUE7dC7XQhIuWBSn4","loginId":1}
复制代码

五、前端项目搭建

前端使用的是ant-design-vue2.0版本,虽然写过不少前端项目,也用过其他框架,如elementUi。但是对于一个后端码农来说,我的审美有点low,平时颜色搭配属于红配绿那种....这里也就不详细介绍前端的搭建过程了,掘金社区有非常多非常优秀的前端大佬,小伙伴们可以去学习学习。

image.png

image.png

总结

篇幅有点长,能坚持看到这里的小伙伴都是 真大佬,但是如果看完后还是觉得晦涩难懂,可以直接下载花Gie开源出来的标准版本项目,对照着调试。如果有其他想法也可以联系花Gie,如果恰巧我也知道的花,肯定会给小伙伴们解答的,今天的分享就到这了,下期还有什么好玩的呢,关注我,继续肝。

如果小伙伴们有其他好的想法,可以来花Gie的开源小队,一起研究技术,一起学习,非常欢迎大家的加入

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花Gie ,有问题大家随时留言讨论 ,我们下期见🦮。

文章持续更新,可以微信搜一搜 花哥编程 第一时间阅读,并且可以获取面试资料学习视频等,有兴趣的小伙伴欢迎关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你觉得这篇文章对你有点用的话,感谢老铁为本文点个赞、评论或转发一下,因为这将是我输出更多优质文章的动力,感谢!

文章分类
后端
文章标签