1.前期设计
1.1 功能模块的设计
用户权限管理系统一般包括以下模块:
1.2 数据库的设计
根据基本的功能可以总结出6张数据库表:
- sys_permission:权限表
- sys_role:角色表
- sys_role_permission:角色-权限关联表
- sys_user:用户表
- sys_user_role:用户角色关联表
- notify:通知表
接下来就是创建数据库acs和表的具体设计了。
1.sys_permission:
CREATE TABLE `sys_permission`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`permission_code` varchar(255) NOT NULL COMMENT '权限的代码/通配符',
`permission_name` varchar(255) NOT NULL COMMENT '权限的中文释义',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_permission_code` (`permission_code`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='权限表';
权限表用于将系统中所有支持进行权限控制的资源及其相关操作进行声明。
WildcardPermission 是 Shiro Permission 接口的一个实现,它允许我们将权限表示为通配符的方式,通配符字符串能作为方法参数传递给 Subject.isPermitted 以完成权限检查。例如:
- “notify:edit”:表示 “编辑通知” 权限。
- “notify:*”:表示对通知的任何操作。
接下来需要将系统中所有的权限都定义好,然后插入到数据库中。
INSERT INTO `sys_permission`(id, permission_code, permission_name)
VALUES (101, 'notify:list', '通知列表'),
(102, 'notify:add', '创建通知'),
(103, 'notify:edit', '编辑通知'),
(104, 'notify:delete', '删除通知'),
(301, 'user:list', '用户列表'),
(302, 'user:add', '新增用户'),
(303, 'user:edit', '编辑用户'),
(304, 'user:delete', '删除用户'),
(501, 'role:list', '角色列表'),
(502, 'role:add', '新增角色'),
(503, 'role:edit', '编辑角色'),
(504, 'role:delete', '删除角色');
2.sys_role:
CREATE TABLE `sys_role`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`role_name` varchar(20) NOT NULL COMMENT '角色名',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_name` (`role_name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='角色表';
我们对具有不同权限的系统用户进行分类,将其分为不同的角色,并保存到角色表中,如 ”通知管理员“,”权限管理员“等。这样我们只需要为用户关联其对应的角色,就可以完成用户的权限分配。
接下来为角色表创建超级管理员这个角色。
INSERT INTO `sys_role`(id, role_name)
VALUES (1, '超级管理员');
3.sys_user:
CREATE TABLE `sys_user`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`username` varchar(255) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`nickname` varchar(255) NULL DEFAULT NULL COMMENT '昵称',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE = InnoDB
AUTO_INCREMENT = 10000
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='用户表';
用户表记录了用户的身份信息,这里记录着登录时对登录者进行身份认证所需标准凭证(用户名,密码)。
接下来为用户表创建超级管理员用户。
INSERT INTO `sys_user`(id, username, password, nickname)
VALUES (10001, 'admin', '123456', '超级管理员');
4.sys_role_permission:
CREATE TABLE `sys_role_permission`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`role_id` int(4) NOT NULL COMMENT '角色id',
`permission_id` int(4) NOT NULL COMMENT '权限id',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_id_permission_id` (`role_id`, `permission_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='角色-权限关联表';
一个角色可以拥有多个权限,这些关联关系将由单独的关联表负责:角色-权限关联表。
接下来角色-权限关联表创建后为超级管理员分配所有权限:
INSERT INTO `sys_role_permission`(role_id, permission_id)
VALUES (1, 101),
(1, 102),
(1, 103),
(1, 104),
(1, 301),
(1, 302),
(1, 303),
(1, 304),
(1, 501),
(1, 502),
(1, 503),
(1, 504);
5.sys_user_role :
CREATE TABLE `sys_user_role`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`user_id` int(4) NOT NULL COMMENT '用户id',
`role_id` int(4) NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 10000
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='用户-角色关联表';
一个用户可以拥有多个角色,这些关联关系将由单独的关联表负责:用户-角色关联表。
接下来创建用户-角色关联表后为将超级管理员的角色与用户关联:
INSERT INTO `sys_user_role`(user_id, role_id)
VALUES (10001, 1);
6.notify:
CREATE TABLE `notify`
(
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`user_id` int(4) NOT NULL COMMENT '创建用户id',
`content` text NOT NULL COMMENT '通知内容',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT ='通知表';
2.初始化项目
继续使用上一篇文章中创建的项目,添加必要的初始依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
2.1 创建分页工具类
对于需要进行分页的数据集,我们需要为其定义一个统一的数据结构,这里将其定义到 PageModel.java 中。创建common包,在该包下创建PageModel.java类。
@Data
public class PageModel<T> implements Serializable {
private int pageNum;
private int pageSize;
private long total;
private int pages;
private List<T> list;
}
2.2 统一异常处理
1.导致请求处理失败的错误:由于应用存在未解决的 bug 或依赖的资源报错而引起的错误,如试图打开不存在文件导致的文件 io 错误;网络请求超时,网络 io 错误;或者是强依赖的三方资源报错;甚至是操作系统 OOM,SOF 等。如果遇到这些错误,用户的请求往往无法继续处理,我们能做的只有给用户提示请求处理失败。
2.非法用户输入引起的错误:用户(这里的用户不仅仅指实际用户,也指 API 使用者)输入是不可预测的,难免输入系统无法处理的内容,我们应该尽可能多的主动监测这类错误,监测到时先中断请求,并提示用户需要进行修正,然后才能再次发起请求。
3.进行处理后仍有可能使请求成功完成的错误:这类错误在 java 中我们可以通过 try - catch - finally 进行处理。
在common包下创建两类异常,用于作为上述异常的代表。
ServiceException.java
public class ServiceException extends RuntimeException {
public ServiceException() {
}
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
UserInputException
public class UserInputException extends RuntimeException {
public UserInputException(String message) {
super(message);
}
}
接着提供统一的返回结果类,出现异常错误,同样需要使用统一的返回结果返回数据,这里使用使用Spring Boot自带的返回响应包装类。在common包下创建Results.java类。
Results
public class Results {
public static ResponseEntity success() {
ResponseEntity rm = new ResponseEntity(HttpStatus.OK);
return rm;
}
public static ResponseEntity userInputError(String msg) {
ResponseEntity rm = new ResponseEntity(msg, HttpStatus.BAD_REQUEST);
return rm;
}
public static ResponseEntity error(String msg) {
ResponseEntity rm = new ResponseEntity(msg, HttpStatus.INTERNAL_SERVER_ERROR);
return rm;
}
public static <T extends Serializable> ResponseEntity<T> success(T data) {
ResponseEntity<T> rm = new ResponseEntity<T>(data, HttpStatus.OK);
return rm;
}
}
然后在 Spring 提供的 ControllerAdvice 中统一处理,创建api包,在该包下创建ExceptionController.java类,使用 ControllerAdvice 统一处理错误。
ExceptionController
@Slf4j
@ControllerAdvice
public class ExceptionController {
/**
* 出现UserInputException异常时,该方法会处理请求返回响应
* @param e
* @return
*/
@ExceptionHandler({UserInputException.class})
@ResponseBody
public ResponseEntity userInputException(UserInputException e) {
log.warn(e.getMessage());
return Results.userInputError(e.getMessage());
}
/**
* 出现ServiceException异常时,该方法会处理请求返回响应
* @param e
* @return
*/
@ExceptionHandler({ServiceException.class})
@ResponseBody
public ResponseEntity serviceException(ServiceException e) {
log.error("未知错误", e);
return Results.error("未知错误");
}
}
2.3 集成Swagger2接口文档
swagger 可以帮助我们自动生成 API 文档,节省了维护文档的时间,并且可一完成接口的基本测试。
2.3.1 添加依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
2.3.2 启用 Swagger2
在 Application.java 中添加 @EnableSwagger2 注解启用 Swagger2。
@SpringBootApplication
@EnableSwagger2
public class SpringbootshiroApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootshiroApplication.class, args);
}
}
2.3.3 进行简要测试
在包api下,创建testController.java类,在该类中使用Swagger2接口文档。
@RestController
public class testController {
/**
* 简要测试Swagger2
* @return
*/
@GetMapping("/hello")
@ApiOperation("简单的测试")
public String test(){
return "hello world";
}
}
启动后打开 http://localhost:8080/swagger-ui.html 页面。此时出现项目启动报错,我们看看具体的错误:
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
出现以上错误,是由Springfox的一个bug引起的。具体来说,它假设MVC的路径匹配将使用基于Ant-based的路径匹配器,而不是基于PathPattern-based的匹配器。基于PathPattern-based的匹配已经是一个选项,并且是SpringBoot2.6的默认选项。
因此,如Spring Boot 2.6发行说明中所述,您可以通过在application.properties文件中将Spring.mvc.pathmatch.matching-strategy设置为ant path matcher来恢复Springfox假定将使用的配置。请注意,只有在不使用Spring Boot的执行器时,此功能才起作用。无论配置的匹配策略如何,执行器始终使用基于路径模式的解析。如果您想在Spring Boot 2.6及更高版本中将其与执行器一起使用,则需要对Springfox进行更改。
所以我们在主配置中添加一个配置:
# 解决集成Swagger2后启动报错
spring.mvc.pathmatch.matching-strategy = ant_path_matcher
继续启动后打开 http://localhost:8080/swagger-ui.html 页面。
可以看到Swagger2接口文档被集成到Spring Boot项目中。
3.使用mybatis逆向工程
这里持久层使用mybatis框架,该框架可以使用简单的配置自动生成必要的Entity,Dao,Mapper文件。
3.1 添加依赖及插件
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- mybatis-generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
</plugin>
3.2 添加 generatorConfig.xml 配置文件
在resources/generator 目录下创建 generatorConfig.xml 配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 数据库驱动:选择你的本地数据库驱动包 -->
<classPathEntry location="/Users/xuzhi/devTools/local-maven/mysql/mysql-connector-java/8.0.22/mysql-connector-java-8.0.22.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressDate" value="true"/>
<!-- 是否去除自动生成的注释 true:是; false:否 -->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 数据库链接URL,用户名、密码 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/acs" userId="root" password="12345678">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 生成模型的包名和位置 -->
<javaModelGenerator targetPackage="com.picacho.springbootshiro.entity" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成映射文件的包名和位置 -->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- 生成DAO的包名和位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.picacho.springbootshiro.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 要生成的表 tableName 是数据库中的表名或视图名 domainObjectName 是实体类名 -->
<table tableName="sys_permission" domainObjectName="SysPermission" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"></table>
<table tableName="sys_role" domainObjectName="SysRole" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"/>
<table tableName="sys_role_permission" domainObjectName="SysRolePermission" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"/>
<table tableName="sys_user" domainObjectName="SysUser" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"/>
<table tableName="sys_user_role" domainObjectName="SysUserRole" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"/>
<table tableName="notify" domainObjectName="Notify" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false"/>
</context>
</generatorConfiguration>
按照如下方式启动插件,生成我们需要的代码,如下图为生成后的文件。
注意生成的Dao接口还没有被注入到Spring容器中,所以需要我们手动添加注解,提示Sprin扫描这些Dao注入到容器中。
- 第一种是在每个Dao接口上添加@Mapper注解。
- 第二种在启动类上添加@MapperScan注解,并且指定扫描路径,这里是"com.picacho.springbootshiro.dao"。
这里我们选择第二种较为方便些。
3.3 添加mybatis配置
在resources文件夹下添加mybatis-config.xml文件,配置生成Entity,Dao,Mapper文件的基本配置。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="useGeneratedKeys" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="SLF4J"/>
<setting name="cacheEnabled" value="true"/>
<setting name="localCacheScope" value="SESSION"/>
</settings>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 -->
<!--<property name="dialect" value="mysql"/>-->
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="false"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="false"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="none"/>
</plugin>
</plugins>
</configuration>
- useGeneratedKeys:允许 JDBC 支持自动生成主键,这样 Mapper(Mapper 主要有两种形式:XML 和 java 注解)中的 insert 操作会将自增的 id 回填到 Entity 中。useGeneratedKeys 是一个全局配置,但只对接口映射器(注解的方式)有效,对 XML 无效,因此在 XML 中我们还是需要为每个 insert 语句进行下面的指定:
<insert id="insert" useGeneratedKeys="true" keyProperty="id"></insert>
- mapUnderscoreToCamelCase:将下划线风格的数据库列名自动映射为 java 字段的驼峰风格。
- logImpl:将 SQL 执行语句通过日志打印出来。
- localCacheScope:控制一级缓存生效范围,有 SESSION 和 STATEMENT 两种。MyBatis 会将 Mapper 中的查询语句()映射为 MappedStatement,执行查询时 MappedStatement 及其查询参数和返回结果会被缓存,下次同样的查询被执行时可以直接从缓存中获取结果。
- cacheEnabled:控制二级缓存,一级缓存的最大生效范围是 SESSION,如果需要在多个 SESSION(即跨 SqlSession 实例)中进行缓存共享,就需要启用二级缓存。
3.4 在主配置文件种添加数据库相关配置
spring.datasource.url = jdbc:mysql://localhost:3306/acs?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 12345678
spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
4.Shiro配置
4.1 引入依赖
在Spring Boot项目中,Shiro 官方提供了专门的 Starter:shiro-spring-boot-web-starter依赖来简化集成。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
4.2 配置Shiro
在主配置文件中对Shiro进行配置。
# Shiro配置
shiro.web.enabled = true
shiro.sessionManager.sessionIdCookieEnabled = true
shiro.sessionManager.sessionIdUrlRewritingEnabled = true
shiro.loginUrl = /login.html
- web.enabled 控制是否启用 Shiro 框架,true 表示启用,在项目开发阶段为了跳过安全检查可以设置为 false。
- sessionManager.sessionIdCookieEnabled:启用或停用通过 cookie 的会话状态保持,false 表示不允许将 sessionID(实际上是 JSESSIONID)放到 cookie 中。
- sessionManager.sessionIdUrlRewritingEnabled:是否允许将 sessionID 放到 URL 中,以 URL 参数的方式进行传递,当sessionManager.sessionIdCookieEnabled 设置为 true 时该项可以设为 false。
- loginUrl 参数用来指定登录页面,当用户访问没有权限的 URL 时将自动跳转到该页面。
4.3 实现ACSRealm
Realm 是 Shiro 的核心组件,是用户身份信息,权限以及角色信息的提供者,从系统数据源,例如mysql中获取数据,然后转交给 Shiro 进行身份认证和权限管理,开发中一般基于其抽象子类 AuthorizingRealm 进行扩展。
创建config包,在该包下创建ACSRealm.java类,ACSRealm 将继承 AuthorizingRealm 。AuthorizingRealm 定义了两个抽象方法:doGetAuthorizationInfo 和 doGetAuthenticationInfo,这两个方法是 Shiro 进行身份认证和授权的关键。
认证
@Component
public class ACSRealm extends AuthorizingRealm {
@Autowired
private SysUserMapper sysUserMapper;
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
SysUser user = sysUserMapper.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
String pwd = user.getPassword();
String password = new String(((char[]) token.getCredentials()));
if (!pwd.equals(password)) {
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(username, pwd, getName());
}
}
/**
* 通过用户名查询用户
* @param username
* @return
*/
SysUser findByUsername(String username);
<select id="findByUsername" resultType="com.picacho.springbootshiro.entity.SysUser">
select
<include refid="Base_Column_List"/>
from sys_user
where username = #{username}
</select>
授权
@Component
public class ACSRealm extends AuthorizingRealm {
@Autowired
private SysUserMapper sysUserMapper;
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Collection<Permission> permissions = loadPermissions(principals);
authorizationInfo.addObjectPermissions(permissions);
return authorizationInfo;
}
private Collection<Permission> loadPermissions(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
List<String> ps = sysUserMapper.findAllPermissions(username);
if (CollectionUtils.isEmpty(ps)) {
return null;
}
return ps.stream().map(WildcardPermission::new).collect(Collectors.toList());
}
}
/**
* 通过用户名查询权限
* @param username
* @return
*/
List<String> findAllPermissions(String username);
<select id="findAllPermissions" resultType="java.lang.String">
SELECT sp.permission_code
FROM sys_user su
LEFT JOIN sys_user_role sur ON su.id = sur.user_id
LEFT JOIN sys_role_permission srp ON sur.role_id = srp.role_id
LEFT JOIN sys_permission sp ON srp.permission_id = sp.id
WHERE username = #{username}
</select>
4.4 配置 SecurityManager 和过滤器
SecurityManager 的配置比较简单,主要是将我们自己的 ACSRealm 配置给 SecurityManager。在config包下创建ShiroConfig.java类。
@Configuration
public class ShiroConfig {
@Bean
DefaultWebSecurityManager securityManager(ACSRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
return manager;
}
}
接下来需要配置 Shiro 过滤器,Shiro 过滤器可以指定对 URL 路径应用什么样的访问控制策略,如控制某一 URL 允许匿名访问,或者需要完成身份认证(登录)后才能访问等。
@Bean
ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// swagger 文档相关的接口允许匿名访问
chainDefinition.addPathDefinition("/swagger**", "anon");
chainDefinition.addPathDefinition("/webjars**", "anon");
// 登录接口允许匿名访问
chainDefinition.addPathDefinition("/api/login", "anon");
// 所有 api 开头的接口都需要登录
chainDefinition.addPathDefinition("/api/**", "authc");
return chainDefinition;
}
Shiro 的过滤器都定义在 org.apache.shiro.web.filter 包中,有唯一的简写字符串与之对应,常见的过滤器有下面几种:
- anon:允许匿名访问。
- authc:需要完成身份认证(登录)后才能访问。
- perms:可指定访问者必须具备的权限,如:“perms[role:add, user:list]”,表示访问者需要同时具备 “role:add” 和 “user:list” 权限才能访问当前路径。
- user:必须存在用户,身份认证通过或通过“记住我”认证通过的都可以访问。
4.5 简单测试一下
创建controller包,并在该包下创建LoginController类中,添加跳转至login.html的方法。
@RestController
public class LoginController {
@GetMapping("/login.html")
public String loginPage() {
return "<p><h1>" +
"模拟登录界面" +
"</h1><h5>" +
"有两种情况该页面被打开:" +
"</h5><ul><li>" +
"用户主动打开" +
"</li><li>" +
"无权限时自动跳转" +
"</li></ul></p>";
}
}
测试一下:访问此路径http://localhost:8080/api/hello,由于没有权限会被映射到http://localhost:8080/login.html路径,因此会出现如下画面。
demo源码下载地址:demo源码下载