這里主要涉及到五張表:用戶表,角色表(用戶所擁有的角色),權限表(角色所涉及到的權限),用戶-角色表(用戶和角色是多對多的),角色-權限表(角色和權限是多對多的).表結構建立的sql語句如下:
CREATE TABLE `module` ( `mid` int(11) NOT NULL AUTO_INCREMENT, `mname` varchar(255) DEFAULT NULL, PRIMARY KEY (`mid`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of module -- ---------------------------- INSERT INTO `module` VALUES ('1', 'add'); INSERT INTO `module` VALUES ('2', 'delete'); INSERT INTO `module` VALUES ('3', 'query'); INSERT INTO `module` VALUES ('4', 'update'); -- ---------------------------- -- Table structure for module_role -- ---------------------------- DROP TABLE IF EXISTS `module_role`; CREATE TABLE `module_role` ( `rid` int(11) DEFAULT NULL, `mid` int(11) DEFAULT NULL, KEY `rid` (`rid`), KEY `mid` (`mid`), CONSTRAINT `mid` FOREIGN KEY (`mid`) REFERENCES `module` (`mid`), CONSTRAINT `rid` FOREIGN KEY (`rid`) REFERENCES `role` (`rid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of module_role -- ---------------------------- INSERT INTO `module_role` VALUES ('1', '1'); INSERT INTO `module_role` VALUES ('1', '2'); INSERT INTO `module_role` VALUES ('1', '3'); INSERT INTO `module_role` VALUES ('1', '4'); INSERT INTO `module_role` VALUES ('2', '1'); INSERT INTO `module_role` VALUES ('2', '3'); -- ---------------------------- -- Table structure for role -- ---------------------------- DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `rid` int(11) NOT NULL AUTO_INCREMENT, `rname` varchar(255) DEFAULT NULL, PRIMARY KEY (`rid`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of role -- ---------------------------- INSERT INTO `role` VALUES ('1', 'admin'); INSERT INTO `role` VALUES ('2', 'customer'); -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `uid` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, PRIMARY KEY (`uid`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES ('1', 'wulifu', '123456'); INSERT INTO `user` VALUES ('2', 'root', '123456'); -- ---------------------------- -- Table structure for user_role -- ---------------------------- DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, KEY `u_fk` (`uid`), KEY `r_fk` (`rid`), CONSTRAINT `r_fk` FOREIGN KEY (`rid`) REFERENCES `role` (`rid`), CONSTRAINT `u_fk` FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user_role -- ---------------------------- INSERT INTO `user_role` VALUES ('1', '1'); INSERT INTO `user_role` VALUES ('2', '2');
二、功能要求
admin要求只有具有admin角色的用戶才能訪問,update需要有update權限的用戶才能訪問,login、loginUser都不做攔截。
預期目標:
wulifu是有admin角色和所有權限,所以用wulifu登錄后,可以訪問update和admin,但是不能訪問guest;而root是customer角色,只有add和query權限,所以不能訪問admin和update。
三、添加依賴,配置文件
1、springboot項目,項目結構如下:

2、添加依賴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.20</version> </dependency> <!--常用的工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!--spring的上下文工具包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>4.1.7.RELEASE</version> </dependency> <!--對jsp的處理--> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency> </dependencies>
3、application.yml
server: port: 8010 tomcat.uri-encoding: UTF-8 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8 username: root password: 123456 mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: mappers/*.xml # mapper.xml中的resultType中經常會用到一些自定義POJO,你可以用完全限定名來指定這些POJO的引用,例如 # <select id="getUsers" resultType="com.majing.learning.mybatis.entity.User">, # 又或者你可以通過在application.yml中指定POJO掃描包來讓mybatis自動掃描到自定義POJO,如下: # mybatis:type-aliases-package: com.majing.learning.mybatis.entity type-aliases-package: com.example.springbootshiro.pojo
三、項目設計
1、pojo層
User.java
public class User implements Serializable { private Integer uid; private String username; private String password; private Set<Role> roles = new HashSet<>(); }
Role.java
public class Role implements Serializable { private Integer rid; private String rname; private Set<Module> modules = new HashSet<>(); }
Module.java
public class Module implements Serializable { private Integer mid; private String mname; }
2、dao層
UserMapper.java
import com.example.springbootshiro.pojo.User; import org.springframework.stereotype.Repository; @Repository public interface UserMapper { User findByUserName(String username); }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.springbootshiro.dao.UserMapper"> <resultMap id="userMap" type="com.example.springbootshiro.pojo.User"> <id property="uid" column="uid"/> <result property="username" column="username"/> <result property="password" column="password"/> <collection property="roles" ofType="com.example.springbootshiro.pojo.Role"> <id property="rid" column="rid"/> <result property="rname" column="rname"/> <collection property="modules" ofType="com.example.springbootshiro.pojo.Module"> <id property="mid" column="mid"/> <result property="mname" column="mname"/> </collection> </collection> </resultMap> <select id="findByUserName" parameterType="string" resultMap="userMap"> SELECT u.*,r.*,m.* FROM user u inner join user_role ur on ur.uid=u.uid inner join role r on r.rid=ur.rid inner join module_role mr on mr.rid=r.rid inner join module m on mr.mid=m.mid WHERE username=#{username}; </select> </mapper>

圖中紅框內可能會報錯,但是依然可以正常運行。
3、service層
IUserService.Interface
public interface IUserService { User findByUserName(String username); }
UserServiceImpl.java
@Service("iUserService")
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public User findByUserName(String username) {
return userMapper.findByUserName(username);
}
}
4、controller層
TestController.java
@Controller public class TestController { @RequestMapping("/login") public String login(){ return "login"; } @RequestMapping("/index") public String index(){ return "index"; } @RequestMapping("/unauthorized") public String unauthorized() { return "unauthorized"; } /** * 擁有admin角色的人才能訪問 * @return */ @RequestMapping("/admin") public String admin(){ return "admin"; } /** * 擁有update權限的人才能訪問 * @return */ @RequestMapping("/update") public String update(){ return "update"; } @RequestMapping("/logout") public String logout(){ Subject subject = SecurityUtils.getSubject();//取出當前驗證主體 if(subject != null){ subject.logout(); //執行一次logout的操作,將session全部清空 } return "login"; } /** * 整個form表單的驗證流程: * 將登陸的用戶/密碼傳入UsernamePasswordToken,當調用subject.login(token)開始, * 調用Relam的doGetAuthenticationInfo方法,開始密碼驗證 * 此時這個時候執行我們自己編寫的CredentialMatcher(密碼匹配器), * 執行doCredentialsMatch方法,具體的密碼比較實現在這實現 */ @RequestMapping(value = "/loginUser") public String loginUser(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); //登陸成功的話,放到session中 User user = (User) subject.getPrincipal(); session.setAttribute("user", user); return "index"; }catch (Exception e) { return "login"; } } }
5、jsp頁面
login.jsp(登錄頁面)
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>login</title> </head> <body> <form action="/loginUser" method="post"> <input type="text" name="username"> <br> <input type="password" name="password"> <br> <input type="submit" value="提交"> </form> </body> </html>
index.jsp(登錄成功后跳轉的頁面)
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>登錄</title> </head> <body> <h1> 歡迎登錄, ${user.username} </h1> </body> </html>
unauthorized.jsp (無權訪問跳轉的頁面)
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> Unauthorized! </body> </html>
admin.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>admin</title> </head> <body> admin! </body> </html>
update.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>update</title> </head> <body> update! </body> </html>
四、配置Shiro
1、核心配置類:ShiroConfiguration.java
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; @Configuration public class ShiroConfiguration { /** * @Qualifier("XXX") Spring的Bean注入配置注解,該注解指定注入的Bean的名稱, * Spring框架使用byName方式尋找合格的bean,這樣就消除了byType方式產生的歧義。 */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); System.out.println(bean); bean.setSecurityManager(manager); bean.setLoginUrl("/login"); //提供登錄到url bean.setSuccessUrl("/index"); //提供登陸成功的url bean.setUnauthorizedUrl("/unauthorized"); /** * 可以看DefaultFilter,這是一個枚舉類,定義了很多的攔截器authc,anon等分別有對應的攔截器 */ //配置訪問權限 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/index", "authc"); //代表着前面的url路徑,用后面指定的攔截器進行攔截 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginUser", "anon"); filterChainDefinitionMap.put("/admin", "roles[admin]"); //admin的url,要用角色是admin的才可以登錄,對應的攔截器是RolesAuthorizationFilter filterChainDefinitionMap.put("/update", "perms[update]"); //擁有update權限的用戶才有資格去訪問 //filterChainDefinitionMap.put("/druid/**", "anon"); //所有的druid請求,不需要攔截,anon對應的攔截器不會進行攔截 filterChainDefinitionMap.put("/**", "user"); //所有的路徑都攔截,被UserFilter攔截,這里會判斷用戶有沒有登陸 bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//設置一個攔截器鏈 return bean; } /** * 定義安全管理器securityManager,注入自定義的realm * @param authRealm * @return */ @Bean("securityManager") public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) { //這個DefaultWebSecurityManager構造函數,會對Subject,realm等進行基本的參數注入 DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(authRealm);//往SecurityManager中注入Realm,代替原本的默認配置 return manager; } //自定義的Realm @Bean("authRealm") //@DependsOn("lifecycleBeanPostProcessor") //可選 public AuthRealm authRealm(@Qualifier("credentialsMatcher") CredentialMatcher matcher) { AuthRealm authRealm = new AuthRealm(); //這邊可以選擇是否將認證的緩存到內存中,現在有了這句代碼就將認證信息緩存的內存中了 //authRealm.setCacheManager(new MemoryConstrainedCacheManager()); //最簡單的情況就是明文直接匹配,然后就是加密匹配,這里的匹配工作則就是交給CredentialsMatcher來完成 authRealm.setCredentialsMatcher(matcher); return authRealm; } /** * Realm在驗證用戶身份的時候,要進行密碼匹配 * 最簡單的情況就是明文直接匹配,然后就是加密匹配,這里的匹配工作則就是交給CredentialsMatcher來完成 * 支持任意數量的方案,包括純文本比較、散列比較和其他方法。除非該方法重寫,否則默認值為 * @return */ @Bean("credentialsMatcher") public CredentialMatcher credentialsMatcher() { CredentialMatcher credentialsMatcher = new CredentialMatcher(); return credentialsMatcher; } /** * 配置shiro跟spring的關聯 * 以下AuthorizationAttributeSourceAdvisor,DefaultAdvisorAutoProxyCreator兩個類是為了支持shiro注解 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } /** * Spring的一個bean , 由Advisor決定對哪些類的方法進行AOP代理 * @return */ @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } /** * lifecycleBeanPostProcessor是負責生命周期的 , 初始化和銷毀的類 * (可選) * @return */ @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } }

這個SecurityManager的導包是org.apache.shiro.mgt.SecurityManager,不要導了java的包。
2、自定義域:AuthRealm.java
編寫AuthRealm完成根據用戶名去數據庫的查詢,並且將用戶信息放入shiro中,供核心配置類:ShiroConfiguration調用。有認證與授權的方法。
import com.example.springbootshiro.pojo.Module; import com.example.springbootshiro.pojo.Role; import com.example.springbootshiro.pojo.User; import com.example.springbootshiro.service.IUserService; import org.apache.shiro.authc.*; 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 org.springframework.beans.factory.annotation.Autowired; import java.util.*; //AuthenticatingRealm是抽象類,用於認證授權 public class AuthRealm extends AuthorizingRealm { @Autowired private IUserService iUserService; /** * 用戶授權 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //獲取前端輸入的用戶信息,封裝為User對象 User userweb = (User) principals.getPrimaryPrincipal(); //獲取前端輸入的用戶名 String username = userweb.getUsername(); //根據前端輸入的用戶名查詢數據庫中對應的記錄 User user = iUserService.findByUserName(username); if(user != null){ SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //因為addRoles和addStringPermissions方法需要的參數類型是Collection //所以先創建兩個collection集合 Collection<String> rolesCollection = new HashSet<String>(); Collection<String> perStringCollection = new HashSet<String>(); //獲取user的Role的set集合 Set<Role> roles = user.getRoles(); //遍歷集合 for (Role role : roles){ //將每一個role的name裝進collection集合 rolesCollection.add(role.getRname()); //獲取每一個Role的permission的set集合 Set<Module> permissionSet = role.getModules(); //遍歷集合 for (Module permission : permissionSet){ //將每一個permission的name裝進collection集合 perStringCollection.add(permission.getMname()); System.out.println(permission.getMname()); } //為用戶授權 info.addStringPermissions(perStringCollection); } //為用戶授予角色 info.addRoles(rolesCollection); return info; } return null; } /** * 用於認證登錄,認證接口實現方法,該方法的回調一般是通過subject.login(token)方法來實現的 * AuthenticationToken 用於收集用戶提交的身份(如用戶名)及憑據(如密碼): * AuthenticationInfo是包含了用戶根據username返回的數據信息,用於在匹馬比較的時候進行相互比較 * * shiro的核心是java servlet規范中的filter,通過配置攔截器,使用攔截器鏈來攔截請求,如果允許訪問,則通過。 * 通常情況下,系統的登錄、退出會配置攔截器。登錄的時候,調用subject.login(token),token是用戶驗證信息, * 這個時候會在Realm中doGetAuthenticationInfo方法中進行認證。這個時候會把用戶提交的驗證信息與數據庫中存儲的認證信息,將所有的數據拿到,在匹配器中進行比較 * 這邊是我們自己實現的CredentialMatcher類的doCredentialsMatch方法,返回true則一致,false則登陸失敗 * 退出的時候,調用subject.logout(),會清除回話信息 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //token攜帶了用戶信息 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //獲取前端輸入的用戶名 String username = usernamePasswordToken.getUsername(); //根據用戶名查詢數據庫中對應的記錄 User user = iUserService.findByUserName(username); /*//當前realm對象的name String realmName = getName(); //鹽值 ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername()); //封裝用戶信息,構建AuthenticationInfo對象並返回 AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user, user.getPassword(), credentialsSalt, realmName);*/ return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName()); } }
3、數據庫密碼為明文時密碼匹配類:CredentialMatcher.java
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; public class CredentialMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken utoken=(UsernamePasswordToken) token; //獲得用戶輸入的密碼:(可以采用加鹽(salt)的方式去檢驗) String inPassword = new String(utoken.getPassword()); //獲得數據庫中的密碼 String dbPassword=(String) info.getCredentials(); //進行密碼的比對 return this.equals(inPassword, dbPassword); } }
4、數據庫為加密加鹽時
authRealm引用的密碼匹配需要更換。

以下密碼匹配直接寫在核心配置類:ShiroConfiguration.java中
/** * 密碼校驗規則HashedCredentialsMatcher * 這個類是為了對密碼進行編碼的 , * 防止密碼在數據庫里明碼保存 , 當然在登陸認證的時候 , * 這個類也負責對form里輸入的密碼進行編碼 * 處理認證匹配處理器:如果自定義需要實現繼承HashedCredentialsMatcher */ @Bean("hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); //指定加密方式為MD5 credentialsMatcher.setHashAlgorithmName("MD5"); //加密次數 credentialsMatcher.setHashIterations(1024); credentialsMatcher.setStoredCredentialsHexEncoded(true); return credentialsMatcher; }
附上明文轉密文的代碼:
public static void main(String[] args) { String hashAlgorithName = "MD5"; String password = "登錄時輸入的密碼"; int hashIterations = 1;//加密次數 ByteSource credentialsSalt = ByteSource.Util.bytes("登錄時輸入的用戶名"); Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations); System.out.println(obj); }
