Spring?Boot在Web應(yīng)用中基于JdbcRealm安全驗(yàn)證過(guò)程
正文
在安全領(lǐng)域,Subject 用來(lái)指代與系統(tǒng)交互的實(shí)體,可以是用戶、第三方應(yīng)用等,它是安全認(rèn)證框架(例如 Shiro)驗(yàn)證的主題。 Principal 是 Subject 具有的屬性,例如用戶名、身份證號(hào)、電話號(hào)碼、郵箱等任何安全驗(yàn)證過(guò)程中關(guān)心的要素。 Primary principal 指能夠唯一區(qū)分 Subject 的屬性,例如身份證號(hào)碼,論壇系統(tǒng)中的登錄名等,通過(guò)它可以唯一識(shí)別一個(gè) Subject。 Credential 是認(rèn)證過(guò)程中與 Principal 一同提交到系統(tǒng)的信息,通常是只有 Subject 知道的加密信息,例如密碼、PGP Key等。
應(yīng)用系統(tǒng)或者說(shuō)安全認(rèn)證框架驗(yàn)證一個(gè) Subject 的過(guò)程為:
- Subject 提供 principal(例如用戶名)和 credential(例如密碼)
- 安全認(rèn)證框架(例如 Shiro)會(huì)驗(yàn)證 Subject 提供的信息與保存在應(yīng)用系統(tǒng)中的信息(例如存儲(chǔ)在數(shù)據(jù)庫(kù)或者 LDAP 中)是否匹配。 若匹配,則認(rèn)為 Subject 為合法用戶;否則,為非法用戶。
01-RBAC 基于角色的訪問(wèn)控制
Role-Based Access Control(RBAC)是最普遍的權(quán)限設(shè)計(jì)模型。 它包含了三個(gè)實(shí)體:
- 用戶
- 角色
- 權(quán)限
我們定義三張表,來(lái)存儲(chǔ)這三個(gè)實(shí)體:
CREATE TABLE `demo_user` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '編號(hào)',
`username` varchar(20) NOT NULL COMMENT '帳號(hào)',
`password` varchar(32) NOT NULL COMMENT '密碼MD5(密碼+鹽)',
`locked` tinyint(4) DEFAULT NULL COMMENT '狀態(tài)(0:正常,1:鎖定)',
`ctime` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶';
-- 插入兩條數(shù)據(jù),表示兩個(gè)用戶
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 '編號(hào)',
`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='角色';
-- 插入兩條數(shù)據(jù),表示兩個(gè)角色
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 '編號(hào)',
`name` varchar(20) DEFAULT NULL COMMENT '名稱',
`permission_value` varchar(50) DEFAULT NULL COMMENT '權(quán)限值',
`status` tinyint(4) DEFAULT NULL COMMENT '狀態(tài)(0:禁止,1:正常)',
PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='權(quán)限';
-- 插入三條數(shù)據(jù),表示三種不同的權(quán)限
INSERT INTO demo_permission(permission_id, name, permission_value, status)
VALUES (1, '新增用戶', 'user:add', 1),
(2, '刪除用戶', 'user:delete', 1),
(3, '查看用戶', 'user:get', 1);
實(shí)體之間具有如下的關(guān)系:
- 角色權(quán)限,一對(duì)多,一個(gè)角色可以具有多個(gè)權(quán)限。
- 用戶角色,一對(duì)多,一個(gè)用戶可以具有多個(gè)角色。
- 用戶權(quán)限,一對(duì)多,一個(gè)用戶有多個(gè)權(quán)限。權(quán)限的來(lái)源有兩種,一類是直接賦予它某些權(quán)限,另一類是通過(guò)賦予它多個(gè)角色而賦予它角色關(guān)聯(lián)的權(quán)限。
我們定義三張表,來(lái)存儲(chǔ)上述三種關(guān)系:
CREATE TABLE `demo_role_permission` (
`role_permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '編號(hào)',
`role_id` int(10) unsigned NOT NULL COMMENT '角色編號(hào)',
`permission_id` int(10) unsigned NOT NULL COMMENT '權(quán)限編號(hào)',
PRIMARY KEY (`role_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=129 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色權(quán)限關(guān)聯(lián)表';
-- 插入四條條數(shù)據(jù),admin 具有增、刪、查用戶權(quán)限,user 具有查用戶權(quán)限
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 '編號(hào)',
`user_id` int(10) unsigned NOT NULL COMMENT '用戶編號(hào)',
`role_id` int(10) DEFAULT NULL COMMENT '角色編號(hào)',
PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶角色關(guān)聯(lián)表';
-- 插入三條數(shù)據(jù)
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 '編號(hào)',
`user_id` int(10) unsigned NOT NULL COMMENT '用戶編號(hào)',
`permission_id` int(10) unsigned NOT NULL COMMENT '權(quán)限編號(hào)',
PRIMARY KEY (`user_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶權(quán)限關(guān)聯(lián)表';
-- 插入五條數(shù)據(jù)
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 實(shí)現(xiàn)用戶認(rèn)證、授權(quán)
Shiro 中 Realm 是負(fù)責(zé)與應(yīng)用系統(tǒng)中的權(quán)限模型打交道的組件,所以它也被稱為 Security DAO(Data Access Object)。 Shiro 中 Realm 的類型設(shè)計(jì)結(jié)構(gòu)圖如下所示:

AuthenticatingRealm 和 AuthorizingRealm 分別實(shí)現(xiàn)了認(rèn)證、授權(quán)的整體流程,將如何獲取存儲(chǔ)認(rèn)證信息、權(quán)限信息通過(guò)模板方法方式留給派生類去實(shí)現(xiàn):
- AuthenticatingRealm#doGetAuthenticationInfo,如何獲取系統(tǒng)存儲(chǔ)的認(rèn)證信息,例如用戶、密碼等。對(duì)應(yīng)上節(jié)中的 demo_user 表。
- AuthorizingRealm#doGetAuthorizationInfo,如何獲得用戶的角色、權(quán)限信息,對(duì)應(yīng)上節(jié)中的 demo_role、demo_permission 表。
接下來(lái),我詳細(xì)分析下 Shiro 提供的一個(gè)基于數(shù)據(jù)庫(kù)的實(shí)現(xiàn)類 JdbcRealm。 簡(jiǎn)化后的 doGetAuthenticationInfo 流程:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 用戶提交上來(lái)的信息
String username = ((UsernamePasswordToken) token).getUsername();
Connection conn = null;
SimpleAuthenticationInfo info = null;
try {
conn = dataSource.getConnection(); // 數(shù)據(jù)庫(kù)引用
// 從數(shù)據(jù)庫(kù)中獲取密碼
String password = getPasswordForUser(conn, username)[0]; // 關(guān)鍵點(diǎn)1
// 創(chuàng)建驗(yàn)證結(jié)果
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;
}
簡(jiǎn)化后的 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(); // 數(shù)據(jù)庫(kù)引用
// Retrieve roles and permissions from database
roleNames = getRoleNamesForUser(conn, username); // 關(guān)鍵點(diǎn)2
if (permissionsLookupEnabled) {
permissions = getPermissions(conn, username, roleNames); // 關(guān)鍵點(diǎn)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);
}
// 創(chuàng)建權(quán)限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
info.setStringPermissions(permissions);
return info;
}
注:從 JdbcRealm 的源碼中能夠發(fā)現(xiàn)如下幾點(diǎn):
- 關(guān)鍵點(diǎn)1,需要一個(gè) SQL 語(yǔ)句,從數(shù)據(jù)庫(kù)表中查詢用戶的密碼。結(jié)合上節(jié)中定義的表,這個(gè) SQL 語(yǔ)句為
SELECT password FROM demo_user WHERE username = ?。 - 關(guān)鍵點(diǎn)2,需要從數(shù)據(jù)庫(kù)中查詢用戶的角色,SQL 語(yǔ)句為
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 = ?。 - 關(guān)鍵點(diǎn)3,需要根據(jù)角色列表從數(shù)據(jù)庫(kù)中查詢角色具備的權(quán)限集合,SQL 語(yǔ)句為
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 = ?。 - 其他點(diǎn),我們需要一個(gè)數(shù)據(jù)庫(kù)引用,即 DataSource 對(duì)象。要查詢權(quán)限,需要
permissionsLookupEnabled == true。
綜上,我們需要對(duì) JdbcRealm 對(duì)象進(jìn)行設(shè)置,使其能夠獲取到我們存儲(chǔ)在數(shù)據(jù)庫(kù)中的信息。
@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 的方法外,還可以仿照它編寫(xiě)我們自己的實(shí)現(xiàn)。接下來(lái),我將結(jié)合 Spring Data JPA 編寫(xiě)一個(gè) 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) {
// 查詢權(quán)限的過(guò)程與 JdbcRealm 一樣,只不過(guò)使用了 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 {
// 查詢身份信息的過(guò)程與 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 應(yīng)用中
接下來(lái),我把前兩節(jié)的東西整合在一個(gè) Spring Boot Web 應(yīng)用中,并測(cè)試下效果吧。
首先,編寫(xiě)兩個(gè) Controller 類,以便能夠從瀏覽器或 Postman 中訪問(wèn)它:
@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 負(fù)責(zé)處理到 "/login" 和 "/index' 的請(qǐng)求,主要是為了匹配 Shiro 中的 loginUrl 和 successfulUrl。
shiro:
enabled: true
web:
enabled: true
loginUrl: /login
successUrl: /index
當(dāng)未登錄用戶訪問(wèn)時(shí),會(huì)重定向到 "/login",你可以看到一個(gè)請(qǐng)求登錄的提示。 登錄成功后,會(huì)重定向到 "/index" ,并顯示 "hello, ${用戶名}" 提示。
注:為了偷懶,我沒(méi)有寫(xiě)登錄界面,默認(rèn)情況下 Shiro 中的 AuthenticatingFilter 會(huì)處理到 loginUrl 的 POST 請(qǐng)求,并從中提取 principal 來(lái)進(jìn)行登錄驗(yàn)證。
// org.apache.shiro.web.filter.AccessControlFilter.isLoginRequest
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
所以,當(dāng)需要登錄時(shí),只需要向 "/login" 發(fā)送一個(gè) POST 請(qǐng)求即可,例如:
curl --location --request POST 'http://localhost:18886/shiro-web/login' \ --form 'username="lihua"' \ --form 'password="lihua123"'
然后,我編寫(xiě)了另一個(gè) Controller 它主要用來(lái)實(shí)現(xiàn)對(duì) 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 起來(lái)檢查一下了。 如果需要完整的源代碼,可以在我的 gitee.com 上下載。
04-總結(jié)
今天,我介紹了如何使用 Shiro 中提供的 JdbcRealm 實(shí)現(xiàn)基于 RBAC 模型的權(quán)限驗(yàn)證。 之后,又仿照 JdbcRealm 實(shí)現(xiàn)了一個(gè)基于 JPA 的 Realm 實(shí)現(xiàn),并將它們集成在了一個(gè) Web 應(yīng)用中進(jìn)行了驗(yàn)證。 希望今天的內(nèi)容能對(duì)你有所幫助。
以上就是Spring Boot在Web應(yīng)用中基于JdbcRealm安全驗(yàn)證過(guò)程的詳細(xì)內(nèi)容,更多關(guān)于Spring Boot JdbcRealm安全驗(yàn)證的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA SpringBoot:Cannot resolve configuration&
這篇文章主要介紹了IDEA SpringBoot:Cannot resolve configuration property配置文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot開(kāi)發(fā)案例之配置Druid數(shù)據(jù)庫(kù)連接池的示例
本篇文章主要介紹了SpringBoot開(kāi)發(fā)案例之配置Druid數(shù)據(jù)庫(kù)連接池的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
IntelliJ IDEA使用SVN分支的簡(jiǎn)單介紹
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA使用SVN分支的簡(jiǎn)單介紹,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10
Mybatis 一級(jí)緩存與二級(jí)緩存的實(shí)現(xiàn)
mybatis作為一個(gè)流行的持久化工具,緩存必然是缺少不了的組件。通過(guò)這篇文章,就讓我們來(lái)了解一下一級(jí)緩存與二級(jí)緩存的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
Springboot整合hutool驗(yàn)證碼的實(shí)例代碼
在 Spring Boot 中,你可以將 Hutool 生成驗(yàn)證碼的功能集成到 RESTful API 接口中,這篇文章主要介紹了Springboot整合hutool驗(yàn)證碼,需要的朋友可以參考下2024-08-08

