摘要:寫在前面在一款應(yīng)用的整個(gè)生命周期,我們都會(huì)談及該應(yīng)用的數(shù)據(jù)安全問題。用戶的合法性與數(shù)據(jù)的可見性是數(shù)據(jù)安全中非常重要的一部分。
寫在前面
在一款應(yīng)用的整個(gè)生命周期,我們都會(huì)談及該應(yīng)用的數(shù)據(jù)安全問題。用戶的合法性與數(shù)據(jù)的可見性是數(shù)據(jù)安全中非常重要的一部分。但是,一方面,不同的應(yīng)用對(duì)于數(shù)據(jù)的合法性和可見性要求的維度與粒度都有所區(qū)別;另一方面,以當(dāng)前微服務(wù)、多服務(wù)的架構(gòu)方式,如何共享Session,如何緩存認(rèn)證和授權(quán)數(shù)據(jù)應(yīng)對(duì)高并發(fā)訪問都迫切需要我們解決。Shiro的出現(xiàn)讓我們可以快速和簡單的應(yīng)對(duì)我們應(yīng)用的數(shù)據(jù)安全問題
Shiro介紹 Shiro簡介這個(gè)官網(wǎng)解釋不抽象,所以直接用官網(wǎng)解釋:Apache Shiro?是一個(gè)強(qiáng)大且易用的 Java 安全框架,可以執(zhí)行身份驗(yàn)證、授權(quán)、加密和會(huì)話管理等?;?Shiro 的易于理解的API,您可以快速、輕松地使任何應(yīng)用程序變得安全(從最小的移動(dòng)應(yīng)用到最大的網(wǎng)絡(luò)和企業(yè)應(yīng)用)。
談及安全,多數(shù) Java 開發(fā)人員都離不開 Spring 框架的支持,自然也就會(huì)先想到 Spring Security,那我們先來看二者的差別
Shiro | Spring Security |
---|---|
簡單、靈活 | 復(fù)雜、笨重 |
可脫離Spring | 不可脫離Spring |
粒度較粗 | 粒度較細(xì) |
雖然 Spring Security 屬于名震中外 Spring 家族的一部分,但是了解 Shiro 之后,你不會(huì)想 “嫁入豪門”,而是選擇追求「詩和遠(yuǎn)方」沖動(dòng)。
橫看成嶺側(cè)成峰,遠(yuǎn)近高低各不同 (依舊是先了解概念就好)
遠(yuǎn)看 Shiro 看輪廓 Subject它是一個(gè)主體,代表了當(dāng)前“用戶”,這個(gè)用戶不一定是一個(gè)具體的人,與當(dāng)前應(yīng)用交互的任何東西都是Subject,如網(wǎng)絡(luò)爬蟲,機(jī)器人等;即一個(gè)抽象概念;所有 Subject 都綁定到 SecurityManager,與 Subject 的所有交互都會(huì)委托給SecurityManager;可以把 Subject 認(rèn)為是一個(gè)門面;SecurityManager 才是實(shí)際的執(zhí)行者
SecurityManager安全管理器;即所有與安全有關(guān)的操作都會(huì)與 SecurityManager 交互;且它管理著所有 Subject;可以看出它是 Shiro 的核心,它負(fù)責(zé)與后邊介紹的其他組件進(jìn)行交互,如果學(xué)習(xí)過 SpringMVC,你可以把它看成 DispatcherServlet前端控制器
Realm域,Shiro 從 Realm 獲取安全數(shù)據(jù)(如用戶、角色、權(quán)限),就是說 SecurityManager 要驗(yàn)證用戶身份,那么它需要從 Realm 獲取相應(yīng)的用戶進(jìn)行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應(yīng)的角色/權(quán)限進(jìn)行驗(yàn)證用戶是否能進(jìn)行操作;可以把 Realm 看成 DataSource,即安全數(shù)據(jù)源。
近看 Shiro 看細(xì)節(jié)看圖瞬間懵逼?別慌,會(huì)為你拆解來看,結(jié)合著圖看下面的解釋,這不是啥大問題,且看:
Subject主體,可以看到主體可以是任何可以與應(yīng)用交互的 “用戶”
SecurityManager相當(dāng)于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心臟;所有具體的交互都通過 SecurityManager 進(jìn)行控制;它管理著所有 Subject、且負(fù)責(zé)進(jìn)行認(rèn)證和授權(quán)、及會(huì)話、緩存的管理
Authenticator認(rèn)證器,負(fù)責(zé)主體認(rèn)證的,這是一個(gè)擴(kuò)展點(diǎn),如果用戶覺得 Shiro 默認(rèn)的不好,可以自定義實(shí)現(xiàn);需要自定義認(rèn)證策略(Authentication Strategy),即什么情況下算用戶認(rèn)證通過了
Authrizer授權(quán)器,或者訪問控制器,用來決定主體是否有權(quán)限進(jìn)行相應(yīng)的操作;即控制著用戶能訪問應(yīng)用中的哪些功能
Realm可以有 1 個(gè)或多個(gè) Realm,可以認(rèn)為是安全實(shí)體數(shù)據(jù)源,即用于獲取安全實(shí)體的;可以是JDBC實(shí)現(xiàn),也可以是LDAP實(shí)現(xiàn),或者內(nèi)存實(shí)現(xiàn)等等;由用戶提供;注意:Shiro 不知道你的用戶/權(quán)限存儲(chǔ)在哪及以何種格式存儲(chǔ);所以我們一般在應(yīng)用中都需要實(shí)現(xiàn)自己的Realm
SessionManager如果寫過 Servlet 就應(yīng)該知道 Session 的概念,Session 需要有人去管理它的生命周期,這個(gè)組件就是 SessionManager;而Shiro 并不僅僅可以用在 Web 環(huán)境,也可以用在如普通的 JavaSE 環(huán)境、EJB等環(huán)境;所以,Shiro 就抽象了一個(gè)自己的Session 來管理主體與應(yīng)用之間交互的數(shù)據(jù);這樣的話,比如我們?cè)?Web 環(huán)境用,剛開始是一臺(tái)Web服務(wù)器;接著又上了臺(tái)EJB 服務(wù)器;這時(shí)又想把兩臺(tái)服務(wù)器的會(huì)話數(shù)據(jù)放到一個(gè)地方,我們就可以實(shí)現(xiàn)自己的分布式會(huì)話(如把數(shù)據(jù)放到Memcached 服務(wù)器)
SessionDAODAO大家都用過,數(shù)據(jù)訪問對(duì)象,用于會(huì)話的 CRUD,比如我們想把 Session 保存到數(shù)據(jù)庫,那么可以實(shí)現(xiàn)自己的SessionDAO,通過如JDBC寫到數(shù)據(jù)庫;比如想把 Session 放到 Memcached 中,可以實(shí)現(xiàn)自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 進(jìn)行緩存,以提高性能;
CacheManager緩存控制器,來管理如用戶、角色、權(quán)限等的緩存的;因?yàn)檫@些數(shù)據(jù)基本上很少去改變,放到緩存中后可以提高訪問的性能
Cryptography密碼模塊,Shiro提高了一些常見的加密組件用于如密碼「加密/解密」的
注意上圖的結(jié)構(gòu),我們會(huì)根據(jù)這張圖來逐步拆分講解,記住這張圖也更有助于我們理解 Shiro 的工作原理,所以依舊是打開兩個(gè)網(wǎng)頁一起看就好嘍
搭建概覽多數(shù)小伙伴都在使用 Spring Boot, Shiro 也很應(yīng)景的定義了 starter,做了更好的封裝,對(duì)于我們來說使用起來也就更加方便,來看選型概覽
序號(hào) | 名稱 | 版本 |
---|---|---|
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 依賴,會(huì)自動(dòng)解決依賴包版本,所以自己嘗試的時(shí)候用最新版本不會(huì)有什么問題,比如 Shiro 現(xiàn)在的版本是 1.5.0 了,整體問題不大,大家自行嘗試就好
添加 Gradle 依賴管理 大體目錄結(jié)構(gòu) application.yml 配置 基本配置你就讓我看這?這只是一個(gè)概覽,先做到心中有數(shù),我們來看具體配置,逐步完成搭建
其中 shiroFilter bean 部分指定了攔截路徑和相應(yīng)的過濾器,”/user/login”, ”/user”, ”/user/loginout” 可以匿名訪問,其他路徑都需要授權(quán)訪問,shiro 提供和多個(gè)默認(rèn)的過濾器,我們可以用這些過濾器來配置控制指定url的權(quán)限(先了解個(gè)大概即可):
配置縮寫 | 對(duì)應(yīng)的過濾器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名訪問 |
authc | FormAuthenticationFilter | 指定url需要form表單登錄,默認(rèn)會(huì)從請(qǐng)求中獲取username、password,rememberMe等參數(shù)并嘗試登錄,如果登錄不了就會(huì)跳轉(zhuǎn)到loginUrl配置的路徑。我們也可以用這個(gè)過濾器做默認(rèn)的登錄邏輯,但是一般都是我們自己在控制器寫登錄邏輯的,自己寫的話出錯(cuò)返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登錄 |
Logout | LogoutFilter | 登出過濾器,配置指定url就可以實(shí)現(xiàn)退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止創(chuàng)建會(huì)話 |
perms | PermissionsAuthorizationFilter | 需要指定權(quán)限才能訪問 |
port | PortFilter | 需要指定端口才能訪問 |
rest | HttpMethodPermissionFilter | 將http請(qǐng)求方法轉(zhuǎn)化成相應(yīng)的動(dòng)詞來構(gòu)造一個(gè)權(quán)限字符串,這個(gè)感覺意義不大,有興趣自己看源碼的注釋 |
roles | RolesAuthorizationFilter | 需要指定角色才能訪問 |
ssl | SslFilter | 需要https請(qǐng)求才能訪問 |
user | UserFilter | 需要已登錄或“記住我”的用戶才能訪問 |
數(shù)據(jù)庫表設(shè)計(jì)請(qǐng)參考 entity package下的 bean,通過@Entity 注解與 JPA 的設(shè)置自動(dòng)生成表結(jié)構(gòu) (你需要簡單的了解一下 JPA 的功能)。
我們要說重點(diǎn)啦~~~
身份認(rèn)證身份認(rèn)證是一個(gè)證明 “李雷是李雷,韓梅梅是韓梅梅” 的過程,回看上圖,Realm 模塊就是用來做這件事的,Shiro 提供了 IniRealm,JdbcReaml,LDAPReam等認(rèn)證方式,但自定義的 Realm 通常是最適合我們業(yè)務(wù)需要的,認(rèn)證通常是校驗(yàn)登錄用戶是否合法。
新建用戶 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; }
這些代碼我需要做一個(gè)說明,你可能也滿肚子疑惑:
這段代碼怎么應(yīng)用了 shiro?
controller 是怎么調(diào)用到 custom realm 的?
重寫的 doGetAuthenticationInfo(…) 方法目的是什么?
認(rèn)證流程說明用戶訪問 /user/login 路徑,生成 UsernamePasswordToken, 通過SecurityUtils.getSubject()獲取Subject(currentUser),調(diào)用 login 方法進(jìn)行驗(yàn)證,讓我們跟蹤一下代碼,瞧一瞧就知道自定義的CustomRealm怎樣起作用的,一起來看源碼:
到這里我們要停一停了,請(qǐng)回看 Shiro 近景圖,將源碼追蹤路徑與其對(duì)比,是完全一致的
授權(quán)身份認(rèn)證是驗(yàn)證你是誰的問題,而授權(quán)是你能干什么的問題,
產(chǎn)品經(jīng)理:申購模塊只能科室看
程序員:好的
產(chǎn)品經(jīng)理:科長權(quán)限大一些,他也能看申購模塊
程序員:好的(黑臉)
產(chǎn)品經(jīng)理:科長不但能看,還能修改數(shù)據(jù)
程序員:關(guān)公提大刀,拿命來
…
作為程序員,我們的宗旨是:「能動(dòng)手就不吵吵」; 硝煙怒火拔地起,耳邊響起駝鈴聲(Shiro):「放下屠刀,立地成佛」授權(quán)沒有那么麻煩,大家好商量…
整個(gè)過程和身份認(rèn)證基本是一毛一樣,你對(duì)比看看
角色實(shí)體創(chuàng)建涉及到授權(quán),自然要和角色相關(guān),所以我們創(chuàng)建 Role 實(shí)體:
@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定義權(quán)限實(shí)體 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建立用戶與角色關(guān)系{ @Query(value = "select permId from RolePermRel pr where pr.roleId in ?1") List findRolePerm(List roleIds); List findByIdIn(List ids); }
其實(shí)可以通過 JPA 注解來制定關(guān)系的,這里為了說明問題,以多帶帶外鍵形式說明
@Data @Entity public class UserRoleRel { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private Long userId; private Long roleId; }建立角色與權(quán)限關(guān)系
@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") 注解說明具有用戶:列表:查看權(quán)限的才可以訪問),官網(wǎng)明確給出權(quán)限定義格式,包括通配符等,我希望你自行去查看
自定義 CustomRealm (主要重寫 doGetAuthorizationInfo) 方法:
與認(rèn)證流程如出一轍,只不過多了用戶,角色,權(quán)限的關(guān)系罷了
授權(quán)流程說明這里通過過濾器(見Shiro配置)和注解二者結(jié)合的方式來進(jìn)行授權(quán),和認(rèn)證流程一樣,最終會(huì)走到我們自定義的 CustomRealm 中,同樣 Shiro 默認(rèn)提供了許多注解用來處理不同的授權(quán)情況
注解 | 功能 |
---|---|
@RequiresGuest | 只有游客可以訪問 |
@RequiresAuthentication | 需要登錄才能訪問 |
@RequiresUser | 已登錄的用戶或“記住我”的用戶能訪問 |
@RequiresRoles | 已登錄的用戶需具有指定的角色才能訪問 |
@RequiresPermissions | 已登錄的用戶需具有指定的權(quán)限才能訪問(如果不想和產(chǎn)品經(jīng)理華山論劍,推薦用這個(gè)注解) |
授權(quán)官網(wǎng)給出明確的授權(quán)策略與案例,請(qǐng)查看:http://shiro.apache.org/permi...
上面的例子我們通過一直在通過訪問 Mysql 獲取用戶認(rèn)證和授權(quán)信息,這中方式明顯不符合生產(chǎn)環(huán)境的需求
Session會(huì)話管理做過 Web 開發(fā)的同學(xué)都知道 Session 的概念,最常用的是 Session 過期時(shí)間,數(shù)據(jù)在 Session 的 CRUD,同樣看上圖,我們需要關(guān)注 SessionManager 和 SessionDAO 模塊,Shiro starter 已經(jīng)提供了基本的 Session配置信息,我們按需在YAML中配置就好(官網(wǎng)https://shiro.apache.org/spri... 已經(jīng)明確給出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) |
分布式服務(wù)中,我們通常需要將Session信息放入Redis中來管理,來應(yīng)對(duì)高并發(fā)的訪問需求,這時(shí)只需重寫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; } }
查看源碼,可以看到調(diào)用默認(rèn)SessionManager的retriveSession方法,我們重寫該方法,將Session放入HttpRequest中,進(jìn)一步提高session訪問效率
向ShiroConfig中添加配置其實(shí)在概覽模塊已經(jīng)給出代碼展示,這里多帶帶列出來做說明:
/** * 自定義RedisSessionDao用來管理Session在Redis中的CRUD * @return */ @Bean(name = "redisSessionDao") public RedisSessionDao redisSessionDao(){ return new RedisSessionDao(); } /** * 自定義SessionManager,應(yīng)用自定義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會(huì)用默認(rèn)Session manager securityManager.setCacheManager(redisCacheManagers()); //可不指定,Shiro會(huì)用默認(rèn)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 管理功能就這樣完成了
緩存管理應(yīng)對(duì)分布式服務(wù),對(duì)于高并發(fā)訪問數(shù)據(jù)庫權(quán)限內(nèi)容是非常低效的方式,同樣我們可以利用Redis來解決這一問題,將授權(quán)數(shù)據(jù)緩存到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數(shù)據(jù)無所謂 } @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 來獲取認(rèn)證和授權(quán)信息,而是通過 Redis 來緩存這些信息,大大提升了效率,也滿足分布式系統(tǒng)的設(shè)計(jì)需求
總結(jié)回復(fù)公眾號(hào) 「demo」獲取 demo 代碼。這里只是梳理了Springboot整合Shiro的流程,以及應(yīng)用Redis最大化利用Shiro,Shiro的使用細(xì)節(jié)還很多,官網(wǎng)說的也很明確,帶著上面的架構(gòu)圖來理解Shiro會(huì)事半功倍,感覺這里面的代碼挺多挺頭大的?那是你沒有自己動(dòng)手去嘗試,結(jié)合官網(wǎng)與 demo 相信你會(huì)對(duì) Shiro 有更好的理解,另外你可以理解 Shiro 是 mini 版本的 Spring Security,我希望以小見大,當(dāng)需要更細(xì)粒度的認(rèn)證授權(quán)時(shí),也會(huì)對(duì)理解 Spring Security 有很大幫助,點(diǎn)擊文末「閱讀原文」,效果更好
落霞與孤鶩齊飛 秋水共長天一色,產(chǎn)品經(jīng)理和程序員一片祥和…
靈魂追問都說 Redis 是單線程,但是很快,你知道為什么嗎?
你們項(xiàng)目中是怎樣控制認(rèn)證授權(quán)的呢?當(dāng)授權(quán)有變化,對(duì)于程序員來說,這個(gè)修改是災(zāi)難嗎?
提高效率工具 MarkDown 表格生成器本文的好多表格是從官網(wǎng)粘貼的,如何將其直接轉(zhuǎn)換成 MD table 呢?那么 https://www.tablesgenerator.c... 就可以幫到你了,無論是生成 MD table,還是粘貼內(nèi)容生成 table 和內(nèi)容都是極好的,當(dāng)然了不止 MD table,自己發(fā)現(xiàn)吧,更多工具,公眾號(hào)回復(fù) 「工具」獲得
推薦閱讀只會(huì)用 git pull ?有時(shí)候你可以嘗試更優(yōu)雅的處理方式
雙親委派模型:大廠高頻面試題,輕松搞定
面試還不知道BeanFactory和ApplicationContext的區(qū)別?
如何設(shè)計(jì)好的RESTful API
程序猿為什么要看源碼?
歡迎持續(xù)關(guān)注公眾號(hào):「日拱一兵」前沿 Java 技術(shù)干貨分享
高效工具匯總 回復(fù)「工具」
面試問題分析與解答
技術(shù)資料領(lǐng)取 回復(fù)「資料」
以讀偵探小說思維輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識(shí),本著將復(fù)雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術(shù)問題,技術(shù)持續(xù)更新,請(qǐng)持續(xù)關(guān)注......
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/75942.html
摘要:專有的內(nèi)容更少,而更多符合標(biāo)準(zhǔn)的成分。當(dāng)前標(biāo)簽實(shí)例的方法被調(diào)用時(shí)當(dāng)前標(biāo)簽的任何一個(gè)祖先的被調(diào)用時(shí)更新從父親到兒子單向傳播。相對(duì)來說,微型場景會(huì)更適合,不想要太多的外部依賴,又需要組件化數(shù)據(jù)驅(qū)動(dòng)等更現(xiàn)代化框架的能力。 Riot.js是什么? Riot 擁有創(chuàng)建現(xiàn)代客戶端應(yīng)用的所有必需的成分: 響應(yīng)式 視圖層用來創(chuàng)建用戶界面 用來在各獨(dú)立模塊之間進(jìn)行通信的事件庫 用來管理URL和瀏覽器回...
摘要:專有的內(nèi)容更少,而更多符合標(biāo)準(zhǔn)的成分。當(dāng)前標(biāo)簽實(shí)例的方法被調(diào)用時(shí)當(dāng)前標(biāo)簽的任何一個(gè)祖先的被調(diào)用時(shí)更新從父親到兒子單向傳播。相對(duì)來說,微型場景會(huì)更適合,不想要太多的外部依賴,又需要組件化數(shù)據(jù)驅(qū)動(dòng)等更現(xiàn)代化框架的能力。 Riot.js是什么? Riot 擁有創(chuàng)建現(xiàn)代客戶端應(yīng)用的所有必需的成分: 響應(yīng)式 視圖層用來創(chuàng)建用戶界面 用來在各獨(dú)立模塊之間進(jìn)行通信的事件庫 用來管理URL和瀏覽器回...
摘要:,大家好,好久不賤呢最近因?yàn)榭戳艘恍┑男≌f,整個(gè)人都比較致郁就在昨天,我用了一天的時(shí)間寫了,又一個(gè)小而美的前端框架可能你覺得,有了和,沒必要再寫一個(gè)了我覺得我還是想想辦法尋找一下它的存在感吧先看的組件化方案最先看到的應(yīng)該是。 halo,大家好,好久不賤呢! 最近因?yàn)榭戳艘恍?be 的小說,整個(gè)人都比較致郁::>__+ {state.count--}}>- ...
摘要:,大家好,好久不賤呢最近因?yàn)榭戳艘恍┑男≌f,整個(gè)人都比較致郁就在昨天,我用了一天的時(shí)間寫了,又一個(gè)小而美的前端框架可能你覺得,有了和,沒必要再寫一個(gè)了我覺得我還是想想辦法尋找一下它的存在感吧先看的組件化方案最先看到的應(yīng)該是。 halo,大家好,好久不賤呢! 最近因?yàn)榭戳艘恍?be 的小說,整個(gè)人都比較致郁::>__+ {state.count--}}>- ...
閱讀 3209·2021-11-23 09:51
閱讀 3669·2021-09-22 15:35
閱讀 3646·2021-09-22 10:02
閱讀 2956·2021-08-30 09:49
閱讀 509·2021-08-05 10:01
閱讀 3376·2019-08-30 15:54
閱讀 1633·2019-08-30 15:53
閱讀 3558·2019-08-29 16:27