摘要:寫在前面在一款應用的整個生命周期,我們都會談及該應用的數據安全問題。用戶的合法性與數據的可見性是數據安全中非常重要的一部分。
寫在前面
在一款應用的整個生命周期,我們都會談及該應用的數據安全問題。用戶的合法性與數據的可見性是數據安全中非常重要的一部分。但是,一方面,不同的應用對于數據的合法性和可見性要求的維度與粒度都有所區別;另一方面,以當前微服務、多服務的架構方式,如何共享Session,如何緩存認證和授權數據應對高并發訪問都迫切需要我們解決。Shiro的出現讓我們可以快速和簡單的應對我們應用的數據安全問題
Shiro介紹 Shiro簡介這個官網解釋不抽象,所以直接用官網解釋:Apache Shiro?是一個強大且易用的 Java 安全框架,可以執行身份驗證、授權、加密和會話管理等。基于 Shiro 的易于理解的API,您可以快速、輕松地使任何應用程序變得安全(從最小的移動應用到最大的網絡和企業應用)。
談及安全,多數 Java 開發人員都離不開 Spring 框架的支持,自然也就會先想到 Spring Security,那我們先來看二者的差別
Shiro | Spring Security |
---|---|
簡單、靈活 | 復雜、笨重 |
可脫離Spring | 不可脫離Spring |
粒度較粗 | 粒度較細 |
雖然 Spring Security 屬于名震中外 Spring 家族的一部分,但是了解 Shiro 之后,你不會想 “嫁入豪門”,而是選擇追求「詩和遠方」沖動。
橫看成嶺側成峰,遠近高低各不同 (依舊是先了解概念就好)
遠看 Shiro 看輪廓 Subject它是一個主體,代表了當前“用戶”,這個用戶不一定是一個具體的人,與當前應用交互的任何東西都是Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有 Subject 都綁定到 SecurityManager,與 Subject 的所有交互都會委托給SecurityManager;可以把 Subject 認為是一個門面;SecurityManager 才是實際的執行者
SecurityManager安全管理器;即所有與安全有關的操作都會與 SecurityManager 交互;且它管理著所有 Subject;可以看出它是 Shiro 的核心,它負責與后邊介紹的其他組件進行交互,如果學習過 SpringMVC,你可以把它看成 DispatcherServlet前端控制器
Realm域,Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那么它需要從 Realm 獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色/權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource,即安全數據源。
近看 Shiro 看細節看圖瞬間懵逼?別慌,會為你拆解來看,結合著圖看下面的解釋,這不是啥大問題,且看:
Subject主體,可以看到主體可以是任何可以與應用交互的 “用戶”
SecurityManager相當于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心臟;所有具體的交互都通過 SecurityManager 進行控制;它管理著所有 Subject、且負責進行認證和授權、及會話、緩存的管理
Authenticator認證器,負責主體認證的,這是一個擴展點,如果用戶覺得 Shiro 默認的不好,可以自定義實現;需要自定義認證策略(Authentication Strategy),即什么情況下算用戶認證通過了
Authrizer授權器,或者訪問控制器,用來決定主體是否有權限進行相應的操作;即控制著用戶能訪問應用中的哪些功能
Realm可以有 1 個或多個 Realm,可以認為是安全實體數據源,即用于獲取安全實體的;可以是JDBC實現,也可以是LDAP實現,或者內存實現等等;由用戶提供;注意:Shiro 不知道你的用戶/權限存儲在哪及以何種格式存儲;所以我們一般在應用中都需要實現自己的Realm
SessionManager如果寫過 Servlet 就應該知道 Session 的概念,Session 需要有人去管理它的生命周期,這個組件就是 SessionManager;而Shiro 并不僅僅可以用在 Web 環境,也可以用在如普通的 JavaSE 環境、EJB等環境;所以,Shiro 就抽象了一個自己的Session 來管理主體與應用之間交互的數據;這樣的話,比如我們在 Web 環境用,剛開始是一臺Web服務器;接著又上了臺EJB 服務器;這時又想把兩臺服務器的會話數據放到一個地方,我們就可以實現自己的分布式會話(如把數據放到Memcached 服務器)
SessionDAODAO大家都用過,數據訪問對象,用于會話的 CRUD,比如我們想把 Session 保存到數據庫,那么可以實現自己的SessionDAO,通過如JDBC寫到數據庫;比如想把 Session 放到 Memcached 中,可以實現自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 進行緩存,以提高性能;
CacheManager緩存控制器,來管理如用戶、角色、權限等的緩存的;因為這些數據基本上很少去改變,放到緩存中后可以提高訪問的性能
Cryptography密碼模塊,Shiro提高了一些常見的加密組件用于如密碼「加密/解密」的
注意上圖的結構,我們會根據這張圖來逐步拆分講解,記住這張圖也更有助于我們理解 Shiro 的工作原理,所以依舊是打開兩個網頁一起看就好嘍
搭建概覽多數小伙伴都在使用 Spring Boot, Shiro 也很應景的定義了 starter,做了更好的封裝,對于我們來說使用起來也就更加方便,來看選型概覽
序號 | 名稱 | 版本 |
---|---|---|
1 | Springboot | 2.0.4 |
2 | JPA | 2.0.4 |
3 | Mysql | 8.0.12 |
4 | Redis | 2.0.4 |
5 | Lombok | 1.16.22 |
6 | Guava | 26.0-jre |
7 | Shiro | 1.4.0 |
使用 Spring Boot,大多都是通過添加 starter 依賴,會自動解決依賴包版本,所以自己嘗試的時候用最新版本不會有什么問題,比如 Shiro 現在的版本是 1.5.0 了,整體問題不大,大家自行嘗試就好
添加 Gradle 依賴管理 大體目錄結構 application.yml 配置 基本配置你就讓我看這?這只是一個概覽,先做到心中有數,我們來看具體配置,逐步完成搭建
其中 shiroFilter bean 部分指定了攔截路徑和相應的過濾器,”/user/login”, ”/user”, ”/user/loginout” 可以匿名訪問,其他路徑都需要授權訪問,shiro 提供和多個默認的過濾器,我們可以用這些過濾器來配置控制指定url的權限(先了解個大概即可):
配置縮寫 | 對應的過濾器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名訪問 |
authc | FormAuthenticationFilter | 指定url需要form表單登錄,默認會從請求中獲取username、password,rememberMe等參數并嘗試登錄,如果登錄不了就會跳轉到loginUrl配置的路徑。我們也可以用這個過濾器做默認的登錄邏輯,但是一般都是我們自己在控制器寫登錄邏輯的,自己寫的話出錯返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登錄 |
Logout | LogoutFilter | 登出過濾器,配置指定url就可以實現退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止創建會話 |
perms | PermissionsAuthorizationFilter | 需要指定權限才能訪問 |
port | PortFilter | 需要指定端口才能訪問 |
rest | HttpMethodPermissionFilter | 將http請求方法轉化成相應的動詞來構造一個權限字符串,這個感覺意義不大,有興趣自己看源碼的注釋 |
roles | RolesAuthorizationFilter | 需要指定角色才能訪問 |
ssl | SslFilter | 需要https請求才能訪問 |
user | UserFilter | 需要已登錄或“記住我”的用戶才能訪問 |
數據庫表設計請參考 entity package下的 bean,通過@Entity 注解與 JPA 的設置自動生成表結構 (你需要簡單的了解一下 JPA 的功能)。
我們要說重點啦~~~
身份認證身份認證是一個證明 “李雷是李雷,韓梅梅是韓梅梅” 的過程,回看上圖,Realm 模塊就是用來做這件事的,Shiro 提供了 IniRealm,JdbcReaml,LDAPReam等認證方式,但自定義的 Realm 通常是最適合我們業務需要的,認證通常是校驗登錄用戶是否合法。
新建用戶 User@Data @Entity public class User implements Serializable { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @Column(unique =true) private String username; private String password; private String salt; }定義 Repository
@Repository public interface UserRepository extends JpaRepository編寫UserController:{ public User findUserByUsername(String username); }
@GetMapping("/login") public void login(String username, String password) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(true); Subject currentUser = SecurityUtils.getSubject(); currentUser.login(token); }自定義 Realm
自定義 Realm,主要是為了重寫 doGetAuthenticationInfo(…)方法
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); User user = userRepository.findUserByUsername(username); SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt())); return simpleAuthenticationInfo; }
這些代碼我需要做一個說明,你可能也滿肚子疑惑:
這段代碼怎么應用了 shiro?
controller 是怎么調用到 custom realm 的?
重寫的 doGetAuthenticationInfo(…) 方法目的是什么?
認證流程說明用戶訪問 /user/login 路徑,生成 UsernamePasswordToken, 通過SecurityUtils.getSubject()獲取Subject(currentUser),調用 login 方法進行驗證,讓我們跟蹤一下代碼,瞧一瞧就知道自定義的CustomRealm怎樣起作用的,一起來看源碼:
到這里我們要停一停了,請回看 Shiro 近景圖,將源碼追蹤路徑與其對比,是完全一致的
授權身份認證是驗證你是誰的問題,而授權是你能干什么的問題,
產品經理:申購模塊只能科室看
程序員:好的
產品經理:科長權限大一些,他也能看申購模塊
程序員:好的(黑臉)
產品經理:科長不但能看,還能修改數據
程序員:關公提大刀,拿命來
…
作為程序員,我們的宗旨是:「能動手就不吵吵」; 硝煙怒火拔地起,耳邊響起駝鈴聲(Shiro):「放下屠刀,立地成佛」授權沒有那么麻煩,大家好商量…
整個過程和身份認證基本是一毛一樣,你對比看看
角色實體創建涉及到授權,自然要和角色相關,所以我們創建 Role 實體:
@Data @Entity public class Role { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @Column(unique =true) private String roleCode; private String roleName; }新建 Role Repository
@Repository public interface RoleRepository extends JpaRepository定義權限實體 Permission{ @Query(value = "select roleId from UserRoleRel ur where ur.userId = ?1") List findUserRole(Long userId); List findByIdIn(List ids); }
@Data @Entity public class Permission { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @Column(unique =true) private String permCode; private String permName; }定義 Permission Repository
@Repository public interface PermissionRepository extends JpaRepository建立用戶與角色關系{ @Query(value = "select permId from RolePermRel pr where pr.roleId in ?1") List findRolePerm(List roleIds); List findByIdIn(List ids); }
其實可以通過 JPA 注解來制定關系的,這里為了說明問題,以多帶帶外鍵形式說明
@Data @Entity public class UserRoleRel { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private Long userId; private Long roleId; }建立角色與權限關系
@Data @Entity public class RolePermRel { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private Long permId; private Long roleId; }編寫 UserController
@RequiresPermissions("user:list:view") @GetMapping() public void getAllUsers(){ Listusers = userRepository.findAll(); }
@RequiresPermissions("user:list:view") 注解說明具有用戶:列表:查看權限的才可以訪問),官網明確給出權限定義格式,包括通配符等,我希望你自行去查看
自定義 CustomRealm (主要重寫 doGetAuthorizationInfo) 方法:
與認證流程如出一轍,只不過多了用戶,角色,權限的關系罷了
授權流程說明這里通過過濾器(見Shiro配置)和注解二者結合的方式來進行授權,和認證流程一樣,最終會走到我們自定義的 CustomRealm 中,同樣 Shiro 默認提供了許多注解用來處理不同的授權情況
注解 | 功能 |
---|---|
@RequiresGuest | 只有游客可以訪問 |
@RequiresAuthentication | 需要登錄才能訪問 |
@RequiresUser | 已登錄的用戶或“記住我”的用戶能訪問 |
@RequiresRoles | 已登錄的用戶需具有指定的角色才能訪問 |
@RequiresPermissions | 已登錄的用戶需具有指定的權限才能訪問(如果不想和產品經理華山論劍,推薦用這個注解) |
授權官網給出明確的授權策略與案例,請查看:http://shiro.apache.org/permi...
上面的例子我們通過一直在通過訪問 Mysql 獲取用戶認證和授權信息,這中方式明顯不符合生產環境的需求
Session會話管理做過 Web 開發的同學都知道 Session 的概念,最常用的是 Session 過期時間,數據在 Session 的 CRUD,同樣看上圖,我們需要關注 SessionManager 和 SessionDAO 模塊,Shiro starter 已經提供了基本的 Session配置信息,我們按需在YAML中配置就好(官網https://shiro.apache.org/spri... 已經明確給出Session的配置信息)
Key | Default Value | Description |
---|---|---|
shiro.enabled | true | Enables Shiro’s Spring module |
shiro.web.enabled | true | Enables Shiro’s Spring web module |
shiro.annotations.enabled | true | Enables Spring support for Shiro’s annotations |
shiro.sessionManager.deleteInvalidSessions | true | Remove invalid session from session storage |
shiro.sessionManager.sessionIdCookieEnabled | true | Enable session ID to cookie, for session tracking |
shiro.sessionManager.sessionIdUrlRewritingEnabled | true | Enable session URL rewriting support |
shiro.userNativeSessionManager | false | If enabled Shiro will manage the HTTP sessions instead of the container |
shiro.sessionManager.cookie.name | JSESSIONID | Session cookie name |
shiro.sessionManager.cookie.maxAge | -1 | Session cookie max age |
shiro.sessionManager.cookie.domain | null | Session cookie domain |
shiro.sessionManager.cookie.path | null | Session cookie path |
shiro.sessionManager.cookie.secure | false | Session cookie secure flag |
shiro.rememberMeManager.cookie.name | rememberMe | RememberMe cookie name |
shiro.rememberMeManager.cookie.maxAge | one year | RememberMe cookie max age |
shiro.rememberMeManager.cookie.domain | null | RememberMe cookie domain |
shiro.rememberMeManager.cookie.path | null | RememberMe cookie path |
shiro.rememberMeManager.cookie.secure | false | RememberMe cookie secure flag |
shiro.loginUrl | /login.jsp | Login URL used when unauthenticated users are redirected to login page |
shiro.successUrl | / | Default landing page after a user logs in (if alternative cannot be found in the current session) |
shiro.unauthorizedUrl | null | Page to redirect user to if they are unauthorized (403 page) |
分布式服務中,我們通常需要將Session信息放入Redis中來管理,來應對高并發的訪問需求,這時只需重寫SessionDAO即可完成自定義的Session管理
整合Redis@Configuration public class RedisConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public RedisTemplate重寫SessionDaostringObjectRedisTemplate() { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }
查看源碼,可以看到調用默認SessionManager的retriveSession方法,我們重寫該方法,將Session放入HttpRequest中,進一步提高session訪問效率
向ShiroConfig中添加配置其實在概覽模塊已經給出代碼展示,這里多帶帶列出來做說明:
/** * 自定義RedisSessionDao用來管理Session在Redis中的CRUD * @return */ @Bean(name = "redisSessionDao") public RedisSessionDao redisSessionDao(){ return new RedisSessionDao(); } /** * 自定義SessionManager,應用自定義SessionDao * @return */ @Bean(name = "customerSessionManager") public CustomerWebSessionManager customerWebSessionManager(){ CustomerWebSessionManager customerWebSessionManager = new CustomerWebSessionManager(); customerWebSessionManager.setSessionDAO(redisSessionDao()); return customerWebSessionManager; } /** * 定義Security manager * @param customRealm * @return */ @Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager(CustomRealm customRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(customRealm); securityManager.setSessionManager(customerWebSessionManager()); // 可不指定,Shiro會用默認Session manager securityManager.setCacheManager(redisCacheManagers()); //可不指定,Shiro會用默認CacheManager // securityManager.setSessionManager(defaultWebSessionManager()); return securityManager; } /** * 定義session管理器 * @return */ @Bean(name = "sessionManager") public DefaultWebSessionManager defaultWebSessionManager(){ DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager(); defaultWebSessionManager.setSessionDAO(redisSessionDao()); return defaultWebSessionManager; }
至此,將 session 信息由 redis 管理功能就這樣完成了
緩存管理應對分布式服務,對于高并發訪問數據庫權限內容是非常低效的方式,同樣我們可以利用Redis來解決這一問題,將授權數據緩存到Redis中
新建 RedisCache@Slf4j @Component public class RedisCache新建 RedisCacheManagerimplements Cache { public static final String SHIRO_PREFIX = "shiro-cache:"; @Resource private RedisTemplate stringObjectRedisTemplate; private String getKey(K key){ if (key instanceof String){ return (SHIRO_PREFIX + key); } return key.toString(); } @Override public V get(K k) throws CacheException { log.info("read from redis..."); V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k)); if (v != null){ return v; } return null; } @Override public V put(K k, V v) throws CacheException { stringObjectRedisTemplate.opsForValue().set(getKey(k), v); stringObjectRedisTemplate.expire(getKey(k), 100, TimeUnit.SECONDS); return v; } @Override public V remove(K k) throws CacheException { V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k)); stringObjectRedisTemplate.delete((String) get(k)); if (v != null){ return v; } return null; } @Override public void clear() throws CacheException { //不要重寫,如果只保存shiro數據無所謂 } @Override public int size() { return 0; } @Override public Set keys() { return null; } @Override public Collection values() { return null; } }
public class RedisCacheManager implements CacheManager { @Resource private RedisCache redisCache; @Override publicCache getCache(String s) throws CacheException { return redisCache; } }
至此,我們不用每次訪問 Mysql DB 來獲取認證和授權信息,而是通過 Redis 來緩存這些信息,大大提升了效率,也滿足分布式系統的設計需求
總結回復公眾號 「demo」獲取 demo 代碼。這里只是梳理了Springboot整合Shiro的流程,以及應用Redis最大化利用Shiro,Shiro的使用細節還很多,官網說的也很明確,帶著上面的架構圖來理解Shiro會事半功倍,感覺這里面的代碼挺多挺頭大的?那是你沒有自己動手去嘗試,結合官網與 demo 相信你會對 Shiro 有更好的理解,另外你可以理解 Shiro 是 mini 版本的 Spring Security,我希望以小見大,當需要更細粒度的認證授權時,也會對理解 Spring Security 有很大幫助,點擊文末「閱讀原文」,效果更好
落霞與孤鶩齊飛 秋水共長天一色,產品經理和程序員一片祥和…
靈魂追問都說 Redis 是單線程,但是很快,你知道為什么嗎?
你們項目中是怎樣控制認證授權的呢?當授權有變化,對于程序員來說,這個修改是災難嗎?
提高效率工具 MarkDown 表格生成器本文的好多表格是從官網粘貼的,如何將其直接轉換成 MD table 呢?那么 https://www.tablesgenerator.c... 就可以幫到你了,無論是生成 MD table,還是粘貼內容生成 table 和內容都是極好的,當然了不止 MD table,自己發現吧,更多工具,公眾號回復 「工具」獲得
推薦閱讀只會用 git pull ?有時候你可以嘗試更優雅的處理方式
雙親委派模型:大廠高頻面試題,輕松搞定
面試還不知道BeanFactory和ApplicationContext的區別?
如何設計好的RESTful API
程序猿為什么要看源碼?
歡迎持續關注公眾號:「日拱一兵」前沿 Java 技術干貨分享
高效工具匯總 回復「工具」
面試問題分析與解答
技術資料領取 回復「資料」
以讀偵探小說思維輕松趣味學習 Java 技術棧相關知識,本著將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/75942.html
摘要:專有的內容更少,而更多符合標準的成分。當前標簽實例的方法被調用時當前標簽的任何一個祖先的被調用時更新從父親到兒子單向傳播。相對來說,微型場景會更適合,不想要太多的外部依賴,又需要組件化數據驅動等更現代化框架的能力。 Riot.js是什么? Riot 擁有創建現代客戶端應用的所有必需的成分: 響應式 視圖層用來創建用戶界面 用來在各獨立模塊之間進行通信的事件庫 用來管理URL和瀏覽器回...
摘要:專有的內容更少,而更多符合標準的成分。當前標簽實例的方法被調用時當前標簽的任何一個祖先的被調用時更新從父親到兒子單向傳播。相對來說,微型場景會更適合,不想要太多的外部依賴,又需要組件化數據驅動等更現代化框架的能力。 Riot.js是什么? Riot 擁有創建現代客戶端應用的所有必需的成分: 響應式 視圖層用來創建用戶界面 用來在各獨立模塊之間進行通信的事件庫 用來管理URL和瀏覽器回...
摘要:,大家好,好久不賤呢最近因為看了一些的小說,整個人都比較致郁就在昨天,我用了一天的時間寫了,又一個小而美的前端框架可能你覺得,有了和,沒必要再寫一個了我覺得我還是想想辦法尋找一下它的存在感吧先看的組件化方案最先看到的應該是。 halo,大家好,好久不賤呢! 最近因為看了一些 be 的小說,整個人都比較致郁::>__+ {state.count--}}>- ...
摘要:,大家好,好久不賤呢最近因為看了一些的小說,整個人都比較致郁就在昨天,我用了一天的時間寫了,又一個小而美的前端框架可能你覺得,有了和,沒必要再寫一個了我覺得我還是想想辦法尋找一下它的存在感吧先看的組件化方案最先看到的應該是。 halo,大家好,好久不賤呢! 最近因為看了一些 be 的小說,整個人都比較致郁::>__+ {state.count--}}>- ...
閱讀 3221·2021-11-23 09:51
閱讀 3686·2021-09-22 15:35
閱讀 3660·2021-09-22 10:02
閱讀 2969·2021-08-30 09:49
閱讀 529·2021-08-05 10:01
閱讀 3395·2019-08-30 15:54
閱讀 1647·2019-08-30 15:53
閱讀 3572·2019-08-29 16:27