一、核心依賴java
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency>
二、認證流程:建立SecurityManager-->主體提交認證-->SecurityMananger認證-->Authentictor認證-->Realm驗證(從subject.login(token)開始跟蹤源碼能夠驗證(idea下ctrl+alt+b跟蹤源碼)),單元測試代碼以下:mysql
package com.example.demo_mg; import junit.framework.Assert; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; import org.apache.shiro.subject.Subject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm(); @Before public void before() { simpleAccountRealm.addAccount("wzs", "123456"); } @Test public void contextLoads() { } @Test public void test() { //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(simpleAccountRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); Assert.assertEquals(subject.isAuthenticated(), true); subject.logout(); Assert.assertEquals(subject.isAuthenticated(), true); } }
帳號錯誤UnknownAccountException,密碼錯誤IncorrectCredentialsException。算法
從subject.login(token);點擊ctrl+alt+b跟蹤源碼到DelegatingSubject的login方法,調用Subject subject = this.securityManager.login(this, token);,繼續跟蹤到DefaultSecurityManager的login方法,調用info = this.authenticate(token);,繼續跟蹤到AuthenticatingSecurityManager的authenticate方法,調用this.authenticator.authenticate(token);,繼續跟蹤到AbstractAuthenticator的authenticate方法,調用info = this.doAuthenticate(token);,繼續跟蹤到ModularRealmAuthenticator的doAuthenticate方法,能夠看到以下認證代碼,經過realm實現認證:spring
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); }
三、受權流程:建立SecurityManager-->主體受權-->SecurityManager受權-->Authorizer受權-->Realm獲取角色權限數據(數據庫|緩存等):sql
package com.example.demo_mg; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; import org.apache.shiro.subject.Subject; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm(); @Before public void before() { simpleAccountRealm.addAccount("wzs", "123456", "admin", "user"); } @Test public void contextLoads() { } @Test public void test() { //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(simpleAccountRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); //受權 subject.checkRole("admin"); subject.checkRoles("admin", "user"); subject.logout(); } }
角色不存在UnauthorizedException數據庫
從subject.checkRoles("admin", "user");點擊ctrl+alt+b跟蹤源碼到DelegatingSubject的checkRoles方法,調用this.securityManager.checkRoles(this.getPrincipals(), roleIdentifiers);,繼續跟蹤到AuthorizingSecurityManager的checkRoles方法,調用this.authorizer.checkRoles(principals, roles);,繼續跟蹤到ModularRealmAuthorizer的checkRoles方法,遍歷roles調用this.checkRole(principals, role);,繼續跟蹤到checkRole方法,調用this.hasRole(principals, role),繼續跟蹤到hasRole方法,可見以下代碼,經過realm受權:apache
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { this.assertRealmsConfigured(); Iterator var3 = this.getRealms().iterator(); Realm realm; do { if (!var3.hasNext()) { return false; } realm = (Realm)var3.next(); } while(!(realm instanceof Authorizer) || !((Authorizer)realm).hasRole(principals, roleIdentifier)); return true; }
四、測試內置的IniRealm:
在src/test下新建Direcotry,名稱resources,右鍵Mark Direcotry As-->Test Sources Root,在下面新建user.ini文件,內容:緩存
[users] wzs=123456,admin [roles] admin=user:delete,user:update
單元測試代碼:dom
package com.example.demo_mg; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { @Test public void contextLoads() { } @Test public void test() { IniRealm iniRealm = new IniRealm("classpath:user.ini"); //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(iniRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); //受權 subject.checkRole("admin"); subject.checkPermissions("user:delete", "user:update"); subject.checkPermission("user:insert"); subject.logout(); } }
權限不存在UnauthorizedExceptionide
五、測試內置的JdbcRealm:
1)安裝mysql8.0
2)按照JdbcRealm的源碼的SQL建表:
CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `password_salt` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; CREATE TABLE `user_roles` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `role_name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; CREATE TABLE `roles_permissions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_name` varchar(255) DEFAULT NULL, `permission` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
3)添加測試數據:
insert into users (username,password) values ('wzs','123456'); insert into user_roles (username,role_name) values ('wzs','admin'); insert into roles_permissions (role_name,permission) values ('admin','user:delete');
4)依賴
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.11</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency>
5)單元測試代碼:
package com.example.demo_mg; import com.alibaba.druid.pool.DruidDataSource; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { DruidDataSource dataSource = new DruidDataSource(); { dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://129.204.58.30:3306/shiro"); dataSource.setUsername("user"); dataSource.setPassword("*********"); } @Test public void contextLoads() { } @Test public void test() { JdbcRealm jdbcRealm = new JdbcRealm(); jdbcRealm.setDataSource(dataSource); jdbcRealm.setPermissionsLookupEnabled(true);//爲了查詢權限表,要開啓權限查詢 //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(jdbcRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); //受權 System.out.println(subject.isAuthenticated()); System.out.println(subject.hasRole("admin")); System.out.println(subject.isPermitted("user:delete")); subject.logout(); } }
6)若自定義用戶、角色、權限三張表,須要自定義查詢用戶、角色、權限的SQL:
參考JdbcRealm的查詢語句: protected String authenticationQuery = "select password from users where username = ?"; protected String userRolesQuery = "select role_name from user_roles where username = ?"; protected String permissionsQuery = "select permission from roles_permissions where role_name = ?"; 設置自定義查詢語句到jdbcRealm對象: jdbcRealm.setAuthenticationQuery("自定義查詢用戶sql"); jdbcRealm.setUserRolesQuery("自定義查詢角色sql"); jdbcRealm.setPermissionsQuery("自定義查詢權限sql");
六、自定義Realm,參考JdbcRealm繼承抽象的AuthorizingRealm類並實現其抽象方法,認證doGetAuthenticationInfo,受權doGetAuthenticationInfo,而後單元測試受權和認證:
package com.example.demo_mg.realm; import org.apache.commons.collections.map.HashedMap; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.*; public class TestRealm extends AuthorizingRealm { //模擬users、user_roles、roles_permissions三張表的查詢,實際應用須要查詢數據庫或緩存 Map<String, String> users = new HashMap<>(); Map<String, Set<String>> user_roles = new HashedMap(); Map<String, Set<String>> roles_permissions = new HashedMap(); { users.put("wzs", "123456"); user_roles.put("wzs", new HashSet<>(Arrays.asList("admin", "test"))); roles_permissions.put("admin", new HashSet<>(Arrays.asList("user:delete", "user:update"))); super.setName("TestRealm"); //設置Realm名稱,可選 } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //從認證信息獲取用戶名 String username = (String)principalCollection.getPrimaryPrincipal(); //從數據庫或緩存中獲取角色、權限數據 Set<String> roles = user_roles.get(username); Set<String> permissions = new HashSet<>(); for (String role : roles) { Set<String> set; if((set = roles_permissions.get(role)) != null) { permissions.addAll(set); } } SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //從主題傳過來的認證信息中,得到用戶名 String username = (String)authenticationToken.getPrincipal(); //經過用戶名從數據庫中獲取憑證 String password = users.get(username); if(password != null) { return new SimpleAuthenticationInfo(username, password, super.getName()); } return null; } }
單元測試:
package com.example.demo_mg; import com.example.demo_mg.realm.TestRealm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { @Test public void contextLoads() { } @Test public void test() { TestRealm testRealm = new TestRealm(); //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(testRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); //受權 System.out.println(subject.isAuthenticated()); System.out.println(subject.hasRole("admin")); System.out.println(subject.isPermitted("user:delete")); subject.logout(); } }
七、密碼加密及加鹽:
在6的基礎上調整代碼:
package com.example.demo_mg.realm; import org.apache.commons.collections.map.HashedMap; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.Md5Hash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import java.util.*; public class TestRealm extends AuthorizingRealm { //模擬users、user_roles、roles_permissions三張表的查詢,實際應用須要查詢數據庫或緩存 Map<String, String> users = new HashMap<>(); Map<String, Set<String>> user_roles = new HashedMap(); Map<String, Set<String>> roles_permissions = new HashedMap(); String salt = UUID.randomUUID().toString().replaceAll("-",""); { //不加鹽(與認證對應) // users.put("wzs", new Md5Hash("123456",null, 2).toString()); //加鹽 users.put("wzs", new Md5Hash("123456",salt, 2).toString()); user_roles.put("wzs", new HashSet<>(Arrays.asList("admin", "test"))); roles_permissions.put("admin", new HashSet<>(Arrays.asList("user:delete", "user:update"))); super.setName("TestRealm"); //設置Realm名稱,可選 } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //從認證信息獲取用戶名 String username = (String)principalCollection.getPrimaryPrincipal(); //從數據庫或緩存中獲取角色、權限數據 Set<String> roles = user_roles.get(username); Set<String> permissions = new HashSet<>(); for (String role : roles) { Set<String> set; if((set = roles_permissions.get(role)) != null) { permissions.addAll(set); } } SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //從主題傳過來的認證信息中,得到用戶名 String username = (String)authenticationToken.getPrincipal(); //經過用戶名從數據庫中獲取憑證 String password = users.get(username); if(password != null) { //不加鹽 // return new SimpleAuthenticationInfo(username, password, super.getName()); //加鹽 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, super.getName()); simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt)); return simpleAuthenticationInfo; } return null; } }
單元測試:
package com.example.demo_mg; import com.example.demo_mg.realm.TestRealm; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class DemoMgApplicationTests { @Test public void contextLoads() { } @Test public void test() { TestRealm testRealm = new TestRealm(); //設置加密 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("md5"); //加密算法 matcher.setHashIterations(2); //加密次數,與new Md5Hash("123456",null, 2).toString();的hashIterations參數一致,不然IncorrectCredentialsException testRealm.setCredentialsMatcher(matcher); //構建SecurityManager環境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(testRealm); //主體提交認證請求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wzs", "123456"); subject.login(token); //受權 System.out.println(subject.isAuthenticated()); System.out.println(subject.hasRole("admin")); System.out.println(subject.isPermitted("user:delete")); subject.logout(); } }
MD5加密不可逆,但不加鹽存在風險,註冊的時候隨機生成鹽,保存到數據庫,這裏返回的認證對象帶的鹽從庫裏查出來。