您好,登錄后才能下訂單哦!
今天小編給大家分享一下Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
在原來的項(xiàng)目當(dāng)中,由于沒有配置緩存,因此每次需要驗(yàn)證當(dāng)前主體有沒有訪問權(quán)限時(shí),都會(huì)去查詢數(shù)據(jù)庫。由于權(quán)限數(shù)據(jù)是典型的讀多寫少的數(shù)據(jù),因此,我們應(yīng)該要對(duì)其加入緩存的支持。
當(dāng)我們加入緩存后,shiro在做鑒權(quán)時(shí)先去緩存里查詢相關(guān)數(shù)據(jù),緩存里沒有,則查詢數(shù)據(jù)庫并將查到的數(shù)據(jù)寫入緩存,下次再查時(shí)就能從緩存當(dāng)中獲取數(shù)據(jù),而不是從數(shù)據(jù)庫中獲取。這樣就能改善我們的應(yīng)用的性能。
接下來,我們?nèi)?shí)現(xiàn)shiro的緩存管理部分。
Shiro 提供了完整的企業(yè)級(jí)會(huì)話管理功能,不依賴于底層容器(如 web 容器 tomcat),不管 JavaSE 還是 JavaEE 環(huán)境都可以使用,提供了會(huì)話管理、會(huì)話事件監(jiān)聽、會(huì)話存儲(chǔ) / 持久化、容器無關(guān)的集群、失效 / 過期支持、對(duì) Web 的透明支持、SSO 單點(diǎn)登錄的支持等特性。
我們將使用 Shiro 的會(huì)話管理來接管我們應(yīng)用的web會(huì)話,并通過Redis來存儲(chǔ)會(huì)話信息。
在Shiro當(dāng)中,它提供了CacheManager這個(gè)類來做緩存管理。
在shiro當(dāng)中,默認(rèn)使用的是EhCache緩存框架。EhCache 是一個(gè)純Java的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點(diǎn)。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
在SpringBoot整合Redis的過程中,還要注意版本匹配的問題,不然有可能報(bào)方法未找到的異常。
private void enableCache(MySQLRealm realm){ //開啟全局緩存配置 realm.setCachingEnabled(true); //開啟認(rèn)證緩存配置 realm.setAuthenticationCachingEnabled(true); //開啟授權(quán)緩存配置 realm.setAuthorizationCachingEnabled(true); //為了方便操作,我們給緩存起個(gè)名字 realm.setAuthenticationCacheName("authcCache"); realm.setAuthorizationCacheName("authzCache"); //注入緩存實(shí)現(xiàn) realm.setCacheManager(new EhCacheManager()); }
然后再在getRealm中調(diào)用這個(gè)方法即可。
提示:在這個(gè)實(shí)現(xiàn)當(dāng)中,只是實(shí)現(xiàn)了本地的緩存。也就是說緩存的數(shù)據(jù)同應(yīng)用一樣共用一臺(tái)機(jī)器的內(nèi)存。如果服務(wù)器發(fā)生宕機(jī)或意外停電,那么緩存數(shù)據(jù)也將不復(fù)存在。當(dāng)然你也可通過cacheManager.setCacheManagerConfigFile()方法給予緩存更多的配置。
接下來我們將通過Redis緩存我們的權(quán)限數(shù)據(jù)
<!--shiro-redis相關(guān)依賴--> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.1.0</version> <!-- 里面這個(gè)shiro-core版本較低,會(huì)引發(fā)一個(gè)異常 ClassNotFoundException: org.apache.shiro.event.EventBus 需要排除,直接使用上面的shiro shiro1.3 加入了時(shí)間總線。--> <exclusions> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </exclusion> </exclusions> </dependency>
在application.yml中添加redis的相關(guān)配置
spring: redis: host: 127.0.0.1 port: 6379 password: hewenping timeout: 3000 jedis: pool: min-idle: 5 max-active: 20 max-idle: 15
修改ShiroConfig配置類,添加shiro-redis插件配置
/**shiro配置類 * @author 賴柄灃 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/6 9:11 */ @Configuration public class ShiroConfig { private static final String CACHE_KEY = "shiro:cache:"; private static final String SESSION_KEY = "shiro:session:"; private static final int EXPIRE = 18000; @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; @Value("${spring.redis.jedis.pool.min-idle}") private int minIdle; @Value("${spring.redis.jedis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.jedis.pool.max-active}") private int maxActive; @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 創(chuàng)建ShiroFilter攔截器 * @return ShiroFilterFactoryBean */ @Bean(name = "shiroFilterFactoryBean") public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //配置不攔截路徑和攔截路徑,順序不能反 HashMap<String, String> map = new HashMap<>(5); map.put("/authc/**","anon"); map.put("/login.html","anon"); map.put("/js/**","anon"); map.put("/css/**","anon"); map.put("/**","authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); //覆蓋默認(rèn)的登錄url shiroFilterFactoryBean.setLoginUrl("/authc/unauthc"); return shiroFilterFactoryBean; } @Bean public Realm getRealm(){ //設(shè)置憑證匹配器,修改為hash憑證匹配器 HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher(); //設(shè)置算法 myCredentialsMatcher.setHashAlgorithmName("md5"); //散列次數(shù) myCredentialsMatcher.setHashIterations(1024); MySQLRealm realm = new MySQLRealm(); realm.setCredentialsMatcher(myCredentialsMatcher); //開啟緩存 realm.setCachingEnabled(true); realm.setAuthenticationCachingEnabled(true); realm.setAuthorizationCachingEnabled(true); return realm; } /** * 創(chuàng)建shiro web應(yīng)用下的安全管理器 * @return DefaultWebSecurityManager */ @Bean public DefaultWebSecurityManager getSecurityManager( Realm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); securityManager.setCacheManager(cacheManager()); SecurityUtils.setSecurityManager(securityManager); return securityManager; } /** * 配置Redis管理器 * @Attention 使用的是shiro-redis開源插件 * @return */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setTimeout(timeout); redisManager.setPassword(password); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(maxIdle+maxActive); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMinIdle(minIdle); redisManager.setJedisPoolConfig(jedisPoolConfig); return redisManager; } @Bean public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); redisCacheManager.setKeyPrefix(CACHE_KEY); // shiro-redis要求放在session里面的實(shí)體類必須有個(gè)id標(biāo)識(shí) //這是組成redis中所存儲(chǔ)數(shù)據(jù)的key的一部分 redisCacheManager.setPrincipalIdFieldName("username"); return redisCacheManager; } }
修改MySQLRealm
中的doGetAuthenticationInfo
方法,將User
對(duì)象整體作為SimpleAuthenticationInfo
的第一個(gè)參數(shù)。shiro-redis將根據(jù)RedisCacheManager
的principalIdFieldName
屬性值從第一個(gè)參數(shù)中獲取id值作為redis中數(shù)據(jù)的key的一部分。
/** * 認(rèn)證 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { if(token==null){ return null; } String principal = (String) token.getPrincipal(); User user = userService.findByUsername(principal); SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo( //由于shiro-redis插件需要從這個(gè)屬性中獲取id作為redis的key //所有這里傳的是user而不是username user, //憑證信息 user.getPassword(), //加密鹽值 new CurrentSalt(user.getSalt()), getName()); return simpleAuthenticationInfo; }
并修改MySQLRealm
中的doGetAuthorizationInfo
方法,從User對(duì)象中獲取主身份信息。
/** * 授權(quán) * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); String username = user.getUsername(); List<Role> roleList = roleService.findByUsername(username); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); for (Role role : roleList) { authorizationInfo.addRole(role.getRoleName()); } List<Long> roleIdList = new ArrayList<>(); for (Role role : roleList) { roleIdList.add(role.getRoleId()); } List<Resource> resourceList = resourceService.findByRoleIds(roleIdList); for (Resource resource : resourceList) { authorizationInfo.addStringPermission(resource.getResourcePermissionTag()); } return authorizationInfo; }
由于Shiro里面默認(rèn)的SimpleByteSource
沒有實(shí)現(xiàn)序列化接口,導(dǎo)致ByteSource.Util.bytes()生成的salt在序列化時(shí)出錯(cuò),因此需要自定義Salt類并實(shí)現(xiàn)序列化接口。并在自定義的Realm的認(rèn)證方法使用new CurrentSalt(user.getSalt())
傳入鹽值。
/**由于shiro當(dāng)中的ByteSource沒有實(shí)現(xiàn)序列化接口,緩存時(shí)會(huì)發(fā)生錯(cuò)誤 * 因此,我們需要通過自定義ByteSource的方式實(shí)現(xiàn)這個(gè)接口 * @author 賴柄灃 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/8 16:17 */ public class CurrentSalt extends SimpleByteSource implements Serializable { public CurrentSalt(String string) { super(string); } public CurrentSalt(byte[] bytes) { super(bytes); } public CurrentSalt(char[] chars) { super(chars); } public CurrentSalt(ByteSource source) { super(source); } public CurrentSalt(File file) { super(file); } public CurrentSalt(InputStream stream) { super(stream); } }
/**SessionId生成器 * <p>@author 賴柄灃 laibingf_dev@outlook.com</p> * <p>@date 2020/8/15 15:19</p> */ public class ShiroSessionIdGenerator implements SessionIdGenerator { /** *實(shí)現(xiàn)SessionId生成 * @param session * @return */ @Override public Serializable generateId(Session session) { Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session); return String.format("login_token_%s", sessionId); } }
/** * <p>@author 賴柄灃 laibingf_dev@outlook.com</p> * <p>@date 2020/8/15 15:40</p> */ public class ShiroSessionManager extends DefaultWebSessionManager { //定義常量 private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; //重寫構(gòu)造器 public ShiroSessionManager() { super(); this.setDeleteInvalidSessions(true); } /** * 重寫方法實(shí)現(xiàn)從請(qǐng)求頭獲取Token便于接口統(tǒng)一 * * 每次請(qǐng)求進(jìn)來, * Shiro會(huì)去從請(qǐng)求頭找Authorization這個(gè)key對(duì)應(yīng)的Value(Token) * @param request * @param response * @return */ @Override public Serializable getSessionId(ServletRequest request, ServletResponse response) { String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION); //如果請(qǐng)求頭中存在token 則從請(qǐng)求頭中獲取token if (!StringUtils.isEmpty(token)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return token; } else { // 這里禁用掉Cookie獲取方式 return null; } } }
在ShiroConfig中添加對(duì)會(huì)話管理器的配置
/** * SessionID生成器 * */ @Bean public ShiroSessionIdGenerator sessionIdGenerator(){ return new ShiroSessionIdGenerator(); } /** * 配置RedisSessionDAO */ @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 * */ @Bean public SessionManager sessionManager() { ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); shiroSessionManager.setSessionDAO(redisSessionDAO()); //禁用cookie shiroSessionManager.setSessionIdCookieEnabled(false); //禁用會(huì)話id重寫 shiroSessionManager.setSessionIdUrlRewritingEnabled(false); return shiroSessionManager; }
目前最新版本(1.6.0)中,session管理器的setSessionIdUrlRewritingEnabled(false)配置沒有生效,導(dǎo)致沒有認(rèn)證直接訪問受保護(hù)資源出現(xiàn)多次重定向的錯(cuò)誤。將shiro版本切換為1.5.0后就解決了這個(gè)bug。
本來這篇文章應(yīng)該是昨晚發(fā)的,因?yàn)檫@個(gè)原因搞了好久,所有今天才發(fā)。。。
在認(rèn)證信息返回前,我們需要做一個(gè)判斷:如果當(dāng)前用戶已在舊設(shè)備上登錄,則需要將舊設(shè)備上的會(huì)話id刪掉,使其下線。
/** * 認(rèn)證 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { if(token==null){ return null; } String principal = (String) token.getPrincipal(); User user = userService.findByUsername(principal); SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo( //由于shiro-redis插件需要從這個(gè)屬性中獲取id作為redis的key //所有這里傳的是user而不是username user, //憑證信息 user.getPassword(), //加密鹽值 new CurrentSalt(user.getSalt()), getName()); //清除當(dāng)前主體舊的會(huì)話,相當(dāng)于你在新電腦上登錄系統(tǒng),把你之前在舊電腦上登錄的會(huì)話擠下去 ShiroUtils.deleteCache(user.getUsername(),true); return simpleAuthenticationInfo; }
我們將會(huì)話信息存儲(chǔ)在redis中,并在用戶認(rèn)證通過后將會(huì)話Id以token的形式返回給用戶。用戶請(qǐng)求受保護(hù)資源時(shí)帶上這個(gè)token,我們根據(jù)token信息去redis中獲取用戶的權(quán)限信息,從而做訪問控制。
@PostMapping("/login") public HashMap<Object, Object> login(@RequestBody LoginVO loginVO) throws AuthenticationException { boolean flags = authcService.login(loginVO); HashMap<Object, Object> map = new HashMap<>(3); if (flags){ Serializable id = SecurityUtils.getSubject().getSession().getId(); map.put("msg","登錄成功"); map.put("token",id); return map; }else { return null; } }
/**shiro異常處理 * @author 賴柄灃 bingfengdev@aliyun.com * @version 1.0 * @date 2020/10/7 18:01 */ @ControllerAdvice(basePackages = "pers.lbf.springbootshiro") public class AuthExceptionHandler { //==================認(rèn)證異常====================// @ExceptionHandler(ExpiredCredentialsException.class) @ResponseBody public String expiredCredentialsExceptionHandlerMethod(ExpiredCredentialsException e) { return "憑證已過期"; } @ExceptionHandler(IncorrectCredentialsException.class) @ResponseBody public String incorrectCredentialsExceptionHandlerMethod(IncorrectCredentialsException e) { return "用戶名或密碼錯(cuò)誤"; } @ExceptionHandler(UnknownAccountException.class) @ResponseBody public String unknownAccountExceptionHandlerMethod(IncorrectCredentialsException e) { return "用戶名或密碼錯(cuò)誤"; } @ExceptionHandler(LockedAccountException.class) @ResponseBody public String lockedAccountExceptionHandlerMethod(IncorrectCredentialsException e) { return "賬戶被鎖定"; } //=================授權(quán)異常=====================// @ExceptionHandler(UnauthorizedException.class) @ResponseBody public String unauthorizedExceptionHandlerMethod(UnauthorizedException e){ return "未授權(quán)!請(qǐng)聯(lián)系管理員授權(quán)"; } }
實(shí)際開發(fā)中,應(yīng)該對(duì)返回結(jié)果統(tǒng)一化,并給出業(yè)務(wù)錯(cuò)誤碼。這已經(jīng)超出了本文的范疇,如有需要,請(qǐng)根據(jù)自身系統(tǒng)特點(diǎn)考量。
登錄成功的情況
用戶名或密碼錯(cuò)誤的情況
為了安全起見,不要暴露具體是用戶名錯(cuò)誤還是密碼錯(cuò)誤。
認(rèn)證后訪問有權(quán)限的資源
認(rèn)證后訪問無權(quán)限的資源
未認(rèn)證直接訪問的情況
三個(gè)鍵值分別對(duì)應(yīng)認(rèn)證信息緩存、授權(quán)信息緩存和會(huì)話信息緩存。
以上就是“Springboot如何實(shí)現(xiàn)認(rèn)證和動(dòng)態(tài)權(quán)限管理”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。