springboot2集成shiro认证鉴权(上篇)
使用shiro有段時間了,相比springsecurity,shiro要更輕量化,雖說功能不及springsecurity那么強大,但也足夠用了。本次將記錄一下springboot2與shiro的集成過程,將分為三篇來進行講述,第一篇是項目的基礎增刪改查,第二篇則是使用session進行認證,第三篇則是去除session,采用無狀態的jwt進行認證。由于水平有限,所以對于原理不會太深入講解,有興趣的大佬可自行上網搜索。
springboot2集成shiro上篇:項目基礎環境搭建
1、新建項目
使用Spring Initializr快速新建maven項目,并添加相應的依賴,pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.7</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.ygr</groupId><artifactId>shiro-boot-session</artifactId><version>0.0.1-SNAPSHOT</version><name>shiro-boot-session</name><description>shiro-boot-session</description><properties><java.version>1.8</java.version><mybatis-plus-boot-starter.version>3.4.3.4</mybatis-plus-boot-starter.version><hutool-all.version>5.7.17</hutool-all.version><knife4j-spring-boot-starter.version>3.0.3</knife4j-spring-boot-starter.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>${knife4j-spring-boot-starter.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus-boot-starter.version}</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool-all.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build> </project>簡單解釋一下上面用到的依賴,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<T> {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 <T> ApiResult<T> ok() {return new ApiResult<>(null, HttpStatus.OK, null, true);}public static <T> ApiResult<T> ok(T data) {return new ApiResult<>(data, HttpStatus.OK, null, true);}public static <T> ApiResult<T> ok(T data, String message) {return new ApiResult<>(data, HttpStatus.OK, message, true);}public static <T> ApiResult<T> ok(T data, HttpStatus status, String message) {return new ApiResult<>(data, status, message, true);}public static <T> ApiResult<T> error(String message) {return new ApiResult<>(null, HttpStatus.INTERNAL_SERVER_ERROR, message, false);}public static <T> ApiResult<T> error(HttpStatus status, String message) {return new ApiResult<>(null, status, message, false);}public static <T> ApiResult<T> error(HttpStatus status, String message, T data) {return new ApiResult<>(null, status, message, false);} }對于列表查詢,因為可能涉及到分頁,所以格式也需要進行約定,代碼如下
@Data @NoArgsConstructor @AllArgsConstructor @Builder public class PageResp<T> {private Boolean paging;private Long pageNum;private Long pageSize;private Long pageCount;private Long totalCount;private List<T> list; }3、應用配置
(1)application.yml
配置很簡單,不過多解釋,代碼如下
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%2B8username: ${MYSQL_USERNAME:root}password: ${MYSQL_PASSWORD:root}driver-class-name: com.mysql.cj.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSourcehikari:minimum-idle: 5connection-test-query: SELECT 1 FROM DUALmaximum-pool-size: 20auto-commit: trueidle-timeout: 30000pool-name: ShiroBootSessionHikariCPmax-lifetime: 60000connection-timeout: 30000jackson:date-format: yyyy-MM-dd HH:mm:sslocale: zhtime-zone: GMT+8serialization:WRITE_DATES_AS_TIMESTAMPS: falsemybatis-plus:configuration:map-underscore-to-camel-case: trueglobal-config:db-config:id-type: autologging: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: infocom.ygr: debug(2)跨域配置
由于是前后端分離項目,所以跨域問題是必須要處理的,跨域的配置方式較多,這里選擇如下方式進行配置
@Configuration public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 添加映射路徑registry.addMapping("/**")// 是否發送Cookie.allowCredentials(true)// 設置放行哪些原始域 SpringBoot2.4.4下低版本使用.allowedOrigins("*").allowedOriginPatterns("*")// 放行哪些請求方式// .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH","OPTIONS")// 放行全部.allowedMethods("*")// 放行哪些原始請求頭部信息.allowedHeaders("*")// 暴露哪些原始請求頭部信息.exposedHeaders("*")// 預檢請求有效時間.maxAge(3600);} }(3)mybatisplus配置
mybatisplus需要配置的地方不多,在上面的application.yml中已經配了部分了,這里主要配置一下字段填充以及分頁插件。字段填充需要配合注解使用,如@TableField(value = "create_time", fill = FieldFill.INSERT),@TableField(value = "update_time", fill = FieldFill.UPDATE, update = "current_timestamp")
@Configuration public class MybatisPlusConfig {/*** 分頁插件** @return mybatisPlusInterceptor*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}/*** 插入和更新時,自動填充時間字段** @return metaObjectHandler*/@Beanpublic MetaObjectHandler metaObjectHandler() {return new MetaObjectHandler() {@Overridepublic void insertFill(MetaObject metaObject) {this.strictInsertFill(metaObject, "createTime", Date::new, Date.class);}@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);}};} }(4)knife4j配置
為了便于接口測試,引入了knife4j,配置方式與swagger沒太大區別。配置掃描路徑時,可以一次性將整個項目的controller都掃描出來,但個人建議還是按模塊來進行掃描,有多個模塊就配置多個Docket
@EnableSwagger2 @Configuration public class Knife4jConfig {@Beanpublic 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<UaUserInfo> {@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;/*** 用戶狀態[1-正常,2-鎖定]*/@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;/*** 獲取主鍵值** @return 主鍵值*/@Overridepublic Serializable pkVal() {return this.id;}} -
mapper
@Mapper public interface UaUserInfoMapper extends BaseMapper<UaUserInfo> {} -
service
public interface UaUserInfoService extends IService<UaUserInfo> {/*** 獲取加密后的密碼** @param password 明文密碼* @return 加密后的密碼*/String encryptPassword(String password); } -
serviceImpl
@Service public class UaUserInfoServiceImpl extends ServiceImpl<UaUserInfoMapper, UaUserInfo> implements UaUserInfoService {@Overridepublic 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;/*** 列表查詢用戶信息** @param needPage 是否需要分頁* @param pageSize 分頁大小* @param pageNum 分頁頁碼* @param params 查詢條件* @return 查詢結果*/@ApiOperation("列表查詢")@GetMapping("/ua-user-info")public ApiResult<PageResp<UaUserInfo>> 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<String, String> params) {UaUserInfo entity = BeanUtil.mapToBean(params, UaUserInfo.class, false, new CopyOptions().setIgnoreCase(false).setIgnoreError(true));QueryWrapper<UaUserInfo> queryWrapper = new QueryWrapper<>(entity);if (needPage) {if (pageNum <= 0 || pageSize <= 0) {return ApiResult.error(HttpStatus.BAD_REQUEST, "分頁參數錯誤!");}Page<UaUserInfo> page = this.service.page(new Page<>(pageNum, pageSize), queryWrapper);PageResp<UaUserInfo> resp = PageResp.<UaUserInfo>builder().paging(true).pageNum(page.getCurrent()).pageSize(page.getSize()).pageCount(page.getPages()).totalCount(page.getTotal()).list(page.getRecords()).build();return ApiResult.ok(resp);}PageResp<UaUserInfo> resp = PageResp.<UaUserInfo>builder().paging(false).list(this.service.list(queryWrapper)).build();return ApiResult.ok(resp);}/*** 通過主鍵查詢用戶信息** @param id 主鍵* @return 單條數據*/@ApiOperation("通過主鍵查詢")@GetMapping("/ua-user-info/{id}")public ApiResult<UaUserInfo> getOne(@PathVariable("id") Serializable id) {return ApiResult.ok(this.service.getById(id));}/*** 新增用戶信息** @param entity 實體對象* @return 新增結果*/@ApiOperation("新增")@PostMapping("/ua-user-info")public ApiResult<UaUserInfo> insert(@RequestBody @Valid UaUserInfo entity) {this.service.save(entity);return ApiResult.ok(entity);}/*** 通過主鍵更新用戶信息** @param entity 實體對象* @return 修改結果*/@ApiOperation("通過主鍵更新")@PutMapping("/ua-user-info")public ApiResult<UaUserInfo> update(@RequestBody @Valid UaUserInfo entity) {ApiResult<Void> 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()));}/*** 通過主鍵刪除用戶信息** @param id 主鍵* @return 刪除結果*/@ApiOperation("通過主鍵刪除")@DeleteMapping("/ua-user-info/{id}")public ApiResult<Void> 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<Void> 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的集成過程。
總結
以上是生活随笔為你收集整理的springboot2集成shiro认证鉴权(上篇)的全部內容,希望文章能夠幫你解決所遇到的問題。