正文
在安全领域,Subject 用来指代与系统交互的实体,可以是用户、第三方应用等,它是安全认证框架(例如 Shiro)验证的主题。 Principal 是 Subject 具有的属性,例如用户名、身份证号、电话号码、邮箱等任何安全验证过程中关心的要素。 Primary principal 指能够唯一区分 Subject 的属性,例如身份证号码,论坛系统中的登录名等,通过它可以唯一识别一个 Subject。 Credential 是认证过程中与 Principal 一同提交到系统的信息,通常是只有 Subject 知道的加密信息,例如密码、PGP Key等。
应用系统或者说安全认证框架验证一个 Subject 的过程为:
- Subject 提供 principal(例如用户名)和 credential(例如密码)
- 安全认证框架(例如 Shiro)会验证 Subject 提供的信息与保存在应用系统中的信息(例如存储在数据库或者 LDAP 中)是否匹配。 若匹配,则认为 Subject 为合法用户;否则,为非法用户。
01-RBAC 基于角色的访问控制
Role-Based Access Control(RBAC)是最普遍的权限设计模型。 它包含了三个实体:
- 用户
- 角色
- 权限
我们定义三张表,来存储这三个实体:
CREATE TABLE `demo_user` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`username` varchar(20) NOT NULL COMMENT '帐号',
`password` varchar(32) NOT NULL COMMENT '密码MD5(密码+盐)',
`locked` tinyint(4) DEFAULT NULL COMMENT '状态(0:正常,1:锁定)',
`ctime` bigint(20) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
-- 插入两条数据,表示两个用户
INSERT INTO demo_user (user_id, username, password, locked, ctime)
VALUES (1, 'admin', 'admin', '0', sysdate()),
(2, 'lihua', 'lihua123', '0', sysdate()),
(3, 'hanmeimei', 'hanmeimei123', '0', sysdate());
CREATE TABLE `demo_role` (
`role_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(20) DEFAULT NULL COMMENT '角色名称',
`description` varchar(1000) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色';
-- 插入两条数据,表示两个角色
INSERT INTO demo_role(role_id, name, description)
VALUES (1, 'admin', '管理员'),
(2, 'user', '普通用户');
CREATE TABLE `demo_permission` (
`permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(20) DEFAULT NULL COMMENT '名称',
`permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
`status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限';
-- 插入三条数据,表示三种不同的权限
INSERT INTO demo_permission(permission_id, name, permission_value, status)
VALUES (1, '新增用户', 'user:add', 1),
(2, '删除用户', 'user:delete', 1),
(3, '查看用户', 'user:get', 1);
实体之间具有如下的关系:
- 角色权限,一对多,一个角色可以具有多个权限。
- 用户角色,一对多,一个用户可以具有多个角色。
- 用户权限,一对多,一个用户有多个权限。权限的来源有两种,一类是直接赋予它某些权限,另一类是通过赋予它多个角色而赋予它角色关联的权限。
我们定义三张表,来存储上述三种关系:
CREATE TABLE `demo_role_permission` (
`role_permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`role_id` int(10) unsigned NOT NULL COMMENT '角色编号',
`permission_id` int(10) unsigned NOT NULL COMMENT '权限编号',
PRIMARY KEY (`role_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=129 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限关联表';
-- 插入四条条数据,admin 具有增、删、查用户权限,user 具有查用户权限
INSERT INTO demo_role_permission(role_permission_id, role_id, permission_id)
VALUES (1, 1, 1),
(2, 1, 2),
(3, 1, 3),
(4, 2, 3);
CREATE TABLE `demo_user_role` (
`user_role_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`user_id` int(10) unsigned NOT NULL COMMENT '用户编号',
`role_id` int(10) DEFAULT NULL COMMENT '角色编号',
PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';
-- 插入三条数据
INSERT INTO demo_user_role (user_role_id, user_id, role_id)
VALUES (1, 1, 1),
(2, 2, 2),
(3, 3, 2);
CREATE TABLE `demo_user_permission` (
`user_permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
`user_id` int(10) unsigned NOT NULL COMMENT '用户编号',
`permission_id` int(10) unsigned NOT NULL COMMENT '权限编号',
PRIMARY KEY (`user_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户权限关联表';
-- 插入五条数据
INSERT INTO demo_user_permission (user_permission_id, user_id, permission_id)
VALUES (1, 1, 1),
(2, 1, 2),
(3, 1, 3),
(4, 2, 3),
(5, 3, 3);
02-Shiro 中基于 JdbcRealm 实现用户认证、授权
Shiro 中 Realm 是负责与应用系统中的权限模型打交道的组件,所以它也被称为 Security DAO(Data Access Object)。 Shiro 中 Realm 的类型设计结构图如下所示:
AuthenticatingRealm 和 AuthorizingRealm 分别实现了认证、授权的整体流程,将如何获取存储认证信息、权限信息通过模板方法方式留给派生类去实现:
- AuthenticatingRealm#doGetAuthenticationInfo,如何获取系统存储的认证信息,例如用户、密码等。对应上节中的 demo_user 表。
- AuthorizingRealm#doGetAuthorizationInfo,如何获得用户的角色、权限信息,对应上节中的 demo_role、demo_permission 表。
接下来,我详细分析下 Shiro 提供的一个基于数据库的实现类 JdbcRealm。 简化后的 doGetAuthenticationInfo 流程:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 用户提交上来的信息
String username = ((UsernamePasswordToken) token).getUsername();
Connection conn = null;
SimpleAuthenticationInfo info = null;
try {
conn = dataSource.getConnection(); // 数据库引用
// 从数据库中获取密码
String password = getPasswordForUser(conn, username)[0]; // 关键点1
// 创建验证结果
info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
} catch (SQLException e) {
final String message = "There was a SQL error while authenticating user [" + username + "]";
throw new AuthenticationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}
return info;
}
简化后的 doGetAuthorizationInfo 方法:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 这里的 principals 主要为获得用户名
String username = (String) getAvailablePrincipal(principals);
Connection conn = null;
Set<String> roleNames = null;
Set<String> permissions = null;
try {
conn = dataSource.getConnection(); // 数据库引用
// Retrieve roles and permissions from database
roleNames = getRoleNamesForUser(conn, username); // 关键点2
if (permissionsLookupEnabled) {
permissions = getPermissions(conn, username, roleNames); // 关键点3
}
} catch (SQLException e) {
final String message = "There was a SQL error while authorizing user [" + username + "]";
throw new AuthorizationException(message, e);
} finally {
JdbcUtils.closeConnection(conn);
}
// 创建权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
info.setStringPermissions(permissions);
return info;
}
注:从 JdbcRealm 的源码中能够发现如下几点:
- 关键点1,需要一个 SQL 语句,从数据库表中查询用户的密码。结合上节中定义的表,这个 SQL 语句为
SELECT password FROM demo_user WHERE username = ?
。 - 关键点2,需要从数据库中查询用户的角色,SQL 语句为
SELECT name FROM demo_user_role ur, demo_user u, demo_role r WHERE ur.user_id = u.user_id AND ur.role_id = r.role_id AND u.username = ?
。 - 关键点3,需要根据角色列表从数据库中查询角色具备的权限集合,SQL 语句为
SELECT permission_value FROM demo_role_permission rp, demo_role r, demo_permission p WHERE rp.role_id = r.role_id AND rp.permission_id = p.permission_id AND r.name = ?
。 - 其他点,我们需要一个数据库引用,即 DataSource 对象。要查询权限,需要
permissionsLookupEnabled == true
。
综上,我们需要对 JdbcRealm 对象进行设置,使其能够获取到我们存储在数据库中的信息。
@Bean
public Realm realm(@Autowired DataSource dataSource) {
final JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
String authQuery = "SELECT password FROM demo_user WHERE username = ?";
jdbcRealm.setAuthenticationQuery(authQuery);
jdbcRealm.setPermissionsLookupEnabled(true);
String roleQuery = "SELECT name FROM demo_user_role ur, demo_user u, demo_role r WHERE ur.user_id = u.user_id AND ur.role_id = r.role_id AND u.username = ?";
String permissionQuery = "SELECT permission_value FROM demo_role_permission rp, demo_role r, demo_permission p WHERE rp.role_id = r.role_id AND rp.permission_id = p.permission_id AND r.name = ?";
jdbcRealm.setUserRolesQuery(roleQuery);
jdbcRealm.setPermissionsQuery(permissionQuery);
return jdbcRealm;
}
除了使用 JdbcRealm 的方法外,还可以仿照它编写我们自己的实现。接下来,我将结合 Spring Data JPA 编写一个 JpaRealm。
public class JpaRealm extends AuthorizingRealm {
@Autowired
private DemoUserRepository userRepository;
@Autowired
private DemoUserRoleRepository userRoleRepository;
@Autowired
private DemoRolePermissionRepository rolePermissionRepository;
@Autowired
private DemoPermissionRepository permissionRepository;
@Autowired
private DemoRoleRepository roleRepository;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 查询权限的过程与 JdbcRealm 一样,只不过使用了 jpa
//null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
String username = (String) getAvailablePrincipal(principals);
final DemoUser user = this.userRepository.findByUsername(username)
.orElseThrow(() -> new UnknownAccountException("No account found for user [" + username + "]"));
final List<Integer> roleIds = userRoleRepository.findByUserId(user.getUserId()).stream()
.map(DemoUserRole::getUserRoleId)
.collect(Collectors.toList());
final Set<String> roleNames = roleRepository.findAllById(roleIds).stream()
.map(DemoRole::getName)
.collect(Collectors.toSet());
final List<Integer> permissionIds = rolePermissionRepository.findAllByRoleIdIn(roleIds).stream()
.map(DemoRolePermission::getPermissionId)
.collect(Collectors.toList());
final Set<String> permissions = permissionRepository.findAllById(permissionIds).stream()
.map(DemoPermission::getPermissionValue)
.collect(Collectors.toSet());
final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
info.setStringPermissions(permissions);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 查询身份信息的过程与 JdbcRealm 一样
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
try {
final DemoUser user = this.userRepository.findByUsername(username)
.orElseThrow(() -> new UnknownAccountException("No account found for user [" + username + "]"));
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, user.getPassword().toCharArray(), getName());
return info;
} catch (Throwable t) {
final String message = "There was a SQL error while authenticating user [" + username + "]";
// Rethrow any SQL errors as an authentication exception
throw new AuthenticationException(message, t);
}
}
}
在之前初始化 JdbcRealm 的地方,换成 JpaRealm 就可以了。
@Bean
public Realm jpaRealm() {
return new JpaRealm();
}
03-集成到 Spring Boot Web 应用中
接下来,我把前两节的东西整合在一个 Spring Boot Web 应用中,并测试下效果吧。
首先,编写两个 Controller 类,以便能够从浏览器或 Postman 中访问它:
@RestController
public class LoginController {
@GetMapping("/login")
public String login() {
return "please login!";
}
@GetMapping("/index")
public String index() {
final Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
final Object principal = subject.getPrincipal();
return "hello, " + principal;
}
return "hello";
}
}
LoginController 负责处理到 "/login" 和 "/index' 的请求,主要是为了匹配 Shiro 中的 loginUrl 和 successfulUrl。
shiro:
enabled: true
web:
enabled: true
loginUrl: /login
successUrl: /index
当未登录用户访问时,会重定向到 "/login",你可以看到一个请求登录的提示。 登录成功后,会重定向到 "/index" ,并显示 "hello, ${用户名}" 提示。
注:为了偷懒,我没有写登录界面,默认情况下 Shiro 中的 AuthenticatingFilter 会处理到 loginUrl 的 POST 请求,并从中提取 principal 来进行登录验证。
// org.apache.shiro.web.filter.AccessControlFilter.isLoginRequest
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
所以,当需要登录时,只需要向 "/login" 发送一个 POST 请求即可,例如:
curl --location --request POST 'http://localhost:18886/shiro-web/login' \
--form 'username="lihua"' \
--form 'password="lihua123"'
然后,我编写了另一个 Controller 它主要用来实现对 User 的增删查操作:
@RestController
public class UserController {
@Autowired
private DemoUserRepository userRepository;
@RequiresPermissions("user:get")
@GetMapping("/users")
public List<DemoUser> all() {
final List<DemoUser> all = userRepository.findAll();
return all;
}
@RequiresPermissions("user:get")
@GetMapping("/users/{id}")
public DemoUser one(@PathVariable Integer id) {
final Optional<DemoUser> byId = userRepository.findById(id);
return byId.orElse(null);
}
@RequiresPermissions("user:add")
@PostMapping("/users")
public String add(@RequestBody DemoUser user) {
userRepository.save(user);
return "success";
}
@RequiresPermissions("user:delete")
@DeleteMapping("/users/{id}")
public String delete(@PathVariable Integer id) {
userRepository.deleteById(id);
return "success";
}
}
加上之前的代码,所有的元素我们就凑齐了,可以 run 起来检查一下了。 如果需要完整的源代码,可以在我的 gitee.com 上下载。
04-总结
今天,我介绍了如何使用 Shiro 中提供的 JdbcRealm 实现基于 RBAC 模型的权限验证。 之后,又仿照 JdbcRealm 实现了一个基于 JPA 的 Realm 实现,并将它们集成在了一个 Web 应用中进行了验证。 希望今天的内容能对你有所帮助。
以上就是Spring Boot在Web应用中基于JdbcRealm安全验证过程的详细内容,更多关于Spring Boot JdbcRealm安全验证的资料请关注编程网其它相关文章!