Java EE 7基於資料庫的Apache Shiro配置

來源:互聯網
上載者:User

標籤: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, 然後覆蓋它的 doGetAuthorizationInfodoGetAuthenticationInfo 方法, 這兩個方法分別用來擷取許可權資訊和認證資訊:

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配置

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.