Spring?Boot在Web應用中基于JdbcRealm安全驗證過程
正文
在安全領域,Subject 用來指代與系統(tǒng)交互的實體,可以是用戶、第三方應用等,它是安全認證框架(例如 Shiro)驗證的主題。 Principal 是 Subject 具有的屬性,例如用戶名、身份證號、電話號碼、郵箱等任何安全驗證過程中關心的要素。 Primary principal 指能夠唯一區(qū)分 Subject 的屬性,例如身份證號碼,論壇系統(tǒng)中的登錄名等,通過它可以唯一識別一個 Subject。 Credential 是認證過程中與 Principal 一同提交到系統(tǒng)的信息,通常是只有 Subject 知道的加密信息,例如密碼、PGP Key等。
應用系統(tǒng)或者說安全認證框架驗證一個 Subject 的過程為:
- Subject 提供 principal(例如用戶名)和 credential(例如密碼)
- 安全認證框架(例如 Shiro)會驗證 Subject 提供的信息與保存在應用系統(tǒng)中的信息(例如存儲在數(shù)據(jù)庫或者 LDAP 中)是否匹配。 若匹配,則認為 Subject 為合法用戶;否則,為非法用戶。
01-RBAC 基于角色的訪問控制
Role-Based Access Control(RBAC)是最普遍的權(quán)限設計模型。 它包含了三個實體:
- 用戶
- 角色
- 權(quán)限
我們定義三張表,來存儲這三個實體:
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 '狀態(tài)(0:正常,1:鎖定)', `ctime` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建時間', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶'; -- 插入兩條數(shù)據(jù),表示兩個用戶 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='角色'; -- 插入兩條數(shù)據(jù),表示兩個角色 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 '權(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);
實體之間具有如下的關系:
- 角色權(quán)限,一對多,一個角色可以具有多個權(quán)限。
- 用戶角色,一對多,一個用戶可以具有多個角色。
- 用戶權(quán)限,一對多,一個用戶有多個權(quán)限。權(quán)限的來源有兩種,一類是直接賦予它某些權(quán)限,另一類是通過賦予它多個角色而賦予它角色關聯(lián)的權(quán)限。
我們定義三張表,來存儲上述三種關系:
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 '權(quán)限編號', PRIMARY KEY (`role_permission_id`) ) ENGINE=InnoDB AUTO_INCREMENT=129 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色權(quá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 '編號', `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='用戶角色關聯(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 '編號', `user_id` int(10) unsigned NOT NULL COMMENT '用戶編號', `permission_id` int(10) unsigned NOT NULL COMMENT '權(quán)限編號', PRIMARY KEY (`user_permission_id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用戶權(quá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 實現(xiàn)用戶認證、授權(quán)
Shiro 中 Realm 是負責與應用系統(tǒng)中的權(quán)限模型打交道的組件,所以它也被稱為 Security DAO(Data Access Object)。 Shiro 中 Realm 的類型設計結(jié)構(gòu)圖如下所示:
AuthenticatingRealm 和 AuthorizingRealm 分別實現(xiàn)了認證、授權(quán)的整體流程,將如何獲取存儲認證信息、權(quán)限信息通過模板方法方式留給派生類去實現(xiàn):
- AuthenticatingRealm#doGetAuthenticationInfo,如何獲取系統(tǒng)存儲的認證信息,例如用戶、密碼等。對應上節(jié)中的 demo_user 表。
- AuthorizingRealm#doGetAuthorizationInfo,如何獲得用戶的角色、權(quán)限信息,對應上節(jié)中的 demo_role、demo_permission 表。
接下來,我詳細分析下 Shiro 提供的一個基于數(shù)據(jù)庫的實現(xiàn)類 JdbcRealm。 簡化后的 doGetAuthenticationInfo 流程:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 用戶提交上來的信息 String username = ((UsernamePasswordToken) token).getUsername(); Connection conn = null; SimpleAuthenticationInfo info = null; try { conn = dataSource.getConnection(); // 數(shù)據(jù)庫引用 // 從數(shù)據(jù)庫中獲取密碼 String password = getPasswordForUser(conn, username)[0]; // 關鍵點1 // 創(chuàng)建驗證結(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; }
簡化后的 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ù)庫引用 // 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); } // 創(chuàng)建權(quán)限信息 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); info.setStringPermissions(permissions); return info; }
注:從 JdbcRealm 的源碼中能夠發(fā)現(xiàn)如下幾點:
- 關鍵點1,需要一個 SQL 語句,從數(shù)據(jù)庫表中查詢用戶的密碼。結(jié)合上節(jié)中定義的表,這個 SQL 語句為
SELECT password FROM demo_user WHERE username = ?
。 - 關鍵點2,需要從數(shù)據(jù)庫中查詢用戶的角色,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,需要根據(jù)角色列表從數(shù)據(jù)庫中查詢角色具備的權(quán)限集合,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 = ?
。 - 其他點,我們需要一個數(shù)據(jù)庫引用,即 DataSource 對象。要查詢權(quán)限,需要
permissionsLookupEnabled == true
。
綜上,我們需要對 JdbcRealm 對象進行設置,使其能夠獲取到我們存儲在數(shù)據(jù)庫中的信息。
@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àn)。接下來,我將結(jié)合 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) { // 查詢權(quán)限的過程與 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 應用中
接下來,我把前兩節(jié)的東西整合在一個 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" 發(fā)送一個 POST 請求即可,例如:
curl --location --request POST 'http://localhost:18886/shiro-web/login' \ --form 'username="lihua"' \ --form 'password="lihua123"'
然后,我編寫了另一個 Controller 它主要用來實現(xiàn)對 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-總結(jié)
今天,我介紹了如何使用 Shiro 中提供的 JdbcRealm 實現(xiàn)基于 RBAC 模型的權(quán)限驗證。 之后,又仿照 JdbcRealm 實現(xiàn)了一個基于 JPA 的 Realm 實現(xiàn),并將它們集成在了一個 Web 應用中進行了驗證。 希望今天的內(nèi)容能對你有所幫助。
以上就是Spring Boot在Web應用中基于JdbcRealm安全驗證過程的詳細內(nèi)容,更多關于Spring Boot JdbcRealm安全驗證的資料請關注腳本之家其它相關文章!
相關文章
Spring中@Transactional注解關鍵屬性和用法小結(jié)
在Spring框架中,@Transactional 是一個注解,用于聲明事務性的方法,它提供了一種聲明式的事務管理方式,避免了在代碼中直接編寫事務管理相關的代碼,本文給大家介紹@Transactional 注解的一些關鍵屬性和用法,感興趣的朋友一起看看吧2023-12-12解決SpringMvc中普通類注入Service為null的問題
這篇文章主要介紹了解決SpringMvc中普通類注入Service為null的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Spring Boot 2.4 對多環(huán)境配置的支持更改示例代碼
這篇文章主要介紹了Spring Boot 2.4 對多環(huán)境配置的支持更改,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12