springboo2集成shiro
简介
Apache Shiro 是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码和会话管理。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
权限管理
常见的权限管理分为 基于主页的权限管理 、基于用户和权限的权限管理 以及 基于角色的访问控制(RBAC)。这里推荐使用 RBAC 模型下的权限管理。
其他权限框架
- shiro:Apache Shrio 是功能强大并且易用的 Java 安全框架
- Spring Security:基于 Spring 的安全框架,依赖 Spring
- OAuth2:第三方授权框架(QQ、微信) 自定义安全认证中心
核心功能
笔记
基本概念:
Anthentication:认证,验证用户是否有相应的身份 登录认证
Authorization:授权,权限验证,通过认证的用户检查是否有权限或者角色
Session Management:会话管理,用户在认证成功后创建会话,在没有退出之前,当前用户的所有信息都会保存在这个会话中
Cryptograsphy:加密,对敏感信息加密
笔记
特性:
1.Web Support Shrio 提供了过滤器,可以通过过滤器拦截 web 请求来处理 web 应用的访问控制 2.Caching 缓存支持,shiro 可以缓存用户信息以及用户的角色权限信息,可以提高执行效率 3.concurrency shiro 支持多线程应用 4.Run As 允许一个用户以另一种身份去访问 5.Remeber Me 记住密码 6.Testing 提供测试支持
警告
Shiro 是一个安全框架,不提供用户、权限的维护,用户的权限管理需要我们去设计,在后文的 Realm 实现方法有所体现。
笔记
核心组件:
Subject:表示待认证和授权的用户
Security Manager :Shiro 框架的核心,Shiro 就是通过 Security Manager 来进行内部实例的管理,并通过它来提供安全管理的各种服务
Realms:shiro 进行认证和授权的数据源,在这里进行身份验证和授权
- 通过 subject.login(token) 登录,将 token(用户名和密码)传递给 SecurityManager
- SecurityManager 调用 Anthenticator 进行身份认证
- Authenticator 把 token 传给对应 Realm(可以是自定义的 Realm,其中我们自己处理认证和授权的逻辑,认证我们就自己校验,授权查询数据库的用户权限进行授权,在访问需要权限的路径时会检查是否包含在我们这里授权的字段,shiro 默认提供了 JDBCRealm,但是要实现它的表结构,如果我们想自己定义表结构,就要自定义相关的 Realm),这里 shiro.ini 是一个示例,IniRealm 只是针对他的一个实现。
- Realm 根据得到的 token,调用 doGetAuthenticationInfo 方法进行认证,认证失败则抛出异常提示认证器环境搭建
- 接着将认证结果返回到上层
IDE 中创建一个 maven 的项目
此处省略…
在 maven 的 pom.xml 中添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
</dependencies>准备 Spring 配置文件
在 Spring Boot 默认资源文件夹 resource 中创建 application.yml 文件:
spring:
profiles:
active: dev
# mybatis的mapper.xml文件的位置
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml额外创建一个 dev 环境的配置文件:
# 设置启动端口号
server:
port: 8080
spring:
# 配置数据源
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/${你的数据库名称}
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 只返回不为null的数据
jackson:
default-property-inclusion: non_null创建数据库表
提示
shiro 内置的有 JDBCRealm,因此 shiro 有一套自己的数据库表的规范,如果你要使用该 Realm,则必须按照如下规范创建表结构。
用户信息表:users
create table users(
id int primary key auto_increment,
username varchar(60) not null unique,
password varchar(20) not null,
password_salt varchar(20)
);
用户角色表:user_roles
create table user_roles(
id int primary key auto_increment,
username varchar(60) not null,
role_name varchar(100) not null
);
权限信息表:roles_permissions
create table roles_permissions(
id int primary key auto_increment,
role_name varchar(100) not null,
permission varchar(100) not null
);提示
如果你使用不一样的表结构,那么就要实现你自己的 Realm 实现认证和授权的方法,通常是这样的
@Component("userRealm")
public class UserRealm extends AuthorizingRealm {
@Autowired
private IUserService iUserService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
QueryWrapper<UserPO> wrapper = Wrappers.query();
wrapper.eq("name", username);
UserPO one = iUserService.getOne(wrapper);
if (Objects.isNull(one)) {
throw new BusinessException("用户名不存在");
}
ByteSource source = ByteSource.Util.bytes(one.getSalt());
return new SimpleAuthenticationInfo(one, one.getPassword(), source, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// //获取用户的用户名
// String username = (String) principalCollection.iterator().next();
// //根据用户名查询用户角色
// Set<String> roles = roleDao.getRoleNamesByUsername(username);
// //根据用户名查询用户权限
// Set<String> permissions = permissionDao.getPermissionByUsername(username);
// SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
// info.setRoles(roles);
// info.setStringPermissions(permissions);
// return info;
//如上代码是常规的授权逻辑,以下只是一个简单授权操作的测试
//用自定义的字符串进行授权,模拟数据库查询用户的权限授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> permissions = new HashSet<>();
permissions.add("user:delete"); //给用户添加一个权限,下面的权限和该权限进行对照,在shiro配置中配置这两个路径需要权限这两个权限字段,可以采用shiro中提供的spring注解完成
// permissions.add("user:update");
info.addStringPermissions(permissions);
return info;
}
}创建一个数据库,这里取名为 shiro,只创建用户表进行测试
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '12345' COMMENT '密码',
PRIMARY KEY (`id`)
)随意插入几条数据…此处省略
创建实体类
- 数据库表对应实体类:UserPO
- 持久层接口:UserMapper
- 业务层接口及实现类:IUserservice、UserServiceImpl
- 控制器类:UserController
提示
通过注解 @MapperScan 配置好 mapper 类的扫描路径,一般配置在某个标注了 @Configuration 注解的配置类上,简单起见可在项目的入口位置配置,如下是一个实例:
@SpringBootApplication(scanBasePackages = SimpleConstants.BASE_COMPONENT_SCAN_PATH)
@ServletComponentScan(basePackages = SimpleConstants.BASE_COMPONENT_SCAN_PATH)
@EnableTransactionManagement
@MapperScan(basePackages = SimpleConstants.BASE_COMPONENT_SCAN_PATH + ".mez.bbs.server.modules.**.mapper")
@Slf4j
public class MezBBSLauncher {
}创建自定义 Realm
警告
注意,这里创建我们自己定义的 Realm,该 Realm 继承自 Shiro 提供的 AuthorizingRealm 类
@Component
public class UserRealm extends AuthorizingRealm {
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//...进行身份验证
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//...进行权限验证
return null;
}
}创建 Shiro 配置类
创建配置类
@Configuration
public class ShiroConfig {
@Bean
public IniRealm getIniRealm(){
IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
return iniRealm;
}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(IniRealm iniRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//securityManager要完成校验,需要realm
securityManager.setRealm(iniRealm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean filter=new ShiroFilterFactoryBean();
filter.setSecurityManager(defaultWebSecurityManager);
//设置shiro的拦截规则
//anon 匿名用户可访问 authc 认证用户可访问
//user 使用RemeberMe的用户可访问 perms 对应权限可访问
//role 对应的角色可访问
Map<String,String> filterMap=new HashMap<>();
filterMap.put("/","anon");
filterMap.put("/login.html","anon");
filterMap.put("/register.html","anon");
filterMap.put("/user/login","anon");
filterMap.put("/user/register","anon");
filterMap.put("/static/**","anon");
filterMap.put("/**","authc");
filter.setFilterChainDefinitionMap(filterMap);
filter.setLoginUrl("/login.html");
//设置未授权页面跳转到登录页面
filter.setUnauthorizedUrl("/login.html");
return filter;
}
}登录认证功能
控制层 UserController:
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public R<Void> login(UserLoginDTO loginDTO) { //R为Json规范响应实例,确保通过validation验证UserLoginDTO实例的参数
userService.login(loginDTO);
return R.success();
}
}业务层 IUserService 以及实现类 UserServiceImpl:
public interface IUserService {
/**
* 登录认证
* @param loginDTO 登录信息
*/
void login(UserLoginDTO loginDTO);
}@Service
public class UserServiceImpl implements IUserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public void login(UserLoginDTO loginDTO) {
String username = loginDTO.getUsername();
// 根据用户名查询用户信息
User user = userMapper.selectByUsername(username);
if (user != null) {
if (user.getIsEnable()) {
// shiro登录认证
UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(token);
// 设置session失效时间:永不超时
subject.getSession().setTimeout(-1001);
} else {
throw new GlobalException(ResponseCode.FORBIDDEN, "账号已被锁定");
}
} else {
throw new GlobalException(ResponseCode.NOT_FOUND, "用户名不存在");
}
}
}在 UserRealm 实现认证方法
@Component("userRealm")
public class UserRealm extends AuthorizingRealm {
@Autowired
private IUserService iUserService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
QueryWrapper<UserPO> wrapper = Wrappers.query();
wrapper.eq("name", username);
UserPO one = iUserService.getOne(wrapper);
if (Objects.isNull(one)) {
throw new BusinessException("用户名不存在");
}
ByteSource source = ByteSource.Util.bytes(one.getSalt());
return new SimpleAuthenticationInfo(one, one.getPassword(), source, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}测试
警告
注意要在 static 目录下创建登录页面 login.html ,在 idea IDE 中通常项目结构为:
java 这里相当于 **src**
com 源码文件存放的包位置 ( **com**命名只是一个示例 )
…
demo.java
resource 该目录存放资源文件
application.yml
static 在这里存放文件 **login.html**
login.html
html
home.html
…
js
jquery-3.7.1.js
login.js
…
创建 login.html 页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>登录shiro验证</title>
<style>
#loginForm {
width: 500px;
margin: 200px auto;
}
</style>
</head>
<body>
<form id="loginForm">
<table>
<tr>
<td>用户名</td>
<td><input type="text" id="name" /></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" id="password" /></td>
</tr>
<tr>
<td>
<button type="button" id="login">登录</button>
</td>
<td>
<button type="reset">重置</button>
</td>
</tr>
</table>
</form>
<script src="/js/jquery-3.7.1.js"></script>
<script src="/js/login.js"></script>
</body>
</html>创建 login.js 文件:
$(document).ready(function () {
$("#login").click(function () {
let username = $("#name").val();
let password = $("#password").val();
let jsonData = {
name: username,
password: password,
};
$.ajax({
url: "/user/login",
type: "POST",
headers: {
"Content-Type": "application/json",
},
contentType: "application/json",
data: JSON.stringify(jsonData),
success: function (response) {
if (response.code === 0) {
//根据实际的响应数据的code填写
location.href = "/html/home.html";
}
},
dataType: "json",
});
});
});创建 home.html 页面 :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>登录首页</title>
</head>
<body>
<h1>欢迎来到系统首页!</h1>
<button type="button" id="delete">删除</button> |
<button type="button" id="update">修改</button>
<script>
$(function () {
$("#delete").click(function () {
$.post("/user/delete", function (resp) {
alert(resp.message);
});
});
$("#update").click(function () {
$.post("/user/update", function (resp) {
alert(resp.message);
});
});
});
</script>
</body>
</html>笔记
接下来访问 localhost:${application.port}/login.html 这个地址,输入用户信息登录,如果身份验证成功跳转至主页 home.html 。
当清空浏览器缓存,刷新页面,页面会重定向到login.html。因为在 shiro 配置类里配置了 /html/home.html 要身份认证之后才能访问。即 map.put("/html/home.html", "authc")
授权功能
配置 realm 授权函数
笔记
UserRealm 里的 doGetAuthorizationInfo()方法中实现授权的代码,查询用户的权限保存到 shiro 中。
UserController 中添加对应接口
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(path = "/login", method = RequestMethod.POST)
public R<Void> login(UserLoginDTO loginDTO) {
userService.login(loginDTO);
return R.success();
}
@PostMapping("delete")
@RequiresPermissions("user:delete")
public R delete() {
return R.success("删除用户成功");
}
@PostMapping("update")
@RequiresPermissions("user:update")//这里在realm中是没为用户提供权限的,所以会访问失败
public R add() {
return R.success("更新用户成功");
}
}测试
用 postman 访问对应接口,只有拥有 realm 的授权方法中的权限才可以访问。
更新日志
8bd98-docs: 添加 blog/04_ARCHIVES 归档文档于
