在一款應用的整個生命週期,咱們都會談及該應用的數據安全問題。用戶的合法性與數據的可見性是數據安全中很是重要的一部分。可是,一方面,不一樣的應用對於數據的合法性和可見性要求的維度與粒度都有所區別;另外一方面,以當前微服務、多服務的架構方式,如何共享Session,如何緩存認證和受權數據應對高併發訪問都迫切須要咱們解決。Shiro的出現讓咱們能夠快速和簡單的應對咱們應用的數據安全問題html
這個官網解釋不抽象,因此直接用官網解釋:Apache Shiro™是一個強大且易用的 Java 安全框架,能夠執行身份驗證、受權、加密和會話管理等。基於 Shiro 的易於理解的API,您能夠快速、輕鬆地使任何應用程序變得安全(從最小的移動應用到最大的網絡和企業應用)。前端
談及安全,多數 Java 開發人員都離不開 Spring 框架的支持,天然也就會先想到 Spring Security,那咱們先來看兩者的差異java
Shiro | Spring Security |
---|---|
簡單、靈活 | 複雜、笨重 |
可脫離Spring | 不可脫離Spring |
粒度較粗 | 粒度較細 |
雖然 Spring Security 屬於名震中外 Spring 家族的一部分,可是瞭解 Shiro 以後,你不會想 「嫁入豪門」,而是選擇追求「詩和遠方」衝動。git
橫當作嶺側成峯,遠近高低各不一樣 (依舊是先了解概念就好)程序員
它是一個主體,表明了當前「用戶」,這個用戶不必定是一個具體的人,與當前應用交互的任何東西都是Subject,如網絡爬蟲,機器人等;即一個抽象概念;全部 Subject 都綁定到 SecurityManager,與 Subject 的全部交互都會委託給SecurityManager;能夠把 Subject 認爲是一個門面;SecurityManager 纔是實際的執行者web
安全管理器;即全部與安全有關的操做都會與 SecurityManager 交互;且它管理着全部 Subject;能夠看出它是 Shiro 的核心,它負責與後邊介紹的其餘組件進行交互,若是學習過 SpringMVC,你能夠把它當作 DispatcherServlet前端控制器面試
域,Shiro 從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它須要從 Realm 獲取相應的用戶進行比較以肯定用戶身份是否合法;也須要從 Realm 獲得用戶相應的角色/權限進行驗證用戶是否能進行操做;能夠把 Realm 當作 DataSource,即安全數據源。redis
看圖瞬間懵逼?別慌,會爲你拆解來看,結合着圖看下面的解釋,這不是啥大問題,且看:spring
主體,能夠看到主體能夠是任何能夠與應用交互的 「用戶」sql
至關於 SpringMVC 中的 DispatcherServlet;是 Shiro 的心臟;全部具體的交互都經過 SecurityManager 進行控制;它管理着全部 Subject、且負責進行認證和受權、及會話、緩存的管理
認證器,負責主體認證的,這是一個擴展點,若是用戶以爲 Shiro 默認的很差,能夠自定義實現;須要自定義認證策略(Authentication Strategy),即什麼狀況下算用戶認證經過了
受權器,或者訪問控制器,用來決定主體是否有權限進行相應的操做;即控制着用戶能訪問應用中的哪些功能
能夠有 1 個或多個 Realm,能夠認爲是安全實體數據源,即用於獲取安全實體的;能夠是JDBC實現,也能夠是LDAP實現,或者內存實現等等;由用戶提供;注意:Shiro 不知道你的用戶/權限存儲在哪及以何種格式存儲;因此咱們通常在應用中都須要實現本身的Realm
若是寫過 Servlet 就應該知道 Session 的概念,Session 須要有人去管理它的生命週期,這個組件就是 SessionManager;而Shiro 並不只僅能夠用在 Web 環境,也能夠用在如普通的 JavaSE 環境、EJB等環境;因此,Shiro 就抽象了一個本身的Session 來管理主體與應用之間交互的數據;這樣的話,好比咱們在 Web 環境用,剛開始是一臺Web服務器;接着又上了臺EJB 服務器;這時又想把兩臺服務器的會話數據放到一個地方,咱們就能夠實現本身的分佈式會話(如把數據放到Memcached 服務器)
DAO你們都用過,數據訪問對象,用於會話的 CRUD,好比咱們想把 Session 保存到數據庫,那麼能夠實現本身的SessionDAO,經過如JDBC寫到數據庫;好比想把 Session 放到 Memcached 中,能夠實現本身的 Memcached SessionDAO;另外 SessionDAO 中可使用 Cache 進行緩存,以提升性能;
緩存控制器,來管理如用戶、角色、權限等的緩存的;由於這些數據基本上不多去改變,放到緩存中後能夠提升訪問的性能
密碼模塊,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 了,總體問題不大,你們自行嘗試就好
你就讓我看這?這只是一個概覽,先作到心中有數,咱們來看具體配置,逐步完成搭建
其中 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 一般是最適合咱們業務須要的,認證一般是校驗登陸用戶是否合法。
@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
public interface UserRepository extends JpaRepository<User, Long> {
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,主要是爲了重寫 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;
}
複製代碼
這些代碼我須要作一個說明,你可能也滿肚子疑惑:
用戶訪問/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;
}
複製代碼
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
@Query(value = "select roleId from UserRoleRel ur where ur.userId = ?1")
List<Long> findUserRole(Long userId);
List<Role> findByIdIn(List<Long> ids);
}
複製代碼
@Data
@Entity
public class Permission {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Column(unique =true)
private String permCode;
private String permName;
}
複製代碼
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
@Query(value = "select permId from RolePermRel pr where pr.roleId in ?1")
List<Long> findRolePerm(List<Long> roleIds);
List<Permission> findByIdIn(List<Long> 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;
}
複製代碼
@RequiresPermissions("user:list:view")
@GetMapping()
public void getAllUsers(){
List<User> users = userRepository.findAll();
}
複製代碼
@RequiresPermissions("user:list:view")
註解說明具備用戶:列表:查看權限的才能夠訪問),官網明確給出權限定義格式,包括通配符等,我但願你自行去查看
自定義 CustomRealm (主要重寫 doGetAuthorizationInfo) 方法:
與認證流程一模一樣,只不過多了用戶,角色,權限的關係罷了
這裏經過過濾器(見Shiro配置)和註解兩者結合的方式來進行受權,和認證流程同樣,最終會走到咱們自定義的 CustomRealm 中,一樣 Shiro 默認提供了許多註解用來處理不一樣的受權狀況
註解 | 功能 |
---|---|
@RequiresGuest | 只有遊客能夠訪問 |
@RequiresAuthentication | 須要登陸才能訪問 |
@RequiresUser | 已登陸的用戶或「記住我」的用戶能訪問 |
@RequiresRoles | 已登陸的用戶需具備指定的角色才能訪問 |
@RequiresPermissions | 已登陸的用戶需具備指定的權限才能訪問(若是不想和產品經理華山論劍,推薦用這個註解) |
受權官網給出明確的受權策略與案例,請查看:shiro.apache.org/permissions…
上面的例子咱們經過一直在經過訪問 Mysql 獲取用戶認證和受權信息,這中方式明顯不符合生產環境的需求
作過 Web 開發的同窗都知道 Session 的概念,最經常使用的是 Session 過時時間,數據在 Session 的 CRUD,一樣看上圖,咱們須要關注 SessionManager 和 SessionDAO 模塊,Shiro starter 已經提供了基本的 Session配置信息,咱們按需在YAML中配置就好(官網https://shiro.apache.org/spring-boot.html 已經明確給出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管理
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, Object> stringObjectRedisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
複製代碼
查看源碼,能夠看到調用默認SessionManager的retriveSession方法,咱們重寫該方法,將Session放入HttpRequest中,進一步提升session訪問效率
其實在概覽模塊已經給出代碼展現,這裏單獨列出來作說明:
/** * 自定義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中
@Slf4j
@Component
public class RedisCache<K, V> implements Cache<K, V> {
public static final String SHIRO_PREFIX = "shiro-cache:";
@Resource
private RedisTemplate<String, Object> 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<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}
複製代碼
public class RedisCacheManager implements CacheManager {
@Resource
private RedisCache redisCache;
@Override
public <K, V> Cache<K, V> 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 有很大幫助,點擊文末「閱讀原文」,效果更好
落霞與孤鶩齊飛 秋水共長天一色,產品經理和程序員一片祥和…
本文的好多表格是從官網粘貼的,如何將其直接轉換成 MD table 呢?那麼 www.tablesgenerator.com/markdown_ta… 就能夠幫到你了,不管是生成 MD table,仍是粘貼內容生成 table 和內容都是極好的,固然了不止 MD table,本身發現吧,更多工具,公衆號回覆 「工具」得到
歡迎持續關注公衆號:「日拱一兵」
- 前沿 Java 技術乾貨分享
- 高效工具彙總 回覆「工具」
- 面試問題分析與解答
- 技術資料領取 回覆「資料」
以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......