每天分享java乾貨,歡迎關注 ,你的成功源於點點滴滴!
前言
Shiro解決了什麼問題?
互聯網無非就是一些用戶C想要使用一些服務S的資源去完成某件事,S的資源不能說給誰用就給誰用,因此產生了權限的概念,即C必須有權限才能操作S的資源。S如何確定C就是C呢?因此又產生了身份驗證的概念。一個Authorization一個Authentication就是Shiro解決的最重要的兩個問題,其他的功能都是給Shiro打輔助的,比如Session管理,加密處理,記住我等。
Shiro是什麼?
把Shiro想象成一家安全公司
公司給服務端提供的服務是:服務端把自己維護的權限啊、用戶啊、角色啊什麼的信息通過接口提供給Shiro,shiro就可以幫服務端處理用戶權限角色等的安全認證和授權等工作
公司給客戶提供的服務是:客戶可以是任何外來的東西,但是想要訪問服務端提供的要求權限驗證等資源,就必須先經過shiro這層把關,shiro會對客戶進行安全認證和授權等工作
image.png
Shiro重要概念有哪些?
image.png
- Subject:可以理解為與shiro打交道的對象,該對象封裝了一些對方的信息,shiro可以通過subject拿到這些信息
- SecurityManager:Shiro的總經理,通過指使Authorizer和Authenticator等對subject進行授權和身份驗證等工作
- Realm:管理著一些如用戶、角色、權限等重要信息,Shiro中所需的這些重要信息都是從Realm這裡獲取的,Realm本質上就是一個重要信息的數據源
- Authenticator:認證器,負責Subject的認證操作,認證過程就是根據Subject提供的信息通過Realm查詢到相關信息,然後做對比,支持擴展
- Authorizer:授權器,控制著Subject對服務資源的訪問權限
- SessionManager:用於管理Session,這個Session可以是web的也可以不是web的。
- SessionDao:把Session的 CRUD和存儲介質聯繫起來的工具,存儲介質可以是數據庫,也可以是緩存,比如把session放到redis裡面
- CacheManager:緩存控制器,Realm管理的數據(用戶、角色、權限)可以放到緩存裡由CacheManager管理,提高認證授權等的速度
- Cryptography:加密組件,Shiro提供了很多加解密算法的組件
- 我們需要操作哪些組件呢?
- 作為一個成熟的跟Apache混的組件,用起來肯定越簡單越符合用戶習慣。我們使用的時候只需要把用戶、角色、權限信息存儲好(一般放到數據庫裡)並提供接口可以把這些信息注入到Shiro中就可以了。然後再對Shiro進行一些簡單的配置即可!
1、SpringBoot整合Shiro
step1
創建5張表,分別記錄用戶、角色、權限、用戶角色關係、角色權限關係。
建表很隨意的,字段隨便起名字,哪些字段也很隨意,甚至有些表可以不存在的(比如權限表)。比如你可以簡單的給user表一個id一個name,給role表一個id一個name。
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(32) NOT NULL, `name` varchar(60) DEFAULT NULL, `mob` varchar(60) DEFAULT NULL, `email` varchar(150) DEFAULT NULL, `valid` int(2) DEFAULT NULL, `pticket` varchar(200) DEFAULT NULL, `role_id` int(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `user_role`; CREATE TABLE `user_role` ( `uid` int(32) NOT NULL, `role_id` int(32) NOT NULL, PRIMARY KEY (`uid`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` int(32) NOT NULL, `description` varchar(255) DEFAULT NULL, `role` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `role_permission`; CREATE TABLE `role_permission` ( `permission_id` int(32) NOT NULL, `role_id` int(32) NOT NULL, PRIMARY KEY (`permission_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `permission`; CREATE TABLE `permission` ( `id` int(32) NOT NULL, `name` varchar(255) DEFAULT NULL, `description` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
step2
設計上面表的dao層和service層,只要提供根據user的id或者某個屬性查詢到所有的角色及權限信息就足夠了
比如我寫的接口,dao用mybatisGenerator生成,service自己完成兩個根據用戶id獲取角色和權限信息的接口。
@Service public class UserService { @Autowired private UserRoleDAO userRoleDAO; @Autowired private UserDAO userDAO; @Autowired private RoleDAO roleDAO; @Autowired private PermissionDAO permissionDAO; @Autowired private RolePermissionDAO rolePermissionDAO; //根據用戶id查詢所有的角色信息 public List findRoles(Integer id) { UserRoleExample example = new UserRoleExample(); example.createCriteria().andUidEqualTo(id); List keyList = userRoleDAO.selectByExample(example); List roleIdList = new ArrayList<>(keyList.size()); for (UserRoleKey userRoleKey : keyList) { roleIdList.add(userRoleKey.getRoleId()); } RoleExample roleExample = new RoleExample(); roleExample.createCriteria().andIdIn(roleIdList); return roleDAO.selectByExample(roleExample); } //根據用戶的id查詢所有權限信息 public List findPermissions(Integer id) { List roles = findRoles(id); List roleIds = new ArrayList<>(roles.size()); for (Role role : roles) { roleIds.add(role.getId()); } RolePermissionExample example = new RolePermissionExample(); example.createCriteria().andRoleIdIn(roleIds); List keyList = rolePermissionDAO.selectByExample(example); List permissionIdList = new ArrayList<>(keyList.size()); for (RolePermissionKey rolePermissionKey : keyList) { permissionIdList.add(rolePermissionKey.getPermissionId()); } PermissionExample permissionExample = new PermissionExample(); if (permissionIdList.size() != 0) { permissionExample.createCriteria().andIdIn(permissionIdList); return permissionDAO.selectByExample(permissionExample); } return new ArrayList<>(); } public User findUserById(String uId) { return userDAO.selectByPrimaryKey(Integer.valueOf(uId)); } @Transactional public int assignDefaultUserRolePermission(User user) { int success1 = userDAO.insert(user); UserRoleKey userRoleKey = new UserRoleKey(); userRoleKey.setUid(user.getId()); userRoleKey.setRoleId(2); int success2 = userRoleDAO.insert(userRoleKey); return success1 + success2; } }
step3
把獲取角色和權限信息的userService的兩個接口提供給Shiro,讓Shiro有法可依。
- 首先,UserRealm這個類繼承了AuthorizingRealm,這個類的作用是兩處獲取信息,一處是Subject即用戶傳過來的信息;一處是我們提供給shiro的userService接口以獲取權限信息和角色信息。拿這兩個信息之後AuthorizingRealm會自動進行比較,判斷用戶名密碼,用戶權限等等。
- 然後,拿用戶憑證信息的是doGetAuthenticationInfo接口,拿角色權限信息的是doGetAuthorizationInfo接口
- 然後,兩個重要參數,AuthenticationToken是我們可以自己實現的用戶憑證/密鑰信息,PrincipalCollection是用戶憑證信息集合。注意Principals表示憑證(比如用戶名、手機號、郵箱等)
- 最後,配置完成對比的兩方之後Subject.login(token)的時候就會調用doGetAuthenticationInfo方法;涉及到Subject.hasRole或者Subject.hasPermission的時候就會調用doGetAuthorizationInfo方法;
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; //shiro的權限信息配置 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); String uid = (String) principals.getPrimaryPrincipal(); List roles = userService.findRoles(Integer.valueOf(uid)); Set roleNames = new HashSet<>(roles.size()); for (Role role : roles) { roleNames.add(role.getRole()); } //此處把當前subject對應的所有角色信息交給shiro,調用hasRole的時候就根據這些role信息判斷 authorizationInfo.setRoles(roleNames); List permissions = userService.findPermissions(Integer.valueOf(uid)); Set permissionNames = new HashSet<>(permissions.size()); for (Permission permission : permissions) { permissionNames.add(permission.getName()); } //此處把當前subject對應的權限信息交給shiro,當調用hasPermission的時候就會根據這些信息判斷 authorizationInfo.setStringPermissions(permissionNames); return authorizationInfo; } //根據token獲取認證信息authenticationInfo @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { /**這裡為什麼是String類型呢?其實要根據Subject.login(token)時候的token來的,你token定義成的pricipal是啥,這裡get的時候就是啥。比如我 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken idEmail = new UsernamePasswordToken(String.valueOf(user.getId()), user.getEmail()); try { idEmail.setRememberMe(true); subject.login(idEmail); } **/ String uId = (String) token.getPrincipal(); User user = userService.findUserById(uId); if (user == null) { return null; } //SimpleAuthenticationInfo還有其他構造方法,比如密碼加密算法等,感興趣可以自己看 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( uId, //表示憑證,可以隨便設,跟token裡的一致就行 user.getEmail(), //表示密鑰如密碼,你可以自己隨便設,跟token裡的一致就行 getName() ); //authenticationInfo信息交個shiro,調用login的時候會自動比較這裡的token和authenticationInfo return authenticationInfo; } }
step4
對shiro進行一些配置,如登陸路徑、權限驗證、密碼匹配等等.
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //攔截器. Map filterChainDefinitionMap = new LinkedHashMap(); //配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了 filterChainDefinitionMap.put("/logout", "anon"); filterChainDefinitionMap.put("/afterlogout", "anon"); // :這是一個坑呢,一不小心代碼就不好使了; // filterChainDefinitionMap.put("/static/**", "anon"); // filterChainDefinitionMap.put("/html/**","anon"); filterChainDefinitionMap.put("/afterlogin", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public Realm myShiroRealm() { UserRealm myShiroRealm = new UserRealm(); return myShiroRealm; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } @Bean(name="lifecycleBeanPostProcessor") public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能 * * @return */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } }
step5
完成以上配置就可以正常些登錄登出接口,以及權限驗證接口了。我這裡邊是利用了類似於openid的統一登錄認證接口,然後寫了幾個登錄接口。注意這裡的Session,一定要用SecurityUtils.getSubject.getSession,不然會有坑。
- 對於需要驗證權限的接口,比如要求角色直接在接口上加@RequiresRoles("admin"),@RequiresPermission("xxx")就可以了,註解裡還有稍微高階點的用法,自己Ctrl點進去看源碼就行。
- @RequireRoles有個坑,就是shiroFilter裡配置的無權跳轉路徑是不跳轉的,因為ajax獲取與url直接獲取是有區別的,我這邊用的ajax獲取。所以寫一個統一的異常處理接口捕獲無權異常就可以了。
@RestController public class LoginController { @Autowired private LoginService loginService; @RequestMapping("/login") public void login(HttpServletResponse response) { response.setStatus(302); try { response.sendRedirect(OPSConstants.OPS_URL_WITH_RETURN); } catch (IOException e) { } } @RequestMapping("/afterlogin") public void recTicket(String ticket, HttpServletRequest request, HttpServletResponse response) { Map paramMap = new HashMap<>(1); paramMap.put("ticket", ticket); String result = HttpHandler.getInstance().usingGetMethod(OPSConstants.OPS_URL_WITH_TICKET, paramMap, null); User user = JSON.parseObject(result, User.class); loginService.registerOrLogin(user); HttpSession session = request.getSession(); session.setMaxInactiveInterval(1000 * 60 * 60); session.setAttribute(session.getId(), user); try { response.sendRedirect("/html/index.html"); } catch (IOException e) { } } @RequestMapping(value = "/logout") public void logout(HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(); User user = (User) session.getAttribute(session.getId()); String pticket = user.getPticket(); String url = OPSConstants.OPS_URL_LOGOUT + "&pticket=" + pticket; response.setStatus(302); try { response.sendRedirect(url); } catch (IOException e) { } } @RequestMapping(value = "/afterlogout") public void afterLogout(HttpServletResponse response) { //這裡一定要使用shiro退出方式,否則session失效 SecurityUtils.getSubject().logout(); try { response.sendRedirect(OPSConstants.OPS_URL_WITH_RETURN); } catch (IOException e) { } } }
無權處理:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = UnauthorizedException.class) @ResponseBody public ResponseBO jsonExceptionHandler(HttpServletRequest req, Exception e) { return new ResponseBO(403, "權限不足!"); } // @ExceptionHandler(value = UnauthorizedException.class) // public ModelAndView businessExceptionHandler(){ // ModelAndView mav = new ModelAndView(); // mav.setStatus(HttpStatus.UNAUTHORIZED); // mav.addObject("message", e.getMessage()); // mav.setViewName("403"); // return mav; // } }
總結
shiro雖然輕量,但是坑還是很多的,官方文檔和網上的博客對初學者並不友好。學習的方法是git上找一個能通的項目,然後直接ctrl+左鍵查看源碼就行,或者debug的時候打斷點,查看那些配置的方法及傳的參數。
來源:簡書