摘要:前言我們知道的核心實(shí)現(xiàn)原理都是從開始的,通過構(gòu)造層層來實(shí)現(xiàn)登錄跳轉(zhuǎn)權(quán)限驗(yàn)證,角色管理等功能。本章通過剖析的核心源碼來說明的是如何開始構(gòu)造并運(yùn)行的。
前言
我們知道Spring Security的核心實(shí)現(xiàn)原理都是從filter開始的,Spring Security通過構(gòu)造層層filter來實(shí)現(xiàn)登錄跳轉(zhuǎn)、權(quán)限驗(yàn)證,角色管理等功能。本章通過剖析Spring Security的核心源碼來說明Spring Security的filter是如何開始構(gòu)造并運(yùn)行的。
從最初開始往往我們定義一個(gè)Spring Security程序都是通過配置一個(gè)WebSecurityConfig類開始的,簡單代碼如下:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(final HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and().formLogin(); } }
通過以上代碼一個(gè)簡單的Spring Security應(yīng)用程序就能成功執(zhí)行了,該程序能攔截除了/login路徑以外的所有請(qǐng)求到登錄頁面。
我們可以看到以上代碼并沒有任何顯示聲明filter的語句,那么Spring Security是如何通過上述代碼生成filter的呢?下面就由我來一層層解剖Spring Security的源碼來說明。
我們注意到如上代碼有個(gè)@EnableWebSecurity注解,進(jìn)入該注解查看
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented #注意這里! @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug() default false; }WebSecurityConfiguration類
我們可以看到如上該注解導(dǎo)入了WebSecurityConfiguration類,進(jìn)入該類查看:
@Configuration public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { private WebSecurity webSecurity; private Boolean debugEnabled; private List> webSecurityConfigurers; private ClassLoader beanClassLoader; @Autowired( required = false ) private ObjectPostProcessor
WebSecurityConfiguration類是作為一個(gè)Spring配置源,同時(shí)定義了許多bean,這里重點(diǎn)看如下這個(gè)方法:
@Autowired( required = false ) public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor
總結(jié)下該方法所做的主要操作:
首先初始化了webSecurity屬性,該屬性對(duì)應(yīng)WebSecurity類
注入了webSecurityConfigurers屬性,該屬性是一個(gè)List
遍歷webSecurityConfigurers集合,調(diào)用webSecurity的apply方法,該方法參數(shù)為SecurityConfigurer接口
這里有一個(gè)重要的接口SecurityConfigurer接口,該接口代碼如下:
public interface SecurityConfigurer> { void init(B var1) throws Exception; void configure(B var1) throws Exception; }
回顧上面我們編寫的WebSecurityConfig配置類,也有一個(gè)configure方法,那么我們猜測(cè)WebSecurityConfig類是不是也實(shí)現(xiàn)了SecurityConfigurer接口呢?答案是是的,我們可以看WebSecurityConfig類的類圖
可以看到WebSecurityConfig類實(shí)現(xiàn)了SecurityConfigurer接口。
因此webSecurityConfigurers屬性通過依賴注入包含了WebSecurityConfig類,通過上述第3條操作將我們配置的WebSecurityConfig類和WebSecurity類關(guān)聯(lián)起來。
到這里我們知道了WebSecurityConfiguration類調(diào)用上述方法將我們配置的WebSecurityConfig類用WebSecurity類的apply方法關(guān)聯(lián)起來,那么我們?cè)敿?xì)看看WebSecurity類的apply方法:
public> C apply(C configurer) throws Exception { configurer.addObjectPostProcessor(this.objectPostProcessor); configurer.setBuilder(this); // 繼續(xù)調(diào)用該類的add方法 this.add(configurer); return configurer; } private > void add(C configurer) throws Exception { Assert.notNull(configurer, "configurer cannot be null"); // 獲取class屬性 Class extends SecurityConfigurer > clazz = configurer.getClass(); // 獲取LinkedHashMap LinkedHashMap var3 = this.configurers; synchronized(this.configurers) { if (this.buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } else { List > configs = this.allowConfigurersOfSameType ? (List)this.configurers.get(clazz) : null; if (configs == null) { configs = new ArrayList(1); } ((List)configs).add(configurer); // 將configurer放入一個(gè)LinkedHashMap中 this.configurers.put(clazz, configs); if (this.buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } } }
從上述代碼可知,實(shí)際上就是將WebSecurityConfig類放入了WebSecurity類的一個(gè)LinkedHashMap中,該LinkedHashMap在WebSecurity中屬性名為configurers。
我們繼續(xù)回到WebSecurityConfiguration類,查看它的另外一個(gè)重要的方法:
@Bean( name = {"springSecurityFilterChain"} ) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() { }); this.webSecurity.apply(adapter); } return (Filter)this.webSecurity.build(); }
該方法即為Spring Security構(gòu)建Filter的核心方法,通過webSecurity的build方法構(gòu)建了Spring Security的Filter。
我們繼續(xù)查看WebSecurity類的build方法:
public final O build() throws Exception { if (this.building.compareAndSet(false, true)) { this.object = this.doBuild(); return this.object; } else { throw new AlreadyBuiltException("This object has already been built"); } }
實(shí)際上調(diào)用了上層的doBuild:
protected final O doBuild() throws Exception { LinkedHashMap var1 = this.configurers; synchronized(this.configurers) { this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING; this.beforeInit(); this.init(); this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING; this.beforeConfigure(); this.configure(); this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING; O result = this.performBuild(); this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT; return result; } }
這里主要看WebSecurity的init方法和performBuild方法,首先看init方法
private void init() throws Exception { // this.getConfigurers()該方法實(shí)際上獲取WebSecurity中LinkedHashMap中的Value值集合 Collection> configurers = this.getConfigurers(); Iterator var2 = configurers.iterator(); SecurityConfigurer configurer; while(var2.hasNext()) { configurer = (SecurityConfigurer)var2.next(); // 調(diào)用SecurityConfigurer的init方法 configurer.init(this); } var2 = this.configurersAddedInInitializing.iterator(); while(var2.hasNext()) { configurer = (SecurityConfigurer)var2.next(); configurer.init(this); } }
通過該代碼可知,該方法首先獲取WebSecurity中的LinkedHashMap中的Value值集合,再對(duì)Value值進(jìn)行遍歷并執(zhí)行其中的init方法,從上面的代碼分析我們知道WebSecurity中的LinkedHashMap實(shí)際存的就是WebSecurityConfig,這段代碼將會(huì)調(diào)用WebSecurityConfig的init方法,而WebSecurityConfig的init方法來自于它的父類WebSecurityConfigurerAdapter,該init方法代碼如下:
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer{ public void init(final WebSecurity web) throws Exception { // 獲取HttpSecurity final HttpSecurity http = this.getHttp(); // 將HttpSecurity放入WebSecurity中 web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = (FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); } protected final HttpSecurity getHttp() throws Exception { if (this.http != null) { return this.http; } else { DefaultAuthenticationEventPublisher eventPublisher = (DefaultAuthenticationEventPublisher)this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher()); this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = this.authenticationManager(); this.authenticationBuilder.parentAuthenticationManager(authenticationManager); this.authenticationBuilder.authenticationEventPublisher(eventPublisher); Map , Object> sharedObjects = this.createSharedObjects(); this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects); if (!this.disableDefaults) { ((HttpSecurity)((DefaultLoginPageConfigurer)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)this.http.csrf().and()).addFilter(new WebAsyncManagerIntegrationFilter()).exceptionHandling().and()).headers().and()).sessionManagement().and()).securityContext().and()).requestCache().and()).anonymous().and()).servletApi().and()).apply(new DefaultLoginPageConfigurer())).and()).logout(); ClassLoader classLoader = this.context.getClassLoader(); List defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); Iterator var6 = defaultHttpConfigurers.iterator(); while(var6.hasNext()) { AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next(); this.http.apply(configurer); } } // 調(diào)用本類的configure方法 this.configure(this.http); return this.http; } } // 模板方法設(shè)計(jì)模式,子類WebSecurityConfig將會(huì)覆蓋該方法 protected void configure(HttpSecurity http) throws Exception { this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic(); } }
以上代碼最終還是實(shí)際調(diào)用了我們寫的WebSecurityConfig類的configure方法。
仔細(xì)觀察以上代碼,我們發(fā)現(xiàn)有一條語句web.addSecurityFilterChainBuilder(http),該語句將構(gòu)建的HttpSecurity放入WebSecurity類中,以下是該方法源碼:
public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder extends SecurityFilterChain> securityFilterChainBuilder) { this.securityFilterChainBuilders.add(securityFilterChainBuilder); return this; }
實(shí)際上就是將HttpSecurity放入了WebSecurity的一個(gè)list集合里,該list集合屬性名為securityFilterChainBuilders。
到目前為止,我們終于知道我們編寫的WebSecurityConfig類的configure方法是如何被調(diào)用的了,但是仍有許多疑問沒解開,比如HttpSecurity類的作用,Spring Security是如何通過HttpSecurity類構(gòu)建一條攔截器鏈等。
這里我們先不分析HttpSecurity類的具體實(shí)現(xiàn),再來看看WebSecurity的init方法執(zhí)行后所執(zhí)行的performBuild方法,該方法源碼如下:
protected Filter performBuild() throws Exception { Assert.state(!this.securityFilterChainBuilders.isEmpty(), () -> { return "At least one SecurityBuilder extends SecurityFilterChain> needs to be specified. Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"; }); int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size(); ListsecurityFilterChains = new ArrayList(chainSize); Iterator var3 = this.ignoredRequests.iterator(); while(var3.hasNext()) { RequestMatcher ignoredRequest = (RequestMatcher)var3.next(); securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest, new Filter[0])); } / 遍歷securityFilterChainBuilders集合 var3 = this.securityFilterChainBuilders.iterator(); while(var3.hasNext()) { SecurityBuilder extends SecurityFilterChain> securityFilterChainBuilder = (SecurityBuilder)var3.next(); // 執(zhí)行securityFilterChainBuilders集合單位的build方法,返回一個(gè)SecurityFilterChain類,并加入List 中 securityFilterChains.add(securityFilterChainBuilder.build()); } // 將List 類構(gòu)建成一個(gè)FilterChainProxy代理類 FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (this.httpFirewall != null) { filterChainProxy.setFirewall(this.httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; if (this.debugEnabled) { this.logger.warn(" ******************************************************************** ********** Security debugging is enabled. ************* ********** This may include sensitive information. ************* ********** Do not use in a production system! ************* ******************************************************************** "); result = new DebugFilter(filterChainProxy); } this.postBuildAction.run(); // 返回FilterChainProxy代理類 return (Filter)result; }
該方法執(zhí)行的操作主要如下:
遍歷securityFilterChainBuilders集合,并執(zhí)行其中的build方法,從上面代碼分析可知,securityFilterChainBuilders集合里存儲(chǔ)了HttpSecurity,所以這里執(zhí)行了HttpSecurity的build方法構(gòu)建SecurityFilterChain類
將List
返回這個(gè)FilterChainProxy代理類
到這里總的過程就非常明了了,實(shí)際上Spring Security的頂層filter就是一個(gè)FilterChainProxy類,而HttpSecurity主要用于注冊(cè)和實(shí)例化各種filter
到這里就分成了兩路,一路是HttpSecurity的build方法構(gòu)建SecurityFilterChain類的原理,一路是FilterChainProxy類的作用,我們先從FilterChainProxy類開始
FilterChainProxy類當(dāng)請(qǐng)求到達(dá)的時(shí)候,F(xiàn)ilterChainProxy會(huì)調(diào)用dofilter()方法,會(huì)遍歷所有的SecurityFilterChain,對(duì)匹配到的url,則一一調(diào)用SecurityFilterChain中的filter做認(rèn)證授權(quán)。FilterChainProxy的dofilter()中調(diào)用了doFilterInternal()方法,如下:
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); // 獲取請(qǐng)求對(duì)應(yīng)的filter列表 Listfilters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); // 執(zhí)行每個(gè)filter vfc.doFilter(fwRequest, fwResponse); } // 通過遍歷filterChains,調(diào)用SecurityFilterChain的matches方法,判斷當(dāng)前的請(qǐng)求對(duì)應(yīng)哪些filter,返回匹配的filter列表 private List getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; }
我們理清了FilterChainProxy類的作用,那么這些SecurityFilterChain是從哪來的呢?從上節(jié)可知SecurityFilterChain是由HttpSecurity的build方法生成的,下面我們分析下HttpSecurity類
HttpSecurityHttpSecurity與WebSecurity一樣,都繼承了AbstractConfiguredSecurityBuilder類,而WebSecurity的build和doBuild方法和LinkedHashMap屬性,均來自AbstractConfiguredSecurityBuilder,故HttpSecurity的build方法代碼與WebSecurity的相同,區(qū)別在于LinkedHashMap存儲(chǔ)的東西不同,HttpSecurity正是通過如此來生成SecurityFilterChain類的。
下面我們來看HttpSecurity構(gòu)建filter的幾個(gè)常見方法:
public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { ApplicationContext context = this.getContext(); return ((ExpressionUrlAuthorizationConfigurer)this.getOrApply(new ExpressionUrlAuthorizationConfigurer(context))).getRegistry(); } public FormLoginConfigurer formLogin() throws Exception { return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer()); }
都調(diào)用了getOrApply方法,再來看getOrApply方法,又調(diào)用了其中的apply方法
private> C getOrApply(C configurer) throws Exception { C existingConfig = (SecurityConfigurerAdapter)this.getConfigurer(configurer.getClass()); return existingConfig != null ? existingConfig : this.apply(configurer); } public > C apply(C configurer) throws Exception { configurer.addObjectPostProcessor(this.objectPostProcessor); configurer.setBuilder(this); this.add(configurer); return configurer; }
apply方法又調(diào)用了add方法,這里的add方法最終還是將該configurer加入了linkedHashMap中
private> void add(C configurer) throws Exception { Assert.notNull(configurer, "configurer cannot be null"); Class extends SecurityConfigurer > clazz = configurer.getClass(); LinkedHashMap var3 = this.configurers; synchronized(this.configurers) { if (this.buildState.isConfigured()) { throw new IllegalStateException("Cannot apply " + configurer + " to already built object"); } else { List > configs = this.allowConfigurersOfSameType ? (List)this.configurers.get(clazz) : null; if (configs == null) { configs = new ArrayList(1); } ((List)configs).add(configurer); this.configurers.put(clazz, configs); if (this.buildState.isInitializing()) { this.configurersAddedInInitializing.add(configurer); } } } }
故HttpSecurity在構(gòu)建filter的過程中,本質(zhì)還是將形如ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer等類加入了它的LinkedHashMap中。
那么將這些Configurer類存入LinkedHashMap的作用又是什么?我們回憶上面WebSecurity類的doBuild方法,我們知道HttpSecurity類調(diào)用的doBuild方法與WebSecurity類一樣,而通過觀察WebSecurity類doBuild方法里this.init();this.configure();這些語句的具體實(shí)現(xiàn),實(shí)際就是調(diào)用其LinkedHashMap中的元素的init方法和configure方法。
我們現(xiàn)在來查看其中一個(gè)ExpressionUrlAuthorizationConfigurer類的configure方法的詳細(xì)代碼:
public void configure(H http) throws Exception { FilterInvocationSecurityMetadataSource metadataSource = this.createMetadataSource(http); if (metadataSource != null) { FilterSecurityInterceptor securityInterceptor = this.createFilterSecurityInterceptor(http, metadataSource, (AuthenticationManager)http.getSharedObject(AuthenticationManager.class)); if (this.filterSecurityInterceptorOncePerRequest != null) { securityInterceptor.setObserveOncePerRequest(this.filterSecurityInterceptorOncePerRequest); } securityInterceptor = (FilterSecurityInterceptor)this.postProcess(securityInterceptor); // 將Filter加入了HttpSecurity的Filters集合中 http.addFilter(securityInterceptor); http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor); } }
最后來看看HttpSecruity的performBuild()方法:
protected DefaultSecurityFilterChain performBuild() throws Exception { Collections.sort(this.filters, this.comparator); return new DefaultSecurityFilterChain(this.requestMatcher, this.filters); }
實(shí)際上就是通過Filters集合構(gòu)建了SecurityFilterChain。
從上面代碼可總結(jié)出,HttpSecurity內(nèi)部維護(hù)一個(gè)Filter列表,而HttpSecurity調(diào)用形如authorizeRequests(),formLogin()等方法實(shí)際上就是將filter添加入它的列表當(dāng)中,最后通過performBuild()方法構(gòu)建出SecurityFilterChain,至此HttpSecurity構(gòu)建filter的總過程就完成了。
總結(jié)到目前為止,我們終于知道Spring Security是如何一步步的構(gòu)建和初始化filter的了,我們最后再來簡單總結(jié)下構(gòu)建過程:
Spring Security啟動(dòng)過程中通過WebSecurityConfiguration實(shí)例化WebSecurity
WebSecurityConfiguration會(huì)將使用者編寫的WebSecurityConfig類放入WebSecurity中的LinkedHashMap中
在構(gòu)建WebSecurity的時(shí)候,會(huì)調(diào)用WebSecurity的doBuild()方法,這個(gè)方法是一個(gè)核心方法。
doBuild中的init方法將會(huì)調(diào)用LinkedHashMap中元素的init方法(這里的元素是WebSecurityConfig),然后WebSecurityConfig的init方法會(huì)調(diào)用configure方法,調(diào)用configure方法后,將會(huì)初始化HttpSecurity構(gòu)建各種Filter,這時(shí)HttpSecurity將會(huì)加入WebSecurity中。
doBuild中的init方法調(diào)用完后將會(huì)調(diào)用下一個(gè)performBuild()方法,該方法會(huì)獲取到HttpSecurity調(diào)用其doBuild方法構(gòu)造SecurityFilterChain
將獲取到的SecurityFilterChain構(gòu)建成一個(gè)FilterChainProxy類,作為Spring Security的頂層filter
至此Spring Security的Filter構(gòu)建完成
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/77713.html
摘要:構(gòu)建完實(shí)例后,將它設(shè)置為的父認(rèn)證管理器并將該傳入構(gòu)造器構(gòu)建實(shí)例。,目前為止已經(jīng)被初始化,接下去需要設(shè)置對(duì)象添加至的列表中打開類結(jié)構(gòu),和一樣,它也實(shí)現(xiàn)了接口,同樣繼承自。最后返回的是的默認(rèn)實(shí)現(xiàn)。 最近在整合微服務(wù)OAuth 2認(rèn)證過程中,它是基于Spring Security之上,而本人對(duì)Spring Security架構(gòu)原理并不太熟悉,導(dǎo)致很多配置搞不太清楚,遂咬牙啃完了Spring ...
摘要:返回總共需要處理個(gè)地方,一個(gè)是異常的處理,需要兼容請(qǐng)求,一個(gè)是成功返回的處理,一個(gè)是失敗返回的處理。這里就是攔截,獲取提交的參數(shù),然后交給去認(rèn)證。之后就是走后續(xù)的,如果成功,則會(huì)進(jìn)行相應(yīng)的配置。動(dòng)態(tài)配置權(quán)限筆記自定義 序 本文講述一下如何自定義spring security的登錄頁,網(wǎng)上給的資料大多過時(shí),而且是基于后端模板技術(shù)的,講的不是太清晰,本文給出一個(gè)采用ajax的登錄及返回的前...
摘要:的版本增加了對(duì)事件監(jiān)聽程序的支持,事件監(jiān)聽程序在建立修改和刪除會(huì)話或環(huán)境時(shí)得到通知。元素指出事件監(jiān)聽程序類。過濾器配置將一個(gè)名字與一個(gè)實(shí)現(xiàn)接口的類相關(guān)聯(lián)。 1.簡介 web.xml文件是Java web項(xiàng)目中的一個(gè)配置文件,主要用于配置歡迎頁、Filter、Listener、Servlet等,但并不是必須的,一個(gè)java web項(xiàng)目沒有web.xml文件照樣能跑起來。Tomcat容器/...
閱讀 3159·2021-11-22 09:34
閱讀 657·2021-11-22 09:34
閱讀 2516·2021-10-08 10:18
閱讀 3446·2021-09-22 15:57
閱讀 2698·2021-09-22 15:25
閱讀 2504·2019-08-30 15:54
閱讀 2256·2019-08-30 15:44
閱讀 1854·2019-08-29 11:18