項目是SpringCloud框架,分布式項目,包括Eureka、Zuul、Config、User-Svr(用戶管理的服務,既是服務端也是客戶端);
SpringCloud框架的SpringBoot 的項目搭建就不再贅述,這里重點介紹如何引入集成 Shiro 框架:
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼學和會話管理。使用Shiro的易於理解的API,您可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
一、數據庫設計
這里數據庫表為5個分別是: 用戶表、角色表、權限表、用戶角色關系表、角色權限資源關系表
遵循三步走:導包,配置,寫代碼
二、導包(引入依賴)
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.sun.xml.fastinfoset/FastInfoset --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <scope>compile</scope> <version>1.4.2</version> </dependency> <!-- shiro+redis緩存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version> <scope>compile</scope> </dependency>
三、創建ShiroConfig配置ShiroServerConfig、ShiroAnnotionConfig
package com.iot.microservice.shiroconfig; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * Created by IntelliJ IDEA * 這是一個神奇的Class * * @author zhz * @date 2019/12/13 16:31 */ @Configuration public class ShiroServerConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 //訪問的是后端url的地址,這里要寫base 服務的公用登錄接口。 shiroFilterFactoryBean.setLoginUrl("http://localhost:18900/base/loginpage"); // 登錄成功后要跳轉的鏈接;現在應該沒用 //shiroFilterFactoryBean.setSuccessUrl("/index"); // 未授權界面;可以寫個公用的403頁面 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // private Map<String, Filter> filters; shiro有一些默認的攔截器 比如auth,它就是FormAuthenticationFilter表單攔截器 <取名,攔截器地址>,可以自定義攔截器放在這 //private Map<String, String> filterChainDefinitionMap; <url,攔截器名>哪些路徑會被此攔截器攔截到 //Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); //AdminFilter ad=new AdminFilter(); //filters.put("ad", ad); // 攔截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的鏈接 順序判斷 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/loginpage", "anon"); filterChainDefinitionMap.put("/swagger-ui.html#", "anon"); filterChainDefinitionMap.put("/base/test", "authc"); // 配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了,加上這個會導致302,請求重置,暫不明白原因 //filterChainDefinitionMap.put("/logout", "logout"); //配置某個url需要某個權限碼 //filterChainDefinitionMap.put("/hello", "perms[how_are_you]"); // 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問;user:remember me的可以訪問--> // filterChainDefinitionMap.put("/fine", "user"); //filterChainDefinitionMap.put("/focus/**", "ad"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); System.out.println("Shiro攔截器工廠類注入成功"); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設置realm. securityManager.setRealm(myShiroRealm()); // 自定義緩存實現 使用redis securityManager.setCacheManager(cacheManager()); // 自定義session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } /** * 身份認證realm; (這個需要自己寫,賬號密碼校驗;權限等) * * @return */ @Bean public ShiroServerRealm myShiroRealm() { ShiroServerRealm myShiroRealm = new ShiroServerRealm(); return myShiroRealm; } /** * cacheManager 緩存 redis實現 * 使用的是shiro-redis開源插件 * * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis開源插件 * * @return */ @Bean public RedisManager redisManager() { RedisManager redisManager = new MyRedisManager(); // RedisManager redisManager = new RedisManager(); // redisManager.setHost(host); // redisManager.setPort(port); // // 配置緩存過期時間 // redisManager.setExpire(expireTime); // redisManager.setTimeout(timeOut); // redisManager.setPassword(password); return redisManager; } // /** // * 配置shiro redisManager // * 網上的一個 shiro-redis 插件,實現了shiro的cache接口、CacheManager接口就 // * @return // */ // @Bean // public RedisManager redisManager() { // RedisManager redisManager = new RedisManager(); // redisManager.setHost("localhost"); // redisManager.setPort(6379); // redisManager.setExpire(18000);// 配置過期時間 // // redisManager.setTimeout(timeout); // // redisManager.setPassword(password); // return redisManager; // } /** * Session Manager * 使用的是shiro-redis開源插件 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } /** * RedisSessionDAO shiro sessionDao層的實現 通過redis * 使用的是shiro-redis開源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } // /** // * 限制同一賬號登錄同時登錄人數控制 // * // * @return // */ // @Bean // public KickoutSessionControlFilter kickoutSessionControlFilter() { // KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); // kickoutSessionControlFilter.setCacheManager(cacheManager()); // kickoutSessionControlFilter.setSessionManager(sessionManager()); // kickoutSessionControlFilter.setKickoutAfter(false); // kickoutSessionControlFilter.setMaxSession(1); // kickoutSessionControlFilter.setKickoutUrl("/auth/kickout"); // return kickoutSessionControlFilter; // } /*** * 授權所用配置 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /*** * 使授權注解起作用不如不想配置可以在pom文件中加入 * <dependency> *<groupId>org.springframework.boot</groupId> *<artifactId>spring-boot-starter-aop</artifactId> *</dependency> * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
package com.iot.microservice.shiroconfig; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; @Configuration public class ShiroAnnotionConfig { /** * Shiro生命周期處理器 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ 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; } }
四、自定義Realm ShiroServerRealm
package com.iot.microservice.shiroconfig; import com.keenyoda.iot.microservice.userservice.PrivilegeService; import com.keenyoda.iot.microservice.userservice.UserService; import com.keenyoda.iot.pojos.rbac.ResourceVo; import com.keenyoda.iot.pojos.user.UserEntity; 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.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; /** * Created by IntelliJ IDEA * 這是一個神奇的Class * * @author zhz * @date 2019/12/13 16:31 */ public class ShiroServerRealm extends AuthorizingRealm { Boolean cachingEnabled=true; @Autowired private PrivilegeService privilegeService; @Autowired private UserService userService; /** * 1.授權方法,在請求需要操作碼的接口時會執行此方法。不需要操作碼的接口不會執行 * 2.實際上是 先執行 AuthorizingRealm,自定義realm的父類中的 getAuthorizationInfo方法, * 邏輯是先判斷緩存中是否有用戶的授權信息(用戶擁有的操作碼),如果有 就直返回不調用自定義 realm的授權方法了, * 如果沒緩存,再調用自定義realm,去數據庫查詢。 * 用庫查詢一次過后,如果 在安全管理器中注入了 緩存,授權信息就會自動保存在緩存中,下一次調用需要操作碼的接口時, * 就肯定不會再調用自定義realm授權方法了。 網上有分析AuthorizingRealm,shiro使用緩存的過程 * 3.AuthorizingRealm 有多個實現類realm,推測可能是把 自定義realm注入了安全管理器,所以才調用自定義的 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo(); UserEntity userEntity=(UserEntity) principals.getPrimaryPrincipal(); List<ResourceVo> resourceVos = privilegeService.selectResourceVoListByUserId(userEntity.getId()); if(resourceVos!=null){ for (ResourceVo resourceVo:resourceVos) { simpleAuthorInfo.addStringPermission(resourceVo.getResource()); } } return simpleAuthorInfo; } /** * 1.和授權方法一樣,AuthenticatingRealm的getAuthenticationInfo,先判斷緩存是否有認證信息,沒有就調用 * 但試驗,登錄之后,再次登錄,發現還是調用了認證方法,說明第一次認證登錄時,沒有將認證信息存到緩存中。不像授權信息, * 將緩存注入安全管理器,就自動保存了授權信息。 難道無法 防止故意多次登錄 ,按理說不應該啊? * 2 可以在登錄controller簡單用session是否有key 判斷是否登錄? */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { //獲取基於用戶名和密碼的令牌 //實際上這個authcToken是從LoginController里面currentUser.login(token)傳過來的 UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String account = token.getUsername(); UserEntity user = userService.findUserUserId(account); if(user==null){throw new AuthenticationException("用戶不存在");} //進行認證,將正確數據給shiro處理 //密碼不用自己比對,AuthenticationInfo認證信息對象,一個接口,new他的實現類對象SimpleAuthenticationInfo /* 第一個參數隨便放,可以放user對象,程序可在任意位置獲取 放入的對象 * 第二個參數必須放密碼, * 第三個參數放 當前realm的名字,因為可能有多個realm*/ UserEntity baseUserVM = EntityUtils.entity2VM(user, UserEntity.class, ""); SimpleAuthenticationInfo authcInfo=new SimpleAuthenticationInfo(baseUserVM, user.getPwd(), this.getName()); //密碼憑證器加鹽 authcInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getId())); //清緩存中的授權信息,保證每次登陸 都可以重新授權。因為AuthorizingRealm會先檢查緩存有沒有 授權信息,再調用授權方法 super.clearCachedAuthorizationInfo(authcInfo.getPrincipals()); return authcInfo; //返回給安全管理器,securityManager,由securityManager比對數據庫查詢出的密碼和頁面提交的密碼 //如果有問題,向上拋異常,一直拋到控制器 } }
工具類
package com.iot.microservice.shiroconfig; import com.github.pagehelper.Page; import org.springframework.beans.BeanUtils; import org.springframework.util.CollectionUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class EntityUtils { /** * 實體列表轉Vm * * @param source 原列表 * @param vmClass vm類 * @param ignoreProperties 忽略的字段 * @param <T> 泛型 * @return vm列表 */ public static <T> List<T> entity2VMList(List<?> source, Class<T> vmClass, String... ignoreProperties) { List<T> target = (source instanceof Page ? new Page<T>() : new ArrayList<T>()); if (source instanceof Page) { BeanUtils.copyProperties(source, target); } if (CollectionUtils.isEmpty(source)) { return target; } source.forEach(e -> { target.add(entity2VM(e, vmClass, ignoreProperties)); }); return target; } /** * 實體轉VM * * @param source 原對象 * @param vmClass 要轉換的對象 * @param ignoreProperties 忽略的屬性 * @param <T> 泛型 * @return 轉換后對象 * @author Say */ public static <T> T entity2VM(Object source, Class<T> vmClass, String... ignoreProperties) { if (null == source) { return null; } try { T target = vmClass.newInstance(); BeanUtils.copyProperties(source, target, ignoreProperties); return target; } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } return null; } /** * VM轉實體 * 底層用的vm2Entity,只是方法名做區分 * * @param source vm * @param entClass 實體 * @param ignoreProperties 忽略的屬性 * @param <T> 泛型 * @return 轉換后的對象 * @author Say */ public static <T> T vm2Entity(Object source, Class<T> entClass, String... ignoreProperties) { return entity2VM(source, entClass, ignoreProperties); } /** * VM轉實體集合 * 底層用的entity2VMList,只是方法名做區分 * * @param source 原對象 * @param entClass 實體 * @param ignoreProperties 忽略的屬性 * @param <T> 泛型 * @return 轉換后的對象 * @author Say */ public static <T> List<T> vm2EntityList(List<?> source, Class<T> entClass, String... ignoreProperties) { return entity2VMList(source, entClass, ignoreProperties); } /** * Entity VM 互轉 * * @param object 數據源 * @param laterObject 轉換對象 * @param <T> 泛型 */ public static <T> void copyProperties(final T object, T laterObject) { if (null == object || null == laterObject) { return; } ConcurrentHashMap<String, Method> getMethods = findGetMethods(object.getClass().getMethods()); ConcurrentHashMap<String, Method> setMethods = findSetMethods(laterObject.getClass().getDeclaredMethods()); Iterator<Map.Entry<String, Method>> iterator = getMethods.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Method> entry = iterator.next(); String methodName = entry.getKey(); Method getMethod = entry.getValue(); Method setMethod = setMethods.get(methodName); if (null == setMethod) { continue; } try { Object value = getMethod.invoke(object, new Object[]{}); setMethod.invoke(laterObject, value); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } /** * 獲取所有的get方法 * * @param methods 所有的方法 * @return 所有的get方法 */ private static ConcurrentHashMap<String, Method> findGetMethods(Method[] methods) { ConcurrentHashMap<String, Method> getMethodsMap = new ConcurrentHashMap<>(); for (Method method : methods) { if (isGetMethod(method.getName())) { getMethodsMap.put(getMethodName(method.getName()), method); } } return getMethodsMap; } /** * 獲取所有的set方法 * * @param methods 所有的方法 * @return 所有的set方法 */ private static ConcurrentHashMap<String, Method> findSetMethods(Method[] methods) { ConcurrentHashMap<String, Method> setMethodsMap = new ConcurrentHashMap<>(); for (Method method : methods) { if (isSetMethod(method.getName())) { setMethodsMap.put(getMethodName(method.getName()), method); } } return setMethodsMap; } /** * 取方法名 * * @param getMethodName 方法名稱 * @return 去掉get set的方法名 */ private static String getMethodName(String getMethodName) { String fieldName = getMethodName.substring(3, getMethodName.length()); return fieldName; } /** * 判斷是否是get方法 * * @param methodName * @return */ private static boolean isGetMethod(String methodName) { int index = methodName.indexOf("get"); if (index == 0) { return true; } return false; } /** * 判斷是否是set方法 * * @param methodName 方法名 * @return 是否為set 方法 */ private static boolean isSetMethod(String methodName) { int index = methodName.indexOf("set"); if (index == 0) { return true; } return false; } }
五、異常處理類,攔截未授權頁面(未授權頁面有三種實現方式,我這里使用異常處理)
package com.iot.microservice.shiroconfig; import com.iot.commons.Message; import com.iot.commons.enumpackage.ErrorCodeEnum; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; /** * Created by IntelliJ IDEA * 這是一個神奇的Class * 全局捕捉無權限異常 * * @author zhz * @date 2019/12/13 15:40 */ @ControllerAdvice public class GlobalDefaultExceptionHandler { @ExceptionHandler(UnauthorizedException.class) @ResponseBody public Message defaultExceptionHandler(HttpServletRequest req,Exception e){ return new Message(ErrorCodeEnum.UNAUTHORIZED.getValue(),"對不起,你沒有訪問權限!"); } @ExceptionHandler(AuthorizationException.class) @ResponseBody public Message throwAuthenticationException(HttpServletRequest req,Exception e){ return new Message(ErrorCodeEnum.AUTHENTICATION_EXCEPTION.getValue(),"賬號驗證異常,請重新登錄!"); } }
六、因為不想我這里把redis單獨做成了一個服務,為了不用多次配置,重寫RedisManager 中的兩個方法
package com.iot.microservice.shiroconfig; import com.iot.microservice.redisservice.RedisService; import org.crazycake.shiro.RedisManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Base64; /** * Created by IntelliJ IDEA * 這是一個神奇的Class * * @author zhz * @date 2019/12/13 16:31 */ @Component public class MyRedisManager extends RedisManager { @Autowired RedisService redisService; @Override public byte[] set(byte[] key, byte[] value, int expire) { String val = Base64.getEncoder().encodeToString(value); expire=12000; redisService.set(new String(key),val,expire); return value; } @Override public byte[] get(byte[] key){ String s = redisService.get(new String(key)); if (s == null){ return null; } return Base64.getDecoder().decode(s); } public static void main(String[] args) { String a = null; System.out.println(Base64.getDecoder().decode(a)); } }
七、登錄部分代碼
/** * 用戶登錄 * zhz * * @param loginUser */ @RequestMapping("login") @ResponseBody public Message<String> login(LoginUserVM loginUser) throws IncorrectCredentialsException { Asserts.notEmpty(loginUser,"登錄用戶不能為空"); String account=loginUser.getLoginName(); String password=loginUser.getPassword(); UsernamePasswordToken token = new UsernamePasswordToken(account,password,false); token.setRememberMe(true); Subject currentUser = SecurityUtils.getSubject(); try { currentUser.login(token); } catch(IncorrectCredentialsException e){ return Message.ok("密碼錯誤",500); } catch (AuthenticationException e) { // return Message.ok("登錄失敗"); return Message.ok(e.getMessage(),500); } return Message.ok(FocusMicroBaseConstants.SUCCESS); }
private Message getUserToken(UserEntity userEntity, UserInfo userInfo) { UsernamePasswordToken userToken = new UsernamePasswordToken(userEntity.getId(), userEntity.getPwd(), false); userToken.setRememberMe(true); Subject currentUser = SecurityUtils.getSubject(); try { currentUser.login(userToken); } catch (IncorrectCredentialsException e) { return new Message(ErrorCodeEnum.PARAM_ERROR.getValue(), "密碼錯誤"); } catch (AuthenticationException e) { return new Message(ErrorCodeEnum.FAILED.getValue(), "failed"); } return new Message(ErrorCodeEnum.SUCCESS.getValue(), userInfo); }
感謝幾位大牛提供的詳細介紹
參考 https://www.iteye.com/blog/jinnianshilongnian-2049092;
https://blog.csdn.net/u014203449/article/details/88087516;