shiro與spring的整合
上一期,我們分享了如何在項目中使用shiro,了解了shiro的基本用法,但畢竟學習shiro的目的就是在項目中應用shiro,更准確地說是在web項目中應用shiro。那么,今天我們就來探討一下shiro在spring web項目中的應用,這里依然參考官方sample部分的代碼。好了,廢話少說,直接開戰。
spring xml方式
首先當然是創建spring項目,這里提供兩種方案,一種是通過xml配置的spring項目,一種是純注解的spring項目。先來說xml配置的方式,為什么要說xml的方式,因為在實際項目應用中,很多公司目前運行的方式還是xml配置的方式,為了我們更好的上手,更好地工作,我們先將xml的方式,當然也是因為目前我們公司采用的就是xml配置的方式。好了,讓我們還是吧!
一、創建spring項目(xml方式)
關於spring項目的創建,這里不做過多說明,但我會放上自己的項目結構和各類配置。
pom.xml文件
先創建web項目
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.syske</groupId>
<artifactId>shiro</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>shiro Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>shiro</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
引入spring依賴
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
添加spring配置:webapp/WEB-INF/spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="io.github.syske.shiro"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:default-servlet-handler/>
</beans>
在web.xml中配置spring容器
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:springApplicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
至此,spring項目創建完成,然后啟動下你的spring項目,如果沒有問題,那就繼續往下看。
二、引入我們今天的主角:shiro和她的小伙伴
引入shiro的依賴包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.6</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.24</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.24</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.6.11</version>
</dependency>
在web.xml中配置shiro攔截器
這個配置是必須的,沒有這個配置,你的項目和shiro半毛錢關系都沒有,更不會有什么效果。所以,當哪位小伙伴發現自己的項目沒效果的時候,檢查下這個配置是否添加了,是否配置正確。
<!-- 配置shiro的shiroFilter -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意:需要注意的一點是,這里的filter-name必須與classpath/springApplicationContext-shiro.xml中ShiroFilterFactoryBean的bean id一致,否項目啟動的時候,會提示找不到name為shiroFilter的bean。
當然,如果你非要修改這個name,要讓他們不一樣,那你必須在filte中添加如下配置:
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
其中,param-value對應你的bean id,否則還是會報相同的錯。
添加shiro的配置:classpath/springApplicationContext-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
配置 SecurityManager!
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"/>
<!--
配置 ShiroFilter.
id 必須和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致.
若不一致, 則會拋出: NoSuchBeanDefinitionException. 因為 Shiro 會來 IOC 容器中查找和 <filter-name> 名字對應的 filter bean.
-->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/** = authc
</value>
</property>
</bean>
</beans>
以上配置是最基本的,然后你就可以啟動項目了。不出意外的話,你會發現,項目會自動跳轉到
login.jsp,當然前提條件是的jsp頁面必須存在。以上步驟只是讓大家看到,shiro本質上是個攔截器,他會根據你的配置信息,攔截相應的路徑,但shiro真正的作用並沒有體現出來,下面讓我們進一步深入了解吧。
三、創建我們的shiro Controller
這里controller的名字你可以隨便起,反正不影響。這里這個controller的作用就是處理我們的的登錄請求先上代碼:
package io.github.syske.shiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @program: shiro-spring4
* @description: shiro 認證授權
* @create: 2019-10-27 06:13
*/
@Controller
@RequestMapping("/shiro")
public class ShiroController {
private static final transient Logger log = LoggerFactory.getLogger(ShiroController.class);
@RequestMapping("/login")
public String login(@RequestParam(name = "username") String username,
@RequestParam(name = "password") String password) {
// 獲取當前用戶Subject
Subject currentUser = SecurityUtils.getSubject();
// 判斷用戶是否已經登錄
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (AuthenticationException ae) {
log.error("登錄失敗:" + ae);
}
}
return "redirect:/list.jsp";
}
}
小伙伴還記得我們分享的第一個shiro示例嗎,這里我們再來回顧下shiro的基本認證流程,然后再來解釋代碼:
// 創建SecurityManager實例工廠
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 通過工廠創建SecurityManager實例
SecurityManager securityManager = factory.getInstance();
// 將SecurityManager對象傳給SecurityUtils
SecurityUtils.setSecurityManager(securityManager);
// 從SecurityUtils中獲取Subject
Subject currentUser = SecurityUtils.getSubject();
// 創建密碼用戶名令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// 設置是否記住登錄狀態
token.setRememberMe(true);
// 用戶登錄
currentUser.login(token);
對照我們的spring配置,你會發現我們只完成了factory、securityManager以及SecurityUtils的設置,對於后面的邏輯,我們並沒有實現。這里controller就是用來實現我們后面幾個步驟的。參照第一個shiro示例應該可以看明白,這里不做過多解釋。
提示:這里要提一點的是,shiro的session非常強大,他可以讓你在非controller中拿到session,更重要的是他的session包含了HttpServletSession中所有內容,也就是說你不需要通過任何轉化或操作,可以直接在shiro的session中拿到你放在HttpServletSession,這樣你在工具類中就可以很輕易地拿到session,是不是很完美^_^!
創建完controller,修改完我們的登錄頁面,再次重啟我們的項目,然后隨便輸入用戶名和密碼,如果沒有什么意外的話,會報錯。你沒看錯,會報錯,大致錯誤提示如下:
java.lang.IllegalStateException: Configuration error: No realms have been configured! One or more realms must be present to execute an authentication attempt.
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.assertRealmsConfigured(ModularRealmAuthenticator.java:161)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:264)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
at io.github.syske.shiro.controller.ShiroController.login(ShiroController.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
錯誤提示很清楚,我們沒有配置realms。那么我們該如何設置realms,設置給誰呢?我們沿着報錯信息排查看看,當你找到ModularRealmAuthenticator這個類的代碼時,你會發現如下代碼:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
我們通過繼承關系可以看到,ModularRealmAuthenticator的父類並沒有realms屬性,所以我們應該將realms配給ModularRealmAuthenticator。但是我們依然不知道如何設置,我們來看下ModularRealmAuthenticator又是什么。同樣是根據我們的報錯信息,通過繼承關系,我們發現ModularRealmAuthenticator是Authenticator的實現類,而AuthenticatingSecurityManager有一個屬性就是Authenticator,而AuthenticatingSecurityManager底層又實現了SecurityManager接口。到這里,我們的思路就有了,設置順序應該是這樣的:
- 將reamls設置給ModularRealmAuthenticator
- 再將Authenticator(ModularRealmAuthenticator)設置給SecurityManager
四、創建realm
通過分析ModularRealmAuthenticator源碼,我們發現realms本質上是集合Realm。在設置SecurityManager的屬性的時候,我發現有個realm屬性,分析源碼發現,當我們只有一個realm時,沒必要通過容器的方式注入,但是注意authenticator的realms和securityManager的realm屬性不能同時設置,否則會報錯。我的配置如下:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm"/>
<!--
<property name="authenticator" ref="authenticator">
</property>
-->
</bean>
<bean id="shiroRealm" class="io.github.syske.shiro.realms.ShiroRealm"/>
<!--
<bean id="authenticator"
class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="shiroRealm"></ref>
</list>
</property>
</bean>
-->
本來打算自己創建realm,但又想能不能不自己創建realm,所以我就選了SimpleAccountRealm。創建自己的realm后面再講,同時也是為了讓大家清楚為什么要創建自己的realm。
再次重啟我們的項目,然后隨意輸入用戶名和密碼,你發現又報錯了,大致錯誤如下:
嚴重: Servlet.service() for servlet [spring] in context with path [] threw exception [Request processing failed; nested exception is org.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.SimpleAccountRealm@6c38d83a] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - 61000004, rememberMe=true].] with root cause
org.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.SimpleAccountRealm@6c38d83a] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - 61000004, rememberMe=true].
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:184)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
at io.github.syske.shiro.controller.ShiroController.login(ShiroController.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
錯誤提示很明顯:未知用戶名,這也就說明我們配置的realm生效了,至於用戶名未知,那是因為我們沒有配置任何用戶信息,shiro找不到我們的用戶信息,所以校驗失敗。那么用戶信息如何設置呢,在哪里設置呢,和上面一樣,我們依據錯誤信息來看源碼:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
SimpleAccount account = this.getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
上面錯誤的原因本質上是因為SimpleAccountRealm的doGetAuthenticationInfo方法中account為空導致的,而account是通過getUser獲取到的,getUser通過在users屬性中查找我們當前用戶,然后返回查找結果,由於我們並沒有給realm設置users屬性,所以自然返回結果就是空。先在你應該清楚了,下一步我們該給我們的realm設置users屬性。
但是在給SimpleAccountRealm的users注入值的時候,發現該屬性無法沒有set方法,但是發現有addAccount方法,可以通過手動方式添加用戶,但我發現這種方式比較麻煩,畢竟我們用的是spring,手動方式並不方便,至少獲取SimpleAccountRealm的bean很麻煩,所以我們直接使用官方的realm宣告失敗😂(有想法的童鞋可以自己試一下,寫個springContext的工具類,獲取到SimpleAccountRealm,然后給他加用戶信息就行了)。
下來,我們開始定義自己的realm,通過繼承官方的realm來滿足自己的特殊需求。直接上代碼:
package io.github.syske.shiro.realms;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @program: shiro-spring
* @description:
* @create: 2019-10-20 22:12
*/
public class ShiroRealm extends AuthenticatingRealm {
private static final transient Logger log = LoggerFactory.getLogger(AuthenticatingRealm.class);
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
log.info("doGetAuthenticationInfo toke" + token);
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
// 下面用到的用戶名密碼在實際應用中對應的是你數據庫查到的用戶信息,這里為了方便演示,沒有配置數據庫,關於收據庫shiro整合,后期詳細講
String username = usernamePasswordToken.getUsername();
String password = null;
if ("admin".equals(username)) {
password = "admin";
} else if ("user".equals(username)) {
password = "user";
} else {
password = "123456";
}
if ("unkonw".equals(username)) {
throw new UnknownAccountException("用戶不存在!");
}
if ("locked".equals(username)) {
throw new LockedAccountException("用戶被鎖定!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
return info;
}
}
這里我們繼承的是AuthenticatingRealm,實現他的doGetAuthenticationInfo方法。關於官方的Realm,我打算抽個時間好好了解熟悉下,到時候詳細說明。
然后將我們地realm改成我們剛剛創建的realm,然后重啟你的項目。不出意外,你就可以登陸成功了。完美,優秀😂
因為今天確實寫的有點太多了,有點像劉姥姥的裹腳布了,所以后面的內容留到后面來講,提前預告下,也算是給自己立的flag:
- shiro整合數據庫
- shiro攔截器詳細配置
- shiro各種realm的應用場景
總結
本次寫shiro部分的內容其實准備的不是很充分,本身我現在也還在學習shiro,實際項目中有應用,但不是很多,所以對於很多細節的知識點掌握的並不太好,也沒有比較深刻的理解,但就這篇博客而言,我覺得我是有收獲的,至少提升了我通過源碼來學習、來解決問題的能力,更重要的是,還很有可能讓一些剛開始學校的小伙伴能避免一些坑,是吧😏