標籤:except token uil 緩衝 group 方式 mapped iter apache
上一篇文章我介紹了在Java EE環境中配置Shiro的基本方法, 但是在真正的開發過程中我們基本上不
會使用基於設定檔的使用者角色配置, 大多數情況下我們會將使用者, 角色和許可權儲存在資料庫中, 然後我們告訴Shiro去資料庫中取資料, 這樣的配置更靈活且功能更強大.
這樣使Shiro能讀資料庫(或LDAP, 檔案系統等)的組件叫做Realm, 可以把Realm看作是一個安全專用的DAO, 下面我詳細介紹一下如何配置Realm:
(所用到的技術和上一篇文章中的類似, 此外, 我們用到了JPA, 項目源碼參考我的Github)
添加依賴
在上一篇文章所用的依賴基礎上, 我們還需要添加:
<dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> <version>1.0.0.Final</version></dependency><dependency> <groupId>org.jboss.spec.javax.ejb</groupId> <artifactId>jboss-ejb-api_3.2_spec</artifactId> <version>1.0.0.Final</version> <scope>provided</scope></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version></dependency>
添加使用者等實體ER圖
這裡我將使用者(User), 角色(Role), 許可權(Permission)的關係定義為:
為簡單起見, ER圖省略了表的欄位, 這裡僅說明表之間的關係: 一個使用者可以有多個角色, 一個角色有多個許可權.
JPA實體
src/main/java/com/github/holyloop/entity/User.java
:
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(unique = true, nullable = false)private Long id;@Column(nullable = false, unique = true)private String username;@Column(nullable = false)private String password;@Column(nullable = false)private String salt;@OneToMany(mappedBy = "user")private Set<UserRoleRel> userRoleRels = new HashSet<>(0);
這裡的 password
為密文密碼, salt
為雜湊加密用到的鹽, 我們讓每個使用者都有一個隨機鹽.
src/main/java/com/github/holyloop/entity/Role.java
:
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(unique = true, nullable = false)private Long id;@Column(name = "role_name", nullable = false, unique = true)private String roleName;@OneToMany(mappedBy = "role")private Set<UserRoleRel> userRoleRels = new HashSet<>(0);@OneToMany(mappedBy = "role")private Set<RolePermissionRel> rolePermissionRels = new HashSet<>(0);
roleName
如名字所述, 為許可權名.
src/main/java/com/github/holyloop/entity/Permission.java
:
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Column(unique = true, nullable = false)private Long id;@Column(name = "permission_str", nullable = false)private String permissionStr;
permissionStr
為許可權字串, 我一般偏愛將許可權字串定義為 resource-type:operation:instance
, 意為操作(operation)資源(resource-type)執行個體(instance)的許可權, 例如: org:update:1
即為"更新id為1的組織"許可權. 這種組織許可權的方式較靈活, 能精確到資源執行個體, 在對許可權要求教高的系統中可以使用這種方式, 一般對於許可權不要求精確到資源執行個體的系統用傳統的RBAC即可滿足要求. 關於許可權的詳細定義可參考Shiro的這個文檔.
剩下的兩個中間關係表較簡單, 這裡不詳細說明.
修改shiro.ini
src/main/webapp/WEB-INF/shiro.ini
:
[main]authc.loginUrl = /logincredentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatchercredentialsMatcher.storedCredentialsHexEncoded = falsecredentialsMatcher.hashIterations = 1024dbRealm = com.github.holyloop.secure.shiro.realm.DBRealmdbRealm.credentialsMatcher = $credentialsMatchersecurityManager.realms = $dbRealm[urls]/index.html = anon/login = authc
比較之前的配置, 我刪掉了 [users]
和 [roles]
塊, 因為這些資料我們將從資料庫中載入. 此外, 我配置了Shiro的密碼匹配器 credentialsMatcher
, 它將用sha256來做密碼匹配; 然後還有我們這篇文章的重點 dbRealm
, 這個Realm將由我們自己來實現.
實現Realm
我們的 DBRealm
將繼承AuthorizingRealm, 然後覆蓋它的 doGetAuthorizationInfo
和 doGetAuthenticationInfo
方法, 這兩個方法分別用來擷取許可權資訊和認證資訊:
src/main/java/com/github/holyloop/secure/shiro/realm/DBRealm.java
:
@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = StringUtils.trim((String) principals.getPrimaryPrincipal()); Set<String> roles = userService.listRolesByUsername(username); Set<String> permissions = userService.listPermissionsByUsername(username); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = StringUtils.trim((String) token.getPrincipal()); if (StringUtils.isEmpty(username)) { throw new AuthenticationException(); } User user = userService.getOneByUsername(username); if (user == null) { throw new AuthenticationException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( username, user.getPassword(), new SimpleByteSource(Base64.decode(user.getSalt())), getName()); return authenticationInfo;}
doGetAuthorizationInfo
將載入使用者的角色和許可權資料, doGetAuthenticationInfo
根據使用者輸入的口令尋找對應使用者的認證資料(密文密碼+鹽).
此外, 這兩個方法都用到了 UserService
, 它的實現比較簡單, 如 getOneByUsername
:
public User getOneByUsername(String username) { if (StringUtils.isEmpty(username)) { throw new IllegalArgumentException("username must not be null"); } try { return userRepository.getOneByUsername(username); } catch (NoResultException e) { return null; }}
UserService
則依賴 UserRepository
, 它是基於JPA的資料庫提供者, 具體的實現細節可以參考我的Github上的源碼.
添加使用者
到上面為止, 使用者認證及鑒權的部分已經基本完成了. 但只有鑒權功能對一個系統來說是不完整的, 我們還需要能添加新的使用者. 我們給 UserService
添加一個添加新使用者的介面:
public void addUser(UserDTO user) throws UsernameExistedException { if (user == null) { throw new IllegalArgumentException("user must not be null"); } try { User duplicateUser = userRepository.getOneByUsername(user.getUsername()); if (duplicateUser != null) { throw new UsernameExistedException(); } } catch (NoResultException e) {} TwoTuple<String, String> hashAndSalt = CredentialEncrypter.saltedHash(user.getPassword()); User entity = new User(); entity.setUsername(user.getUsername()); entity.setPassword(hashAndSalt.t1); entity.setSalt(hashAndSalt.t2); userRepository.save(entity);}
UsernameExistedException
如該異常名所描述, 當新使用者輸入的使用者名稱與已有的使用者重複時拋出該異常. UserDTO
結構如下:
src/main/java/com/github/holyloop/dto/UserDTO.java
:
private String username;private String password;
這裡我將它定義得很簡單, 實際項目開發時它肯定會有更多的屬性, 比如郵箱, 生日, 手機號等等.
此外很重要的一個功能是密碼加密, 這裡我實現了 CredentialEncrypter
這個加密器:
src/main/java/com/github/holyloop/secure/shiro/util/CredentialEncrypter.java
:
private static RandomNumberGenerator rng = new SecureRandomNumberGenerator();/*** hash + salt* * @param plainTextPassword* 純文字密碼* @return*/public static TwoTuple<String, String> saltedHash(String plainTextPassword) { if (StringUtils.isEmpty(plainTextPassword)) { throw new IllegalArgumentException("plainTextPassword must not be null"); } Object salt = rng.nextBytes(); String hash = new Sha256Hash(plainTextPassword, salt, 1024).toBase64(); return new TwoTuple<>(hash, salt.toString());}
為了更高的安全性, 我們為每個使用者都產生一個隨機鹽 RandomNumberGenerator
, saltedHash
將純文字密碼和隨機鹽Sha256加密然後base64編碼. 方法將返回密文密碼和隨機鹽.
到這裡, 整個系統的加密和解密功能就算基本完成了, 接下來我們進行一個簡單的應用.
基本應用添加REST介面
這裡我們實現一個添加新使用者的介面:
src/main/java/com/github/holyloop/rest/controller/UserController.java
:
@RequiresPermissions("user:insert")@POST@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)public Response addUser(UserDTO user) { if (user == null || StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) { return Response.status(Status.BAD_REQUEST).entity("username or password must not be null").build(); } user.setUsername(StringUtils.trim(user.getUsername())); user.setPassword(StringUtils.trim(user.getPassword())); try { userService.addUser(user); } catch (UsernameExistedException e) { return Response.status(Status.BAD_REQUEST).entity("username existed").build(); } return Response.status(Status.OK).build();}
我這裡以jax-rs的風格實現了這個介面, 實現邏輯很簡單: 校正使用者輸入是否合法, 合法則調用 UserService.addUser()
添加該使用者, 添加成功時返回200; 需要注意的是我在該介面上添加一個 @RequiresPermissions("user:insert")
註解, 這個註解說明介面的調用者需要有 user-insert
的許可權.
部署測試
接下來我們將應用部署並用curl進行幾個簡單的測試.
- 測試定義的
@RequiresPermissions("user:insert")
許可權要求:
curl -i -d "username=guest&password=guest" -c cookies "localhost:8080/shiro-db/login"curl -i -X POST -H "content-type:application/json" -b cookies > -d ‘{"username":"newuser", "password":"123"}‘ > localhost:8080/shiro-db/api/user
這裡我以guest使用者登入, 該使用者沒有 "user:insert" 的許可權, 可以看到請求添加使用者介面響應為403, :
然後換一個擁有 "user:insert" 許可權的使用者登入:
curl -i -d "username=root&password=secret" -c cookies "localhost:8080/shiro-db/login"curl -i -X POST -H "content-type:application/json" -b cookies > -d ‘{"username":"newuser", "password":"123"}‘ > localhost:8080/shiro-db/api/user
可以看到響應為200, 然後我們去資料庫中看一下有沒有新添加的 "newuser" 使用者:
可以看到使用者添加成功, 並且密碼為密文儲存
curl -i -d "username=newuser&password=123" -c cookies "localhost:8080/shiro-db/login"
登入成功.
我們在添加使用者的邏輯裡面定義了如果使用者名稱重複, 則新使用者應添加失敗:
如我們所期望的, 伺服器響應為400並提示我們"username existed".
以上就是Shiro基於資料庫的使用者,角色,許可權配置的方法, 核心的地方在於Realm的配置, Realm負責擷取使用者的認證和鑒權資訊; 為了提高認證資訊的儲存安全性, 我們還進行了密碼的加密處理(對於一個完備的系統來說這必不可少); 對於規模稍大的系統來說, 純基於資料庫的認證/鑒權機制是不夠的, 為了提高效率還需要添加一些緩衝機制, 這篇文章暫時說到這裡, 關於緩衝, 我們後面再聊.
Java EE 7基於資料庫的Apache Shiro配置