使用shiro有段时间了,相比springsecurity,shiro要更轻量化,虽说功能不及springsecurity那么强大,但也足够用了。本次将记录一下springboot2与shiro的集成过程,将分为三篇来进行讲述,第一篇是项目的基础增删改查,第二篇则是使用session进行认证,第三篇则是去除session,采用无状态的jwt进行认证。由于水平有限,所以对于原理不会太深入讲解,有兴趣的大佬可自行上网搜索。
springboot2集成shiro上篇:项目基础环境搭建
上篇-项目基础增删改查- 1、新建项目
- 2、统一返回格式
- 3、应用配置
- (1)application.yml
- (2)跨域配置
- (3)mybatisplus配置
- (4)knife4j配置
- 4、表结构
- 5、代码生成
使用Spring Initializr快速新建maven项目,并添加相应的依赖,pom.xml文件如下
4.0.0 org.springframework.boot spring-boot-starter-parent 2.5.7 com.ygr shiro-boot-session 0.0.1-SNAPSHOT shiro-boot-session shiro-boot-session 1.8 3.4.3.4 5.7.17 3.0.3 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation com.github.xiaoymin knife4j-spring-boot-starter ${knife4j-spring-boot-starter.version} org.springframework.boot spring-boot-devtools runtime true mysql mysql-connector-java runtime com.baomidou mybatis-plus-boot-starter ${mybatis-plus-boot-starter.version} cn.hutool hutool-all ${hutool-all.version} org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok
简单解释一下上面用到的依赖,knife4j有些人可能不熟悉,但说到swagger应该都懂吧,注意,这个swagger说的不是Taiwan那个哈,这里说的是swagger-ui,knife4j可以说是swagger-ui的美化增强版。至于其他的依赖,就不多解释了。
新建完成后建议设置一下SDK为JDK1.8,新版的idea中似乎默认是JDK11,会出现找不到核心类库而爆红的情况,快捷键Ctrl + Shift + Alt + S
2、统一返回格式在前后端分离趋势下,后端接口只需要返回约定格式的JSON即可,包路径为com.ygr.web,代码如下
@Data public class ApiResult{ private boolean ok; private Integer code; private String message; private T data; private ApiResult() { this.code = HttpStatus.OK.value(); } private ApiResult(T data, HttpStatus status, String message, boolean ok) { this.data = data; this.code = status.value(); this.message = message; this.ok = ok; } public static ApiResult ok() { return new ApiResult<>(null, HttpStatus.OK, null, true); } public static ApiResult ok(T data) { return new ApiResult<>(data, HttpStatus.OK, null, true); } public static ApiResult ok(T data, String message) { return new ApiResult<>(data, HttpStatus.OK, message, true); } public static ApiResult ok(T data, HttpStatus status, String message) { return new ApiResult<>(data, status, message, true); } public static ApiResult error(String message) { return new ApiResult<>(null, HttpStatus.INTERNAL_SERVER_ERROR, message, false); } public static ApiResult error(HttpStatus status, String message) { return new ApiResult<>(null, status, message, false); } public static ApiResult error(HttpStatus status, String message, T data) { return new ApiResult<>(null, status, message, false); } }
对于列表查询,因为可能涉及到分页,所以格式也需要进行约定,代码如下
@Data @NoArgsConstructor @AllArgsConstructor @Builder public class PageResp3、应用配置 (1)application.yml{ private Boolean paging; private Long pageNum; private Long pageSize; private Long pageCount; private Long totalCount; private List list; }
配置很简单,不过多解释,代码如下
spring:
datasource:
url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/shiro-boot-session?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadonly=false&serverTimezone=GMT%2B8
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:root}
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
connection-test-query: SELECt 1 FROM DUAL
maximum-pool-size: 20
auto-commit: true
idle-timeout: 30000
pool-name: ShiroBootSessionHikariCP
max-lifetime: 60000
connection-timeout: 30000
jackson:
date-format: yyyy-MM-dd HH:mm:ss
locale: zh
time-zone: GMT+8
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
logging:
pattern:
console: '%date{yyyy-MM-dd HH:mm:ss.SSS} | %highlight(%5level) [%green(%16.16thread)] %clr(%-50.50logger{49}){cyan} %4line -| %highlight(%msg%n)'
level:
root: info
com.ygr: debug
(2)跨域配置
由于是前后端分离项目,所以跨域问题是必须要处理的,跨域的配置方式较多,这里选择如下方式进行配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加映射路径
registry.addMapping("
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public metaObjectHandler metaObjectHandler() {
return new metaObjectHandler() {
@Override
public void insertFill(metaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date::new, Date.class);
}
@Override
public void updateFill(metaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);
}
};
}
}
(4)knife4j配置
为了便于接口测试,引入了knife4j,配置方式与swagger没太大区别。配置扫描路径时,可以一次性将整个项目的controller都扫描出来,但个人建议还是按模块来进行扫描,有多个模块就配置多个Docket
@EnableSwagger2
@Configuration
public class Knife4jConfig {
@Bean
public Docket uaRestApi() {
return new Docket(documentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("ua模块 api文档")
.description("shiro-boot-session api")
.version("1.0")
.build())
.select()
.apis(RequestHandlerSelectors.basePackage("com.ygr.modules.ua.controller"))
.paths(PathSelectors.any())
.build();
}
}
4、表结构
认证授权使用的是经典的RBAC模型,涉及的表结构如下
create database if not exists `shiro-boot-session` default character set utf8mb4 collate utf8mb4_general_ci;
use `shiro-boot-session`;
drop table if exists ua_user_info;
create table ua_user_info
(
id bigint auto_increment primary key,
name varchar(32) not null unique comment '用户名',
password varchar(256) not null comment '密码',
history_name varchar(1024) comment '历史名称',
status tinyint default 1 comment '用户状态[1-正常,2-锁定]',
phone varchar(32) comment '电话',
email varchar(128) comment '邮箱',
remark varchar(1024) comment '备注',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime comment '变更时间'
) comment '用户信息';
drop table if exists ua_role_info;
create table ua_role_info
(
id bigint auto_increment primary key,
code varchar(32) not null unique comment '角色编号',
name varchar(64) not null comment '角色名称',
status tinyint not null default 1 comment '角色状态[1-正常,2-禁用]',
remark varchar(1024) comment '备注',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime comment '变更时间'
) comment '角色信息';
drop table if exists ua_authority_info;
create table ua_authority_info
(
id bigint auto_increment primary key,
parent_id bigint not null default -1 comment '上级id',
name varchar(64) not null comment '权限名称',
uri varchar(256) not null comment 'URI',
type tinyint not null comment '类型[1-菜单,2-按钮/api]',
perm_tag varchar(64) comment '权限标识',
group_name varchar(32) comment '分组',
status tinyint not null default 1 comment '状态',
view varchar(256) comment '视图',
hide bit not null default 0 comment '掩藏',
icon varchar(64) comment '图标',
sort int default 0 comment '排序',
remark varchar(1024) comment '备注',
create_time datetime default current_timestamp comment '创建时间',
update_time datetime comment '变更时间'
) comment '权限信息';
drop table if exists ua_user_role_relation;
create table ua_user_role_relation
(
id bigint auto_increment primary key,
user_id bigint not null comment '用户id',
role_id bigint not null comment '角色id',
create_time datetime default current_timestamp comment '创建时间'
) comment '用户角色关联关系';
create index ua_user_role_relation_user_id on ua_user_role_relation (user_id);
create index ua_user_role_relation_role_id on ua_user_role_relation (role_id);
drop table if exists ua_role_authority_relation;
create table ua_role_authority_relation
(
id bigint auto_increment primary key,
role_id bigint not null comment '角色id',
authority_id bigint not null comment '权限id',
create_time datetime default current_timestamp comment '创建时间'
) comment '角色权限关联关系';
5、代码生成
使用mybatisplus插件或其他代码生成插件生成相应表的实体类以及对应的增删改查代码,以ua_user_info表为例,代码如下
-
entity
@Data @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) @TableName("ua_user_info") @ApiModel(value = "UaUserInfo", description = "用户信息表实体类") public class UaUserInfo extends Model{ @TableId("id") private Long id; @ApiModelProperty(value = "用户名") @TableField("name") private String name; @ApiModelProperty(value = "密码") @TableField("password") private String password; @ApiModelProperty(value = "历史名称") @TableField("history_name") private String historyName; @ApiModelProperty(value = "用户状态[1-正常,2-锁定]") @TableField("status") private Integer status; @ApiModelProperty(value = "电话") @TableField("phone") private String phone; @ApiModelProperty(value = "邮箱") @TableField("email") private String email; @ApiModelProperty(value = "备注") @TableField("remark") private String remark; @ApiModelProperty(value = "创建时间") @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @ApiModelProperty(value = "变更时间") @TableField(value = "update_time", fill = FieldFill.UPDATE, update = "current_timestamp") private Date updateTime; @Override public Serializable pkVal() { return this.id; } } -
mapper
@Mapper public interface UaUserInfoMapper extends baseMapper
{ } -
service
public interface UaUserInfoService extends IService
{ String encryptPassword(String password); } -
serviceImpl
@Service public class UaUserInfoServiceImpl extends ServiceImpl
implements UaUserInfoService { @Override public String encryptPassword(String password) { Sha256Hash hash = new Sha256Hash(password, AuthConstant.SECRET_SALT, 1024); return hash.tobase64(); } } AuthConstant定义如下
public interface AuthConstant { String SECRET_SALT = "my-secret-salt"; } -
controller
@Api(tags = "用户信息") @Validated @RequiredArgsConstructor @RestController public class UaUserInfoController { private final UaUserInfoService service; @ApiOperation("列表查询") @GetMapping("/ua-user-info") public ApiResult> queryList(@RequestParam(value = "needPage", required = false, defaultValue = "false") boolean needPage, @RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize, @RequestParam(value = "pageNum", required = false, defaultValue = "1") int pageNum, @RequestParam(required = false) Map params) { UaUserInfo entity = BeanUtil.mapToBean(params, UaUserInfo.class, false, new CopyOptions().setIgnoreCase(false).setIgnoreError(true)); QueryWrapper queryWrapper = new QueryWrapper<>(entity); if (needPage) { if (pageNum <= 0 || pageSize <= 0) { return ApiResult.error(HttpStatus.BAD_REQUEST, "分页参数错误!"); } Page page = this.service.page(new Page<>(pageNum, pageSize), queryWrapper); PageResp resp = PageResp. builder() .paging(true) .pageNum(page.getCurrent()) .pageSize(page.getSize()) .pageCount(page.getPages()) .totalCount(page.getTotal()) .list(page.getRecords()) .build(); return ApiResult.ok(resp); } PageResp resp = PageResp. builder() .paging(false) .list(this.service.list(queryWrapper)) .build(); return ApiResult.ok(resp); } @ApiOperation("通过主键查询") @GetMapping("/ua-user-info/{id}") public ApiResult getOne(@PathVariable("id") Serializable id) { return ApiResult.ok(this.service.getById(id)); } @ApiOperation("新增") @PostMapping("/ua-user-info") public ApiResult insert(@RequestBody @Valid UaUserInfo entity) { this.service.save(entity); return ApiResult.ok(entity); } @ApiOperation("通过主键更新") @PutMapping("/ua-user-info") public ApiResult update(@RequestBody @Valid UaUserInfo entity) { ApiResult checkPkVal = checkPkVal(entity); if (!checkPkVal.isOk()) { return ApiResult.error(HttpStatus.resolve(checkPkVal.getCode()), checkPkVal.getMessage()); } UaUserInfo oldData = this.service.getById(entity.pkVal()); if (oldData == null) { return ApiResult.error(HttpStatus.NOT_FOUND, "数据不存在!"); } this.service.updateById(entity); return ApiResult.ok(this.service.getById(entity.pkVal())); } @ApiOperation("通过主键删除") @DeleteMapping("/ua-user-info/{id}") public ApiResult delete(@PathVariable("id") Serializable id) { if (this.service.getById(id) == null) { return ApiResult.error(HttpStatus.NOT_FOUND, "数据不存在!"); } this.service.removeById(id); return ApiResult.ok(); } private ApiResult checkPkVal(UaUserInfo entity) { if (entity.pkVal() == null || "".equals(entity.pkVal().toString())) { return ApiResult.error(HttpStatus.BAD_REQUEST, "id不能为空!"); } return ApiResult.ok(); } }
其他的表的代码与用户表基本上一样,就不贴出来了。项目结构如下所示
到这里,项目的增删改查就OK了,启动项目,访问 http://localhost:8080/doc.html 后,可以看到如下界面,接下来就可以方便的进行接口测试了。
代码已上传至gitee,见master分支:https://gitee.com/yang-guirong/shiro-boot/tree/master/
下一篇将讲述shiro的集成过程。



