相信你們前來看這篇文章的時候,是有SpringBoot和Shiro基礎的,因此本文只介紹整合的步驟,若是哪裏寫的很差,懇請你們能指出錯誤,謝謝!依賴以及一些配置文件請在源碼裏參考,請參見 https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication ,javascript
我的博客:www.fqcoder.cnhtml
在本文中,咱們使用RBAC(Role-Based Access Control,基於角色的訪問控制)模型設計用戶,角色和權限間的關係。簡單地說,一個用戶擁有若干角色,每個角色擁有若干權限。這樣,就構形成「用戶-角色-權限」的受權模型。在這種模型中,用戶與角色之間,角色與權限之間,通常者是多對多的關係。以下圖所示:前端
而後咱們在來根據這個模型圖,設計數據庫表,記得本身添加一點測試數據哦java
CREATE TABLE `tb_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名稱', `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_role_permission` ( `role_id` int(11) NOT NULL COMMENT '角色id', `permission_id` int(11) NOT NULL COMMENT '權限id' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `create_time` datetime(0) DEFAULT NULL, `status` int(10) DEFAULT NULL COMMENT '狀態', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_user_role` ( `role_id` int(11) NOT NULL COMMENT '角色id', `user_id` int(11) NOT NULL COMMENT '用戶id' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
咱們建立對應的類,筆者這裏用了lombok插件,記得先安裝插件git
@Data public class User implements Serializable { private Integer id; private String username; private String password; private Date createTime; private Integer status; } @Data public class Role implements Serializable { private Integer id; private String name; private String description; } @Data public class Permission implements Serializable { private Integer id; private String url; private String name; }
由於咱們只是作一個演示,只涉及到用戶登陸,用戶角色、權限查找,並未實現過多方法github
建立UserMapper
、RolePermissionMapper
、UserRoleMapper
三個接口web
注意:記得在Mapper接口上面加一個掃描註解@Mapper或者在boot啓動類上加一個@MapperScan(value = "mapper包路徑")註解ajax
public interface UserMapper { @Select("select * from tb_user where username=#{username}") User selectByName(String username); } -------------------------- public interface UserRoleMapper { /** * * 查詢用戶角色(可能一個用戶有多個角色) * @param username * @return */ @Select("select r.id,r.name,r.description from tb_role r " + "left join tb_user_role ur on(r.id = ur.role_id)" + "left join tb_user u on(u.id=ur.user_id)" + "where u.username =#{username}") List<Role> findByUserName(String username); } ------------------------------------------------ public interface RolePermissionMapper { /** * 經過角色id查詢權限 * @param roleId * @return */ @Select("select p.id,p.url,p.name from tb_permission p " + "left join tb_role_permission rp on(p.id=rp.permission_id)" + "left join tb_role r on(r.id=rp.role_id)" + "where r.id=#{roleId}") List<Permission> findByRoleId(Integer roleId); }
好了,前面的一些東西,都是能夠算是準備工做,如今纔是真正開始整合Shiro了,咱們先來屢一下思路,實現認證權限功能主要能夠概括爲3點:算法
1.定義一個ShiroConfig配置類,配置 SecurityManager Bean , SecurityManager爲Shiro的安全管理器,管理着全部Subject;spring
2.在ShiroConfig中配置 ShiroFilterFactoryBean ,它是Shiro過濾器工廠類,依賴SecurityManager ;
3.自定義Realm實現類,包含 doGetAuthorizationInfo()
和doGetAuthenticationInfo()
方法 ,
/** * @ClassName ShiroConfig * @Description TODO * @Author fqCoder * @Date 2020/2/29 3:08 * @Version 1.0 */ @Configuration public class ShiroConfig { /** * 這是shiro的大管家,至關於mybatis裏的SqlSessionFactoryBean * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //登陸 shiroFilterFactoryBean.setLoginUrl("/login"); //首頁 shiroFilterFactoryBean.setSuccessUrl("/index"); //錯誤頁面,認證不經過跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); //頁面權限控制 shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap()); shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; } /** * web應用管理配置 * @param shiroRealm * @param cacheManager * @param manager * @return */ @Bean public DefaultWebSecurityManager securityManager(Realm shiroRealm, CacheManager cacheManager, RememberMeManager manager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setCacheManager(cacheManager); securityManager.setRememberMeManager(manager);//記住Cookie securityManager.setRealm(shiroRealm); securityManager.setSessionManager(sessionManager()); return securityManager; } /** * session過時控制 * @return * @author fuce * @Date 2019年11月2日 下午12:49:49 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager(); // 設置session過時時間3600s Long timeout=60L*1000*60;//毫秒級別 defaultWebSessionManager.setGlobalSessionTimeout(timeout); return defaultWebSessionManager; } /** * 加密算法 * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5");//採用MD5 進行加密 hashedCredentialsMatcher.setHashIterations(1);//加密次數 return hashedCredentialsMatcher; } /** * 記住個人配置 * @return */ @Bean public RememberMeManager rememberMeManager() { Cookie cookie = new SimpleCookie("rememberMe"); cookie.setHttpOnly(true);//經過js腳本將沒法讀取到cookie信息 cookie.setMaxAge(60 * 60 * 24);//cookie保存一天 CookieRememberMeManager manager=new CookieRememberMeManager(); manager.setCookie(cookie); return manager; } /** * 緩存配置 * @return */ @Bean public CacheManager cacheManager() { MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用內存緩存 return cacheManager; } /** * 配置realm,用於認證和受權 * @param hashedCredentialsMatcher * @return */ @Bean public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) { MyShiroRealm shiroRealm = new MyShiroRealm(); //校驗密碼用到的算法 // shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher); return shiroRealm; } /** * 啓用shiro方言,這樣能在頁面上使用shiro標籤 * @return */ @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * 啓用shiro註解 *加入註解的使用,不加入這個註解不生效 */ @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } }
注意:(當時筆者遇到的一個小問題,貼出來給你們漲姿式)
註解無效,登陸時不會執行驗證角色和權限的方法,只會執行登陸驗證方法,遂查詢資料,得知shiro在subject.login(token)方法時不會執行doGetAuthorizationInfo方法,只有在訪問到有權限驗證的接口時會調用查看權限,因而猜測註解無效,發現shiro的權限註解須要開啓纔能有用,添加在配置文件中加入
advisorAutoProxyCreator
和getAuthorizationAttributeSourceAdvisor
兩個bean開啓shiro註解,解決問題。
注意:
1.這裏要用LinkedHashMap 保證有序
2.filterChain基於短路機制,即最早匹配原則,
3.像anon、authc等都是Shiro爲咱們實現的過濾器,我給出了一張表,在文章尾附錄,自行查看
/** * @ClassName ShiroFilterMapFactory * @Description TODO * @Author fqCoder * @Date 2020/2/29 3:09 * @Version 1.0 */ public class ShiroFilterMapFactory { public static Map<String, String> shiroFilterMap() { // 設置路徑映射,注意這裏要用LinkedHashMap 保證有序 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //對全部用戶認證 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); //對全部頁面進行認證 filterChainDefinitionMap.put("/**", "authc"); return filterChainDefinitionMap; } }
配置完了ShiroConfig後,實現本身的Realm,而後注入到SecurityManager裏
自定義Realm類須要繼承 AuthorizingRealm 類,實現 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法便可 ,
doGetAuthorizationInfo() 方法是進行受權的方法,獲取角色的權限信息
doGetAuthenticationInfo()方法是進行用戶認證的方法,驗證用戶名和密碼
/** * @ClassName MyShiroRealm * @Description TODO * @Author fqCoder * @Date 2020/2/29 3:08 * @Version 1.0 */ @Service public class MyShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private UserRoleMapper userRoleMapper; @Autowired private RolePermissionMapper rolePermissionMapper; /** * 獲取用戶角色和權限 * @param principal * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { if(principal == null){ throw new AuthorizationException("principals should not be null"); } User userInfo= (User) SecurityUtils.getSubject().getPrincipal(); System.out.println("用戶-->"+userInfo.getUsername()+"獲取權限中"); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //用戶獲取角色集 List<Role> roleList=userRoleMapper.findByUserName(userInfo.getUsername()); Set<String> roleSet=new HashSet<>(); for (Role r:roleList){ Integer roleId=r.getId();//獲取角色id simpleAuthorizationInfo.addRole(r.getName());//添加角色名字 List<Permission> permissionList=rolePermissionMapper.findByRoleId(roleId); for (Permission p:permissionList){ //添加權限 simpleAuthorizationInfo.addStringPermission(p.getName()); } } System.out.println("角色爲-> " + simpleAuthorizationInfo.getRoles()); System.out.println("權限爲-> " + simpleAuthorizationInfo.getStringPermissions()); return simpleAuthorizationInfo; } /** * 登陸認證 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //獲取用戶輸入的用戶名密碼 String username= (String) token.getPrincipal(); String password=new String((char[])token.getCredentials()); System.out.println("用戶輸入--->username:"+username+"-->password:"+password); //在數據庫中查詢 User userInfo=userMapper.selectByName(username); if (userInfo == null) { throw new UnknownAccountException("用戶名或密碼錯誤!"); } if (!password.equals(userInfo.getPassword())) { throw new IncorrectCredentialsException("用戶名或密碼錯誤!"); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, // 用戶名 userInfo.getPassword(), // 密碼 getName() // realm name ); return authenticationInfo; } }
其中UnknownAccountException
等異常爲Shiro自帶異常,Shiro具備豐富的運行時AuthenticationException
層次結構,能夠準確指出嘗試失敗的緣由。
用來處理登陸訪問請求
/** * @ClassName LoginController * @Description TODO * @Author fqCoder * @Date 2020/2/29 6:06 * @Version 1.0 */ @Controller public class LoginController { @GetMapping("/login") public String login(){ return "login"; } @GetMapping("/") public String home(){ return "redirect:/index"; } @GetMapping("/index") public String index(Model model){ User user= (User) SecurityUtils.getSubject().getPrincipal(); model.addAttribute("user",user); return "index"; } @PostMapping("login") @ResponseBody public AjaxResult login(User user,Boolean rememberMe){ System.out.println("user = " + user); UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); //獲取Subject 對象 Subject subject= SecurityUtils.getSubject(); try { if (rememberMe){ token.setRememberMe(true); } subject.login(token); return AjaxResult.success("/index"); } catch (UnknownAccountException e) { return AjaxResult.error(e.getMessage()); } catch (IncorrectCredentialsException e) { return AjaxResult.error(e.getMessage()); } } @GetMapping("/403") public String forbid(){ return "403"; } }
用於處理User類的訪問請求,並使用Shiro權限註解控制權限:
/** * @ClassName UserController * @Description TODO * @Author fqCoder * @Date 2020/3/3 15:14 * @Version 1.0 */ @RestController @RequestMapping("/user") public class UserController { @RequiresPermissions("user:queryAll") @GetMapping("/queryAll") public String queryAll(){ //只演示框架...功能不實現 return "查詢列表"; } @RequiresPermissions("user:add") @GetMapping("/add") public String userAdd(){ return "添加用戶"; } @RequiresPermissions("user:delete") @GetMapping("/delete") public String userDelete(){ return "刪除用戶"; } }
這裏我只貼重要代碼,具體的代碼,到這裏找哦!
<form id="loginForm"> <input type="text" id="username" name="username" class="text" /> <input type="password" id="password" name="password" /> </form> <div class="signin"> <input id="loginBut" type="button" value="Login" > </div> -------js代碼---- <script type="text/javascript"> $.fn.serializeObject = function () { var o = {}; var a = this.serializeArray(); $.each(a, function () { if (o[this.name]) { if (!o[this.name].push) { o[this.name] = [o[this.name]]; } o[this.name].push(this.value); } else { o[this.name] = this.value || ''; } }); return o; }; $(function () { $("#loginBut").click(function () { var arr=$('#loginForm').serializeObject(); $.ajax({ url: '/login', type: 'post', data: arr, dataType: "json", success: function (data) { if (data.code==200){ location.href=data.msg; } else { alert(data.msg); } }, error: function (data) { alert(data.msg); } }) }); }); </script>
當用戶登陸進來的時候調到index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首頁</title> </head> <body> <h1>番茄歡迎您!</h1> 登陸用戶:【[[${user.username}]]】 <a th:href="@{/logout}">註銷</a> <h2>權限測試</h2> <a th:href="@{/user/queryAll}">獲取用戶所有信息</a> <a th:href="@{/user/add}">添加用戶</a> <a th:href="@{/user/delete}">刪除用戶</a> </body> </html>
比較簡單,此處能用就行
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>403</title> </head> <body> <h1>403權限不夠</h1> <a href="/index">首頁</a> </body> </html>
啓動項目:訪問http://localhost:8080/,它會自動攔截,頁面重定向到 http://localhost:8080/login ,登陸成功跳轉到http://localhost:8080/index
問題:
登陸測試用戶的時候,訪問沒有權限的連接請求時,後臺拋出org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
異常
當時覺得在ShiroConfig配置類中配置了shiroFilterFactoryBean.setUnauthorizedUrl("/403");
沒有權限的請求會自動從定向到/403,而後倒是拋出了異常,後來在一篇文章中看到了,說這個設置只對filterChain起做用 ,針對這個問題,咱們能夠定義一個全局異常捕獲類:
@ControllerAdvice @Order(value = Ordered.HIGHEST_PRECEDENCE) public class GlobalExceptionHandler { @ExceptionHandler(value = AuthorizationException.class) public String handleAuthorizationException() { return "403"; } }
而後再啓動項目,登陸測試帳號,訪問沒有權限的請求,頁面成功定向到/403
源碼連接: https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication
至此,筆者剛開始寫,不是寫的很好,歡迎各位網友踊躍指出不足,謝謝!
Filter Name | Class | Description |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名攔截器,即不須要登陸便可訪問;通常用於靜態資源過濾;示例/static/**=anon |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基於表單的攔截器;如/**=authc ,若是沒有登陸會跳到相應的登陸頁面登陸 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP身份驗證攔截器 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/),示例/logout=logout |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不建立會話攔截器,調用subject.getSession(false) 不會有什麼問題,可是若是subject.getSession(true) 將拋出DisabledSessionException 異常 |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 權限受權攔截器,驗證用戶是否擁有全部權限;屬性和roles同樣;示例/user/**=perms["user:create"] |
port | org.apache.shiro.web.filter.authz.PortFilter | 端口攔截器,主要屬性port(80) :能夠經過的端口;示例/test= port[80] ,若是用戶訪問該頁面是非80,將自動將請求端口改成80並重定向到該80端口,其餘路徑/參數等都同樣 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest風格攔截器,自動根據請求方法構建權限字符串;示例/users=rest[user] ,會自動拼出user:read,user:create,user:update,user:delete權限字符串進行權限匹配(全部都得匹配,isPermittedAll) |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色受權攔截器,驗證用戶是否擁有全部角色;示例/admin/**=roles[admin] |
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL攔截器,只有請求協議是https才能經過;不然自動跳轉會https端口443;其餘和port攔截器同樣; |
user | org.apache.shiro.web.filter.authc.UserFilter | 用戶攔截器,用戶已經身份驗證/記住我登陸的均可;示例/**=user |