<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在安全領域,Subject 用來指代與系統互動的實體,可以是使用者、第三方應用等,它是安全認證框架(例如 Shiro)驗證的主題。 Principal 是 Subject 具有的屬性,例如使用者名稱、身份證號、電話號碼、郵箱等任何安全驗證過程中關心的要素。 Primary principal 指能夠唯一區分 Subject 的屬性,例如身份證號碼,論壇系統中的登入名等,通過它可以唯一識別一個 Subject。 Credential 是認證過程中與 Principal 一同提交到系統的資訊,通常是隻有 Subject 知道的加密資訊,例如密碼、PGP Key等。
應用系統或者說安全認證框架驗證一個 Subject 的過程為:
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);
Shiro 中 Realm 是負責與應用系統中的許可權模型打交道的元件,所以它也被稱為 Security DAO(Data Access Object)。 Shiro 中 Realm 的型別設計結構圖如下所示:
AuthenticatingRealm 和 AuthorizingRealm 分別實現了認證、授權的整體流程,將如何獲取儲存認證資訊、許可權資訊通過模板方法方式留給派生類去實現:
接下來,我詳細分析下 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 的原始碼中能夠發現如下幾點:
SELECT password FROM demo_user WHERE username = ?
。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 = ?
。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 = ?
。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(); }
接下來,我把前兩節的東西整合在一個 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 上下載。
今天,我介紹瞭如何使用 Shiro 中提供的 JdbcRealm 實現基於 RBAC 模型的許可權驗證。 之後,又仿照 JdbcRealm 實現了一個基於 JPA 的 Realm 實現,並將它們整合在了一個 Web 應用中進行了驗證。 希望今天的內容能對你有所幫助。
以上就是Spring Boot在Web應用中基於JdbcRealm安全驗證過程的詳細內容,更多關於Spring Boot JdbcRealm安全驗證的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45