In the previous article I introduced the basic method of configuring Shiro in the Java EE environment, but in the real development process we basically do not
will use profile-based user role configuration, in most cases we will store users, roles and permissions in the database, and then we tell Shiro to go to the database to fetch data, so the configuration is more flexible and more powerful.
So that the Shiro can read the database (or LDAP, file system, etc.) of the components called realm, you can think of realm as a security-specific DAO, let me explain in detail how to configure realm:
(The technology used is similar to the one in the previous article, in addition, we used JPA, project source reference my GitHub)
Add dependency
On the basis of the dependencies used in the previous article, we also need to add:
<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>
Add entities such as user ER diagram
Here I define the relationship of the user, role, and Permissions (Permission) as:
For simplicity, the ER diagram omits the table's fields, which only describe the relationships between the tables: one user can have multiple roles, and one role has multiple permissions.
JPA Entities
src/main/java/com/github/holyloop/entity/User.java
:
@Id @ Generatedvalue (strategy = Generationtype.) @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 );
Here for the password
ciphertext password, salt
for the hash encryption used in the salt, we let each user has a random 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< /span>) private String roleName; @OneToMany (Mappedby = "role" ) private set<userrolerel> userrolerels = new hashset<> (0 ); @OneToMany (Mappedby = "role" ) private set< rolepermissionrel> rolepermissionrels = new hashset<> (0 );
roleName
As the name describes, the permission name.
src/main/java/com/github/holyloop/entity/Permission.java
:
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Columntruefalse)private Long id;@Column"permission_str"false)private String permissionStr;
permissionStr
As a permission string, I generally prefer to define a permission string as a resource-type:operation:instance
permission that means an operation (operation) resource (resource-type) instance (instance), for example: org:update:1
is the "Update organization with ID 1" permission. This kind of organization authority is more flexible, can be accurate to the resource instance , can use this way in the system that the permission request teaches high, generally to the system that the permission does not require accurate to the resource instance uses the traditional RBAC to satisfy the request. A detailed definition of permissions can be found in the Shiro documentation.
The remaining two intermediate relational tables are simpler and are not explained in detail here.
Modify Shiro.ini
src/main/webapp/WEB-INF/shiro.ini
:
[main]Authc.loginurl=/loginCredentialsmatcher=Org.apache.shiro.authc.credential.Sha256CredentialsMatchercredentialsmatcher.storedcredentialshexencoded= falsecredentialsmatcher.hashiterations= 1024x768Dbrealm=Com.github.holyloop.secure.shiro.realm.DBRealmDbrealm.credentialsmatcher=$credentialsMatcherSecuritymanager.realms=$dbRealm[URLs]/index.html=Anon/login=authc
Compared to the previous configuration, I deleted the [users]
and [roles]
block, because this data we will load from the database. In addition, I configured the Shiro password match credentialsMatcher
, it will use the sha256 to do the password match; and then there's the focus of this article dbRealm
, this real M will be implemented by ourselves.
Achieve Realm
We DBRealm
will inherit Authorizingrealm and then overwrite its doGetAuthorizationInfo
and doGetAuthenticationInfo
methods, these two methods are used to obtain permission information and authentication information:
src/main/java/com/github/holyloop/secure/shiro/realm/DBRealm.java
:
@OverrideprotectedAuthorizationinfoDogetauthorizationinfo(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);returnAuthorizationinfo;}@OverrideprotectedAuthenticationInfoDogetauthenticationinfo(Authenticationtoken token)throwsauthenticationexception {String username = stringutils.Trim(String) token.Getprincipal());if(StringUtils.IsEmpty(username)) {Throw NewAuthenticationexception (); } User user = UserService.Getonebyusername(username);if(User = =NULL) {Throw NewAuthenticationexception (); } Simpleauthenticationinfo AuthenticationInfo =New Simpleauthenticationinfo(username, user.)GetPassword(),New Simplebytesource(Base64.Decode(User.GetSalt())),GetName());returnAuthenticationInfo;}
doGetAuthorizationInfo
The user's role and permission data are loaded, and the user's doGetAuthenticationInfo
authentication data (cipher + salt) is found based on the password entered by the user.
In addition, both methods are used UserService
, and its implementation is relatively simple, such as getOneByUsername
:
publicgetOneByUsername(String username) { if (StringUtils.isEmpty(username)) { thrownew IllegalArgumentException("username must not be null"); } try { return userRepository.getOneByUsername(username); catch (NoResultException e) { returnnull; }}
UserService
is dependent UserRepository
, it is a JPA-based database access interface, specific implementation details can refer to my GitHub source code.
Add user
Up to this point, the user certification and authentication section has been basically completed. But only the authentication function is incomplete for a system, and we also need to be able to add new users. Let's UserService
add an interface to add a new user:
Public void AddUser(userdto user)throwsusernameexistedexception {if(User = =NULL) {Throw NewIllegalArgumentException ("User must not being 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
As the exception name describes, the exception is thrown when the user name entered by the new user is repeated with the existing user. The UserDTO
structure is as follows:
src/main/java/com/github/holyloop/dto/UserDTO.java
:
private String username;private String password;
Here I define it very simple, the actual project development when it will certainly have more properties, such as mailbox, birthday, mobile phone number and so on.
Another important feature is password encryption, where I implemented CredentialEncrypter
this cipher:
src/main/java/com/github/holyloop/secure/shiro/util/CredentialEncrypter.java
:
Private StaticRandomNumberGenerator rng =New Securerandomnumbergenerator();/*** hash + salt* * @param plaintextpassword* PlainText Password* @return*/ Public StaticTwotuple<string, string>Saltedhash(String Plaintextpassword) {if(StringUtils.IsEmpty(Plaintextpassword)) {Throw NewIllegalArgumentException ("Plaintextpassword must not being null"); } Object salt = rng.nextbytes(); String hash =New Sha256hash(Plaintextpassword, salt,1024x768).toBase64();return NewTwotuple<> (hash, salt.toString());}
For higher security, we generate a random salt for each user RandomNumberGenerator
, saltedHash
encrypt the plaintext password and the random salt Sha256 and then Base64 encode. The method returns the ciphertext password and the random salt.
Here, the entire system of encryption and decryption function is basically completed, then we have a simple application.
Basic app Add Rest interface
Here we implement an interface to add a new user:
src/main/java/com/github/holyloop/rest/controller/UserController.java
:
@RequiresPermissions("User:insert")@POST@Consumes(mediatype.Application_json)@Produces(mediatype.Application_json) PublicResponseAddUser(userdto user) {if(User = =NULL|| StringUtils.IsEmpty(User.GetUserName()) || StringUtils.IsEmpty(User.GetPassword())) {returnResponse.Status(Status.bad_request).Entity("username or password must not is null").Build(); } user.Setusername(StringUtils.Trim(User.GetUserName())); User.SetPassword(StringUtils.Trim(User.GetPassword()));Try{UserService.AddUser(user); }Catch(Usernameexistedexception e) {returnResponse.Status(Status.bad_request).Entity("username existed").Build(); }returnResponse.Status(Status.OK).Build();}
I have implemented this interface in a jax-rs style, and the logic is simple: verifying that the user input is legitimate, calling to add the user if it is valid, UserService.addUser()
and returning 200 when the add succeeds; it is important to note that I add an annotation to the interface that @RequiresPermissions("user:insert")
indicates that the caller of the interface needs to have .
Deployment test
Next we'll apply the deployment and use curl for a few simple tests.
- To test the defined
@RequiresPermissions("user:insert")
permission requirements:
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
Here I log in as Guest user, the user does not have "User:insert" permission, can see the request to add the user interface response to 403,:
Then change to a user who has the "User:insert" permission to log on:
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
You can see that the response is 200, and then we go to the database and look at the newly added "NewUser" User:
You can see that the user was added successfully, and the password is ciphertext stored
curl -i -d "username=newuser&password=123" -c cookies "localhost:8080/shiro-db/login"
Login successful.
- Add the new user repeatedly
We define in the logic of adding users that if the user name repeats, the new user should add the failure:
As we expected, the server response was 400 and prompted us to "username existed".
The above is Shiro database-based users, roles, permissions configuration method, the core is the realm of the configuration, realm is responsible for obtaining the user's authentication and authorization information; In order to improve the storage security of authentication information, we also encrypt the password (which is necessary for a complete system). For a slightly larger system, pure database-based certification/authentication mechanism is not enough, in order to improve efficiency also need to add some caching mechanism, this article for the time being here, about the cache, we talk about later.
Java EE 7 Database-based Apache Shiro configuration