個人公衆號: MarkerHub,網站: https://markerhub.com更多精選文章請點擊:Java筆記大全.md前端
有源碼的教程,不會的同窗下載源碼,根據教程學一下哈~java
做者:Sans_juejin.im/post/5d087d605188256de9779e64mysql
Shiro 是一個安全框架, 項目中主要用它作認證, 受權, 加密, 以及用戶的會話管理, 雖然 Shiro 沒有 SpringSecurity 功能更豐富, 可是它輕量, 簡單, 在項目中一般業務需求 Shiro 也都能勝任.react
數據表 (SQL 文件在項目中): 數據庫中測試號的密碼進行了加密, 密碼皆爲 123456git
Maven 依賴以下:github
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- AOP依賴,必定要加,不然權限攔截驗證不生效 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- lombok插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <!-- mybatisPlus 核心庫 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <!-- 引入阿里數據庫鏈接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> <!-- Shiro 核心依賴 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- Shiro-redis插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> </dependency> <!-- StringUitlS工具 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> </dependencies>
配置以下:web
# 配置端口 server: port: 8764 spring: # 配置數據源 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: root type: com.alibaba.druid.pool.DruidDataSource # Redis數據源 redis: host: localhost port: 6379 timeout: 6000 password: 123456 jedis: pool: max-active: 1000 # 鏈接池最大鏈接數(使用負值表示沒有限制) max-wait: -1 # 鏈接池最大阻塞等待時間(使用負值表示沒有限制) max-idle: 10 # 鏈接池中的最大空閒鏈接 min-idle: 5 # 鏈接池中的最小空閒鏈接 # mybatis-plus相關配置 mybatis-plus: # xml掃描,多個目錄用逗號或者分號分隔(告訴 Mapper 所對應的 XML 文件位置) mapper-locations: classpath:mapper/*.xml # 如下配置均有默認值,能夠不設置 global-config: db-config: #主鍵類型 AUTO:"數據庫ID自增" INPUT:"用戶輸入ID",ID_WORKER:"全局惟一ID (數字類型惟一ID)", UUID:"全局惟一ID UUID"; id-type: auto #字段策略 IGNORED:"忽略判斷" NOT_NULL:"非 NULL 判斷") NOT_EMPTY:"非空判斷" field-strategy: NOT_EMPTY #數據庫類型 db-type: MYSQL configuration: # 是否開啓自動駝峯命名規則映射:從數據庫列名到Java屬性駝峯命名的相似映射 map-underscore-to-camel-case: true # 若是查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段 call-setters-on-nulls: true # 這個配置會將執行的sql打印出來,在開發或測試的時候能夠用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
用戶實體, Dao,Service 等在這裏省略, 請參考源碼redis
編寫 Exception 類來處理 Shiro 權限攔截異常算法
建立 SHA256Util 加密工具spring
建立 Spring 工具
/** * @Description Spring上下文工具類 * @Author Sans * @CreateTime 2019/6/17 13:40 */ @Component public class SpringUtil implements ApplicationContextAware { private static ApplicationContext context; /** * Spring在bean初始化後會判斷是否是ApplicationContextAware的子類 * 若是該類是,setApplicationContext()方法,會將容器中ApplicationContext做爲參數傳入進去 * @Author Sans * @CreateTime 2019/6/17 16:58 */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } /** * 經過Name返回指定的Bean * @Author Sans * @CreateTime 2019/6/17 16:03 */ public static <T> T getBean(Class<T> beanClass) { return context.getBean(beanClass); } }
建立 Shiro 工具
/** * @Description Shiro工具類 * @Author Sans * @CreateTime 2019/6/15 16:11 */ public class ShiroUtils { /** 私有構造器 **/ private ShiroUtils(){} private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class); /** * 獲取當前用戶Session * @Author Sans * @CreateTime 2019/6/17 17:03 * @Return SysUserEntity 用戶信息 */ public static Session getSession() { return SecurityUtils.getSubject().getSession(); } /** * 用戶登出 * @Author Sans * @CreateTime 2019/6/17 17:23 */ public static void logout() { SecurityUtils.getSubject().logout(); } /** * 獲取當前用戶信息 * @Author Sans * @CreateTime 2019/6/17 17:03 * @Return SysUserEntity 用戶信息 */ public static SysUserEntity getUserInfo() { return (SysUserEntity) SecurityUtils.getSubject().getPrincipal(); } /** * 刪除用戶緩存信息 * @Author Sans * @CreateTime 2019/6/17 13:57 * @Param username 用戶名稱 * @Param isRemoveSession 是否刪除Session * @Return void */ public static void deleteCache(String username, boolean isRemoveSession){ //從緩存中獲取Session Session session = null; Collection<Session> sessions = redisSessionDAO.getActiveSessions(); SysUserEntity sysUserEntity; Object attribute = null; for(Session sessionInfo : sessions){ //遍歷Session,找到該用戶名稱對應的Session attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (attribute == null) { continue; } sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal(); if (sysUserEntity == null) { continue; } if (Objects.equals(sysUserEntity.getUsername(), username)) { session=sessionInfo; } } if (session == null||attribute == null) { return; } //刪除session if (isRemoveSession) { redisSessionDAO.delete(session); } //刪除Cache,在訪問受限接口時會從新受權 DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); Authenticator authc = securityManager.getAuthenticator(); ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute); } }
建立 Shiro 的 SessionId 生成器
建立 Realm 用於受權和認證
/** * @Description Shiro權限匹配和帳號密碼匹配 * @Author Sans * @CreateTime 2019/6/15 11:27 */ public class ShiroRealm extends AuthorizingRealm { @Autowired private SysUserService sysUserService; @Autowired private SysRoleService sysRoleService; @Autowired private SysMenuService sysMenuService; /** * 受權權限 * 用戶進行權限驗證時候Shiro會去緩存中找,若是查不到數據,會執行這個方法去查權限,並放入緩存中 * @Author Sans * @CreateTime 2019/6/12 11:44 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal(); //獲取用戶ID Long userId =sysUserEntity.getUserId(); //這裏能夠進行受權和處理 Set<String> rolesSet = new HashSet<>(); Set<String> permsSet = new HashSet<>(); //查詢角色和權限(這裏根據業務自行查詢) List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId); for (SysRoleEntity sysRoleEntity:sysRoleEntityList) { rolesSet.add(sysRoleEntity.getRoleName()); List<SysMenuEntity> sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId()); for (SysMenuEntity sysMenuEntity :sysMenuEntityList) { permsSet.add(sysMenuEntity.getPerms()); } } //將查到的權限和角色分別傳入authorizationInfo中 authorizationInfo.setStringPermissions(permsSet); authorizationInfo.setRoles(rolesSet); return authorizationInfo; } /** * 身份認證 * @Author Sans * @CreateTime 2019/6/12 12:36 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //獲取用戶的輸入的帳號. String username = (String) authenticationToken.getPrincipal(); //經過username從數據庫中查找 User對象,若是找到進行驗證 //實際項目中,這裏能夠根據實際狀況作緩存,若是不作,Shiro本身也是有時間間隔機制,2分鐘內不會重複執行該方法 SysUserEntity user = sysUserService.selectUserByName(username); //判斷帳號是否存在 if (user == null) { throw new AuthenticationException(); } //判斷帳號是否被凍結 if (user.getState()==null||user.getState().equals("PROHIBIT")){ throw new LockedAccountException(); } //進行驗證 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用戶名 user.getPassword(), //密碼 ByteSource.Util.bytes(user.getSalt()), //設置鹽值 getName() ); //驗證成功開始踢人(清除緩存和Session) ShiroUtils.deleteCache(username,true); return authenticationInfo; } }
建立 SessionManager 類
建立 ShiroConfig 配置類
/** * @Description Shiro配置類 * @Author Sans * @CreateTime 2019/6/10 17:42 */ @Configuration public class ShiroConfig { private final String CACHE_KEY = "shiro:cache:"; private final String SESSION_KEY = "shiro:session:"; private final int EXPIRE = 1800; //Redis配置 @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.password}") private String password; /** * 開啓Shiro-aop註解支持 * @Attention 使用代理方式因此須要開啓代碼支持 * @Author Sans * @CreateTime 2019/6/12 8:38 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro基礎配置 * @Author Sans * @CreateTime 2019/6/12 8:42 */ @Bean public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 注意過濾器配置順序不能顛倒 // 配置過濾:不會被攔截的連接 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/userLogin/**", "anon"); filterChainDefinitionMap.put("/**", "authc"); // 配置shiro默認登陸界面地址,先後端分離中登陸界面跳轉應由前端路由控制,後臺僅返回json數據 shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 安全管理器 * @Author Sans * @CreateTime 2019/6/12 10:34 */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 自定義Ssession管理 securityManager.setSessionManager(sessionManager()); // 自定義Cache實現 securityManager.setCacheManager(cacheManager()); // 自定義Realm驗證 securityManager.setRealm(shiroRealm()); return securityManager; } /** * 身份驗證器 * @Author Sans * @CreateTime 2019/6/12 10:37 */ @Bean public ShiroRealm shiroRealm() { ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return shiroRealm; } /** * 憑證匹配器 * 將密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理,在這裏作匹配配置 * @Author Sans * @CreateTime 2019/6/12 10:48 */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher(); // 散列算法:這裏使用SHA256算法; shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME); // 散列的次數,好比散列兩次,至關於 md5(md5("")); shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS); return shaCredentialsMatcher; } /** * 配置Redis管理器 * @Attention 使用的是shiro-redis開源插件 * @Author Sans * @CreateTime 2019/6/12 11:06 */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setTimeout(timeout); redisManager.setPassword(password); return redisManager; } /** * 配置Cache管理器 * 用於往Redis存儲權限和角色標識 * @Attention 使用的是shiro-redis開源插件 * @Author Sans * @CreateTime 2019/6/12 12:37 */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setKeyPrefix(CACHE_KEY); // 配置緩存的話要求放在session裏面的實體類必須有個id標識 redisCacheManager.setPrincipalIdFieldName("userId"); return redisCacheManager; } /** * SessionID生成器 * @Author Sans * @CreateTime 2019/6/12 13:12 */ @Bean public ShiroSessionIdGenerator sessionIdGenerator(){ return new ShiroSessionIdGenerator(); } /** * 配置RedisSessionDAO * @Attention 使用的是shiro-redis開源插件 * @Author Sans * @CreateTime 2019/6/12 13:44 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setSessionIdGenerator(sessionIdGenerator()); redisSessionDAO.setKeyPrefix(SESSION_KEY); redisSessionDAO.setExpire(expire); return redisSessionDAO; } /** * 配置Session管理器 * @Author Sans * @CreateTime 2019/6/12 14:25 */ @Bean public SessionManager sessionManager() { ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); shiroSessionManager.setSessionDAO(redisSessionDAO()); return shiroSessionManager; } }
Shiro 能夠用代碼或者註解來控制權限, 一般咱們使用註解控制, 不只簡單方便, 並且更加靈活. Shiro 註解一共有五個:
通常狀況下咱們在項目中作權限控制, 使用最多的是 RequiresPermissions 和 RequiresRoles, 容許存在多個角色和權限, 默認邏輯是 AND, 也就是同時擁有這些才能夠訪問方法, 能夠在註解中以參數的形式設置成 OR
示例
使用順序: Shiro 註解是存在順序的, 當多個註解在一個方法上的時候, 會逐個檢查, 知道所有經過爲止, 默認攔截順序是: RequiresRoles->RequiresPermissions->RequiresAuthentication->
RequiresUser->RequiresGuest
示例
建立 UserRoleController 角色攔截測試類
/** * @Description 角色測試 * @Author Sans * @CreateTime 2019/6/19 11:38 */ @RestController @RequestMapping("/role") public class UserRoleController { @Autowired private SysUserService sysUserService; @Autowired private SysRoleService sysRoleService; @Autowired private SysMenuService sysMenuService; @Autowired private SysRoleMenuService sysRoleMenuService; /** * 管理員角色測試接口 * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getAdminInfo") @RequiresRoles("ADMIN") public Map<String,Object> getAdminInfo(){ Map<String,Object> map = new HashMap<>(); map.put("code",200); map.put("msg","這裏是只有管理員角色能訪問的接口"); return map; } /** * 用戶角色測試接口 * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getUserInfo") @RequiresRoles("USER") public Map<String,Object> getUserInfo(){ Map<String,Object> map = new HashMap<>(); map.put("code",200); map.put("msg","這裏是只有用戶角色能訪問的接口"); return map; } /** * 角色測試接口 * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getRoleInfo") @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR) @RequiresUser public Map<String,Object> getRoleInfo(){ Map<String,Object> map = new HashMap<>(); map.put("code",200); map.put("msg","這裏是只要有ADMIN或者USER角色能訪問的接口"); return map; } /** * 登出(測試登出) * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getLogout") @RequiresUser public Map<String,Object> getLogout(){ ShiroUtils.logout(); Map<String,Object> map = new HashMap<>(); map.put("code",200); map.put("msg","登出"); return map; } }
建立 UserMenuController 權限攔截測試類
/** * @Description 權限測試 * @Author Sans * @CreateTime 2019/6/19 11:38 */ @RestController @RequestMapping("/menu") public class UserMenuController { @Autowired private SysUserService sysUserService; @Autowired private SysRoleService sysRoleService; @Autowired private SysMenuService sysMenuService; @Autowired private SysRoleMenuService sysRoleMenuService; /** * 獲取用戶信息集合 * @Author Sans * @CreateTime 2019/6/19 10:36 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getUserInfoList") @RequiresPermissions("sys:user:info") public Map<String,Object> getUserInfoList(){ Map<String,Object> map = new HashMap<>(); List<SysUserEntity> sysUserEntityList = sysUserService.list(); map.put("sysUserEntityList",sysUserEntityList); return map; } /** * 獲取角色信息集合 * @Author Sans * @CreateTime 2019/6/19 10:37 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getRoleInfoList") @RequiresPermissions("sys:role:info") public Map<String,Object> getRoleInfoList(){ Map<String,Object> map = new HashMap<>(); List<SysRoleEntity> sysRoleEntityList = sysRoleService.list(); map.put("sysRoleEntityList",sysRoleEntityList); return map; } /** * 獲取權限信息集合 * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getMenuInfoList") @RequiresPermissions("sys:menu:info") public Map<String,Object> getMenuInfoList(){ Map<String,Object> map = new HashMap<>(); List<SysMenuEntity> sysMenuEntityList = sysMenuService.list(); map.put("sysMenuEntityList",sysMenuEntityList); return map; } /** * 獲取全部數據 * @Author Sans * @CreateTime 2019/6/19 10:38 * @Return Map<String,Object> 返回結果 */ @RequestMapping("/getInfoAll") @RequiresPermissions("sys:info:all") public Map<String,Object> getInfoAll(){ Map<String,Object> map = new HashMap<>(); List<SysUserEntity> sysUserEntityList = sysUserService.list(); map.put("sysUserEntityList",sysUserEntityList); List<SysRoleEntity> sysRoleEntityList = sysRoleService.list(); map.put("sysRoleEntityList",sysRoleEntityList); List<SysMenuEntity> sysMenuEntityList = sysMenuService.list(); map.put("sysMenuEntityList",sysMenuEntityList); return map; } /** * 添加管理員角色權限(測試動態權限更新) * @Author Sans * @CreateTime 2019/6/19 10:39 * @Param username 用戶ID * @Return Map<String,Object> 返回結果 */ @RequestMapping("/addMenu") public Map<String,Object> addMenu(){ //添加管理員角色權限 SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity(); sysRoleMenuEntity.setMenuId(4L); sysRoleMenuEntity.setRoleId(1L); sysRoleMenuService.save(sysRoleMenuEntity); //清除緩存 String username = "admin"; ShiroUtils.deleteCache(username,false); Map<String,Object> map = new HashMap<>(); map.put("code",200); map.put("msg","權限添加成功"); return map; } }
建立 UserLoginController 登陸類
/** * @Description 用戶登陸 * @Author Sans * @CreateTime 2019/6/17 15:21 */ @RestController @RequestMapping("/userLogin") public class UserLoginController { @Autowired private SysUserService sysUserService; /** * 登陸 * @Author Sans * @CreateTime 2019/6/20 9:21 */ @RequestMapping("/login") public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){ Map<String,Object> map = new HashMap<>(); //進行身份驗證 try{ //驗證身份和登錄 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword()); //驗證成功進行登陸操做 subject.login(token); }catch (IncorrectCredentialsException e) { map.put("code",500); map.put("msg","用戶不存在或者密碼錯誤"); return map; } catch (LockedAccountException e) { map.put("code",500); map.put("msg","登陸失敗,該用戶已被凍結"); return map; } catch (AuthenticationException e) { map.put("code",500); map.put("msg","該用戶不存在"); return map; } catch (Exception e) { map.put("code",500); map.put("msg","未知異常"); return map; } map.put("code",0); map.put("msg","登陸成功"); map.put("token",ShiroUtils.getSession().getId().toString()); return map; } /** * 未登陸 * @Author Sans * @CreateTime 2019/6/20 9:22 */ @RequestMapping("/unauth") public Map<String,Object> unauth(){ Map<String,Object> map = new HashMap<>(); map.put("code",500); map.put("msg","未登陸"); return map; } }
登陸成功後會返回 TOKEN, 由於是單點登陸, 再次登錄的話會返回新的 TOKEN, 以前 Redis 的 TOKEN 就會失效了
當第一次訪問接口後咱們能夠看到緩存中已經有權限數據了, 在次訪問接口的時候, Shiro 會直接去緩存中拿取權限, 注意訪問接口時候要設置請求頭.
ADMIN 這個號如今沒有 sys:info:all 這個權限的, 因此沒法訪問 getInfoAll 接口, 咱們要動態分配權限後, 要清掉緩存, 在訪問接口時候, Shiro 會去從新執行受權方法, 以後再次把權限和角色數據放入緩存中
訪問添加權限測試接口, 由於是測試, 我把增長權限的用戶 ADMIN 寫死在裏面了, 權限添加後, 調用工具類清掉緩存, 咱們能夠發現, Redis 中已經沒有緩存了
再次訪問 getInfoAll 接口, 由於緩存中沒有數據, Shiro 會從新受權查詢權限, 攔截經過
https://gitee.com/liselotte/s...
(完)