Spring Security 入门(1-4-1)Spring Security - 认证过程
理解时可结合一下这位老兄的文章:http://www.importnew.com/20612.html
1、Spring Security的认证过程
1.1、登录过程 - 如果用户直接访问登录页面
- 用户使用用户名和密码进行登录。
- Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
- 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
- AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
- 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。
- 在认证成功后,跳转到指定的成功页面,默认是应用的根路径。
此后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。
1.2、如果用户直接访问一个受保护的资源
那么认证过程将如下:
- 引导用户进行登录,通常是重定向到一个基于 form 表单进行登录的页面,具体视配置而定。
- 用户输入用户名和密码后请求认证,后台还是会像上节描述的那样获取用户名和密码封装成一个 UsernamePasswordAuthenticationToken 对象,然后把它传递给 AuthenticationManager 进行认证。
- 如果认证失败将继续执行步骤 1,如果认证成功则会保存返回的 Authentication 到 SecurityContext,然后默认会将用户重定向到之前访问的页面。
- 用户登录认证成功后再次访问之前受保护的资源时就会对用户进行权限鉴定,如不存在对应的访问权限,则会返回 403 错误码。
在上述步骤中将有很多不同的类参与,但其中主要的参与者是 ExceptionTranslationFilter。
2、认证请求拦截、认证异常处理 和 认证信息保存
2.1、认证异常的处理
ExceptionTranslationFilter(拦截AuthenticationException、AccessDeniedException )、
ExceptionTranslationFilter 是用来处理来自 AbstractSecurityInterceptor 抛出的 AuthenticationException 和 AccessDeniedException 的。
当 ExceptionTranslationFilter 捕获到的是 AuthenticationException 时将调用 AuthenticationEntryPoint 引导用户进行登录;
如果捕获的是 AccessDeniedException,但是用户还没有通过认证,则调用 AuthenticationEntryPoint 引导用户进行登录认证,否则将返回一个表示不存在对应权限的 403 错误码。
2.2、认证请求的拦截
AbstractSecurityInterceptor (FilterSecurityInterceptor、MethodSecurityInterceptor )
AbstractSecurityInterceptor 是 Spring Security 用于拦截请求进行权限鉴定的,
其拥有两个具体的子类,拦截方法调用的 MethodSecurityInterceptor 和拦截 URL 请求的 FilterSecurityInterceptor。
2.3、用户认证信息Authentication 的产生和保存
Authentication 是一个接口,用来表示用户认证信息的,在用户登录认证之前相关信息会封装为一个 Authentication 具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。
Spring Security 使用一个 Authentication 对象来描述当前用户的相关信息。
SecurityContextHolder 中持有的是当前用户的 SecurityContext,而 SecurityContext 持有的是代表当前用户相关信息的 Authentication 的引用。
这个 Authentication 对象不需要我们自己去创建,在与系统交互的过程中,Spring Security 会自动为我们创建相应的 Authentication 对象,然后赋值给当前的 SecurityContext。
但是往往我们需要在程序中获取当前用户的相关信息,比如最常见的是获取当前登录用户的用户名。
在程序的任何地方,通过如下方式我们可以获取到当前用户的用户名。
public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return String.valueOf(principal);
}
通过 Authentication.getPrincipal() 可以获取到代表当前用户的信息,这个对象通常是 UserDetails 的实例。获取当前用户的用户名是一种比较常见的需求,关于上述代码其实 Spring Security 在 Authentication 中的实现类中已经为我们做了相关实现,所以获取当前用户的用户名最简单的方式应当如下。
public String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,
如果对应的 SecurityContext 不存在,则 Spring Security 将为我们建立一个空的 SecurityContext 并进行返回。
2.4、用户认证信息的传递控制:SecurityContext、SecurityContextHolder 和 SecurityContextPersistentFilter
SecurityContextHolder 是用来保存 SecurityContext 的。SecurityContext 中含有当前正在访问系统的用户的详细信息。默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext。因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的。这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal。
可能你早就有这么一个疑问了,既然 SecurityContext 是存放在 ThreadLocal 中的,而且在每次权限鉴定的时候都是从 ThreadLocal 中获取 SecurityContext 中对应的 Authentication 所拥有的权限,并且不同的 request 是不同的线程,为什么每次都可以从 ThreadLocal 中获取到当前用户对应的 SecurityContext 呢?
在 Web 应用中这是通过 SecurityContextPersistentFilter 实现的,默认情况下其会在每次请求开始的时候从 session 中获取 SecurityContext,然后把它设置给 SecurityContextHolder,在请求结束后又会将 SecurityContextHolder 所持有的 SecurityContext 保存在 session 中,并且清除 SecurityContextHolder 所持有的 SecurityContext。
这样当我们第一次访问系统的时候,SecurityContextHolder 所持有的 SecurityContext 肯定是空的,待我们登录成功后,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有认证成功的 Authentication 对象,待请求结束后我们就会将 SecurityContext 存在 session 中,等到下次请求的时候就可以从 session 中获取到该 SecurityContext 并把它赋予给 SecurityContextHolder 了,由于 SecurityContextHolder 已经持有认证过的 Authentication 对象了,所以下次访问的时候也就不再需要进行登录认证了。
2.5、控制SecurityContext的使用范围:SecurityContextHolderStrategy
SecurityContextHolderStrategy
ThreadLocalSecurityContextHolderStrategy(默认,web场景)
GlobalSecurityContextHolderStrategy(单机cs应用)
InheritableThreadLocalSecurityContextHolderStrategy (子线程继承父线程的线程变量)
SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法内部逻辑基本上都是通过 SecurityContextHolder 持有的 SecurityContextHolderStrategy 来实现的,如 getContext()、setContext()、clearContext()等。而默认使用的 strategy 就是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy。另外,Spring Security 还提供了两种类型的 strategy 实现,GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy,前者表示全局使用同一个 SecurityContext,如 C/S 结构的客户端;后者使用 InheritableThreadLocal 来存放 SecurityContext,即子线程可以使用父线程中存放的变量。
一般而言,我们使用默认 strategy 就可以了,但是如果要改变默认 strategy,Spring Security 提供了两种方法,这两种方式都是通过改变 strategyName 来实现的。
SecurityContextHolder 中为三种不同类型的 strategy 分别命名为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL。
- 第一种方式是通过 SecurityContextHolder 的静态方法 setStrategyName() 来指定需要使用的 strategy;
- 第二种方式是通过系统属性进行指定,其中属性名默认为 “spring.security.strategy”,属性值为对应 strategy 的名称。
2.6、认证成功后清除凭证
默认情况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码。所以如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了。所以在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存。
3、使用AuthenticationManager认证用户
3.1、认证管理器 AuthenticationManager-》ProviderManager
AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),
该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。
在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,而且它不直接自己处理认证请求,
而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,
如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。
然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果。
如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException。
3.2、认证提供者列表-认证提供者:AuthenticationProvider列表 -》默认的认证提供者DaoAuthenticationProvider:
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。
当我们使用 authentication-provider 元素来定义一个 AuthenticationProvider 时,如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 默认会使用 DaoAuthenticationProvider。
- DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,我们可以实现自己的 AuthenticationProvider;
- 如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。实现了自己的 AuthenticationProvider 之后,我们可以在配置文件中这样配置来使用我们自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我们自己的 AuthenticationProvider 实现类对应的 bean。
<security:authentication-manager>
<security:authentication-provider ref="myAuthenticationProvider"/>
</security:authentication-manager>
- 实现了自己的 UserDetailsService 之后,我们可以在配置文件中这样配置来使用我们自己的 UserDetailsService。其中的 myUserDetailsService 就是我们自己的 UserDetailsService 实现类对应的 bean。
<security:authentication-manager>
<security:authentication-provider user-service-ref="myUserDetailsService"/>
</security:authentication-manager>
3.3、用户信息的获取和保存:UserDetailsService和UserDetails-》Authentitcation-》SecurityContext
用户信息对象UserDetails
UserDetails 是 Spring Security 中一个核心的接口。其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法。
Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类。
在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的 UserDetails,
比如在登录认证的时候。登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,
认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,
然后再把该 Authentication 存入到 SecurityContext 中。
UserDetails.getAuthorities() -》 Authority -》 GrantedAuthority -》SimpleGrantedAuthority
Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限。
其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限。
GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails 的。
GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null。
Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。
该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。
从Authentication对象获取用户信息UserDetails
用户认证之后,如果需要使用用户信息可以通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal。
通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例。
校验用户请求最常用的方法-根据请求的用户名加载对应的UserDetails
校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过。
Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails,UserDetailsService 将在下节讲解。
在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。
认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中。
- 当我们在使用 NameSpace 时, authentication-manager 元素的使用会使 Spring Security 在内部创建一个 ProviderManager,然后可以通过 authentication-provider 元素往其中添加 AuthenticationProvider。当定义 authentication-provider 元素时,如果没有通过 ref 属性指定关联哪个 AuthenticationProvider,Spring Security 默认就会使用 DaoAuthenticationProvider。使用了 NameSpace 后我们就不要再声明 ProviderManager 了。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider user-service-ref="userDetailsService"/>
</security:authentication-manager>
- 如果我们没有使用 NameSpace,那么我们就应该在 ApplicationContext 中声明一个 ProviderManager。
3.4、扩展UserDetails和扩展UserDetailsService
用户信息UserDetails扩展
通常我们需要在应用中获取当前用户的其它信息,如 Email、电话等。
这时存放在 Authentication 的 principal 中只包含有认证相关信息的 UserDetails 对象可能就不能满足我们的要求了。
这时我们可以实现自己的 UserDetails,在该实现类中我们可以定义一些获取用户其它信息的方法,这样将来我们就可以直接从当前 SecurityContext 的 Authentication 的 principal 中获取这些信息了。
用户信息服务UserDetailsService扩展
上文已经提到了 UserDetails 是通过 UserDetailsService 的 loadUserByUsername() 方法进行加载的。
UserDetailsService 也是一个接口,我们也需要实现自己的 UserDetailsService 来加载我们自定义的 UserDetails 信息。
然后把它指定给 AuthenticationProvider 即可。如下是一个配置 UserDetailsService 的示例。
<!-- 用于认证的 AuthenticationManager -->
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider user-service-ref="userDetailsService" />
</security:authentication-manager>
<bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
上述代码中我们使用的 JdbcDaoImpl 是 Spring Security 为我们提供的 UserDetailsService 的实现,
另外 Spring Security 还为我们提供了 UserDetailsService 另外一个实现,InMemoryDaoImpl。
其作用是从数据库中加载 UserDetails 信息。其中已经定义好了加载相关信息的默认脚本,这些脚本也可以通过 JdbcDaoImpl 的相关属性进行指定。
3.4.1、UserDetailsService 的实现1-JdbcDaoImpl
JdbcDaoImpl 允许我们从数据库来加载 UserDetails,其底层使用的是 Spring 的 JdbcTemplate 进行操作,所以我们需要给其指定一个数据源。此外,我们需要通过 usersByUsernameQuery 属性指定通过 username 查询用户信息的 SQL 语句;通过 authoritiesByUsernameQuery 属性指定通过 username 查询用户所拥有的权限的 SQL 语句;如果我们通过设置 JdbcDaoImpl 的 enableGroups 为 true 启用了用户组权限的支持,则我们还需要通过 groupAuthoritiesByUsernameQuery 属性指定根据 username 查询用户组权限的 SQL 语句。当这些信息都没有指定时,将使用默认的 SQL 语句,默认的 SQL 语句如下所示。
select username, password, enabled from users where username=? -- 根据 username 查询用户信息
select username, authority from authorities where username=? -- 根据 username 查询用户权限信息
select g.id, g.group_name, ga.authority from groups g, groups_members gm, groups_authorities ga where gm.username=? and g.id=ga.group_id and g.id=gm.group_id -- 根据 username 查询用户组权限
使用默认的 SQL 语句进行查询时意味着我们对应的数据库中应该有对应的表和表结构,Spring Security 为我们提供的默认表的创建脚本如下。
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) notnull);
create table group_authorities (
group_id bigint notnull,
authority varchar(50) notnull,
constraint fk_group_authorities_group foreign key(group_id) references groups(id));
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) notnull,
group_id bigint notnull,
constraint fk_group_members_group foreign key(group_id) references groups(id));
此外,使用 jdbc-user-service 元素时在底层 Spring Security 默认使用的就是 JdbcDaoImpl。
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider>
<!-- 基于 Jdbc 的 UserDetailsService 实现,JdbcDaoImpl -->
<security:jdbc-user-service data-source-ref="dataSource"/>
</security:authentication-provider>
</security:authentication-manager>
3.4.2、UserDetailsService 的实现1-InMemoryDaoImpl
InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中。使用 NameSpace 时,使用 user-service 元素 Spring Security 底层使用的 UserDetailsService 就是 InMemoryDaoImpl。此时,我们可以简单的使用 user 元素来定义一个 UserDetails。
<security:user-service>
<security:user name="user" password="user" authorities="ROLE_USER"/>
</security:user-service>
如上配置表示我们定义了一个用户 user,其对应的密码为 user,拥有 ROLE_USER 的权限。此外,user-service 还支持通过 properties 文件来指定用户信息,如:
<security:user-service properties="/WEB-INF/config/users.properties"/>
其中属性文件应遵循如下格式:
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
所以,对应上面的配置文件,我们的 users.properties 文件的内容应该如下所示:
#username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
user=user,ROLE_USER
Spring Security 入门(1-4-1)Spring Security - 认证过程的更多相关文章
- Spring Security 入门(1-6-1)Spring Security - 配置文件解析和访问请求处理
1.在pom.xml中添加maven坐标 <dependency> <groupId>org.springframework.security</groupId> ...
- Spring Security 入门(1-7)Spring Security - Session管理
参考链接:https://xueliang.org/article/detail/20170302232815082 session 管理 Spring Security 通过 http 元素下的子元 ...
- Spring Security 入门(1-3-5)Spring Security - remember me!
Remember-Me 功能 概述 Remember-Me 是指网站能够在 Session 之间记住登录用户的身份,具体来说就是我成功认证一次之后在一定的时间内我可以不用再输入用户名和密码进行登录了, ...
- Spring Security 入门(1-6-2)Spring Security - 内置的filter顺序、自定义filter、http元素和对应的filterChain
Spring Security 的底层是通过一系列的 Filter 来管理的,每个 Filter 都有其自身的功能,而且各个 Filter 在功能上还有关联关系,所以它们的顺序也是非常重要的. 1.S ...
- Spring Security 入门(1-3-2)Spring Security - http元素 - intercept-url配置
http元素下可以配置登录页面,也可以配置 url 拦截. 1.直接配置拦截url和对应的访问权限 <security:http use-expressions="false" ...
- Spring Security 入门(1-4-2)Spring Security - 认证过程之AuthenticationProvider的扩展补充说明
1.用户信息从数据库获取 通常我们的用户信息都不会向第一节示例中那样简单的写在配置文件中,而是从其它存储位置获取,比如数据库.根据之前的介绍我们知道用户信息是通过 UserDetailsService ...
- Spring Security 入门(1-3-1)Spring Security - http元素 - 默认登录和登录定制
登录表单配置 - http 元素下的 form-login 元素是用来定义表单登录信息的.当我们什么属性都不指定的时候 Spring Security 会为我们生成一个默认的登录页面. 如果不想使用默 ...
- Spring Security 入门(1-2)Spring Security - 从 配置例子例子 开始我们的学习历程
1.Spring Security 的配置文件 我们需要为 Spring Security 专门建立一个 Spring 的配置文件,该文件就专门用来作为 Spring Security 的配置. &l ...
- Spring Security入门(2-3)Spring Security 的运行原理 4 - 自定义登录方法和页面
参考链接,多谢作者: http://blog.csdn.net/lee353086/article/details/52586916 http元素下的form-login元素是用来定义表单登录信息的. ...
随机推荐
- Node与apidoc的邂逅——NodeJS Restful 的API文档生成
作为后台根据需求文档开发完成接口后,交付给前台(angular vue等)做开发,不可能让前台每个接口调用都去查看你的后台代码一点点查找.前台开发若不懂你的代码呢?让他一个接口一个接口去问你怎么调用, ...
- Numpy库的下载及安装(吐血总结)
Python很火,我也下了个来耍耍一阵子.可是渐渐地,我已经不满足于它的基本库了,我把目光转到了Numpy~~~~~ 然而想法总是比现实容易,因为我之前下的是Python3.3.x,所有没有自带pip ...
- git将文件托管到github上遇到的问题
先来一问题描述: 执行:$ git push -u origin master 结果Warning: Permanently added the RSA host key for IP address ...
- jQuery 3.0最终版发布,十大新特性眼前一亮
jQuery 3.0在日前发布了最终的全新版本.从2014年10月,jQuery团队对这个主要大版本进行维护开始,web开发者社区便一直在期待着这一刻的到来,终于在2016年6月他们迎来了这一个最终板 ...
- laravel-Policy步骤
用户授权Policy 定义策略类 php artisan make:policy <name> 定义方法 注册策略类和模型关联 app > Providers > AuthSe ...
- 笔记:Struts2 拦截器
配置拦截器 Struts.xml 配置文件中,使用<interceptor-/>来定义拦截器,有属性 name 表示拦截器的名称,class 表示拦截器的具体首先类,可以使用<par ...
- Bootstrap3 datetimepicker控件的使用
Bootstrap3 日期+时间选择控件 1.支持日期选择,格式设定 2.支持时间选择 3.支持时间段选择控制 4.支持中文 官网地址:http://eonasdan.github.io/bootst ...
- Java多线程:ThreadLocal
一.ThreadLocal基础知识 ThreadLocal是线程的一个本地化对象,或者说是局部变量.当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的 ...
- SQL注入之Sqli-labs系列第三篇
废话不在多说 let's go! 继续挑战第三关(Error Based- String) 1.访问地址,加入参数后 and 1=1和and 1=2进行测试,木有任何动静 2.再使用 ' 出现报 ...
- LeetCode-101.对称二叉树
链接:https://leetcode-cn.com/problems/symmetric-tree/description/ 给定一个二叉树,检查它是否是它自己的镜像(即,围绕它的中心对称). 例如 ...