系列教程观看地址:我真的在B站学习
一、IOC容器结构说明
在Spring的具体实现上,子容器和父容器都是通过
ServletContext
的setAttribute方法放到ServletContext中的。但是,ContextLoaderListener
会先于DispatcherServlet创建ApplicationContext,DispatcherServlet在创建时会先找到由ContextLoaderListener所创建的ApplicationContext,再将后者的ApplicationContext作为参数传给DispatcherServlet的ApplicationContext的setParent()
方法。也就是说,子容器的创建依赖于父容器的创建,父容器先于子容器创建
。在Spring源代码中,你可以在FrameServlet.Java中找到如下代码:
wac.setParent(parent);
其中,wac即为由DisptcherServlet创建的ApplicationContext,而parent则为有ContextLoaderListener创建的ApplicationContext。
此后,框架又会调用ServletContext的setAttribute()方法将wac加入到ServletContext中。
由上可知,为了提供更加安全的配置,我们需要将spring-security.xml
放置在applicationContext.xml中加载。
<import resource="classpath:spring-security.xml" />
这样的话,就无法直接访问sercrity的内容,但是当我们开启了方法控制权限的时候,
参考文章地址:基于方法的权限控制
<!-- 开启权限控制的注解支持
secured-annotations="enabled" springSecurity内部的权限控制注解开关
pre-post-annotations="enabled" spring指定的权限控制的注解开关
jsr250-annotations="enabled" 开启java250注解支持
-->
<security:global-method-security
secured-annotations="enabled"
pre-post-annotations="enabled"
jsr250-annotations="enabled" />
相应的注释只能放在applicationContext层级别(即service层等,不能放在controller)
,但是我们 可以将上面上面的那段xml放在spring-mvc
的配置文件中,就可以在controller
层中使用注解了。
二、过滤器
2.1 常见过滤器
- org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用
SecurityContextRepository
在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
SecurityContext中存储了当前用户的认证以及权限信息。
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
- org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
- org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有
post
请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。开启之后,登录和退出都是Post请求,需要添加Token
- org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
- org.springframework.security.web.authentication.
UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
- org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
- org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
- org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
- org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
- org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
- org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
- org.springframework.security.web.access.intercept.
FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权
限。URL级别方法控制
读取数据库中url进行拦截
2.2 过滤器如何加载
springMVC过滤器超类-GenericFilterBean
我们在web.xml中配置了一个名称为springSecurityFilterChain
的过滤器DelegatingFilterProxy
,通过分析doFilter方法得知,通过springSecurityFilterChain
这个名称,得到了一个FilterChainProxy
过滤器,最终在第三步执行了这个过滤器。该类中有 List<SecurityFilterChain> filterChains
成员变量,通过执行doFilterInternal
方法可以获得的所有的过滤器。
三、认证过程解析
3.1 配置文件示例
<?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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--释放静态资源-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载springsecurity的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springsecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--让认证页面可以匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--拦截资源-->
<!--
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<!--配置认证信息-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"/>
<!--配置退出登录信息-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
<!--去掉csrf拦截的过滤器-->
<!--<security:csrf disabled="true"/>-->
</security:http>
<!--把加密对象放入的IOC容器中-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<!--
springsecurity默认的认证必须是加密的,加上{noop}表示不加密认证。
-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!-- 开启权限控制的注解支持
secured-annotations="enabled" springSecurity内部的权限控制注解开关
pre-post-annotations="enabled" spring指定的权限控制的注解开关
jsr250-annotations="enabled" 开启java250注解支持
-->
<security:global-method-security
secured-annotations="enabled"
pre-post-annotations="enabled"
jsr250-annotations="enabled" />
</beans>
3.2 认证流程解析
发送一个/login
的post
请求,被 UsernamePasswordAuthenticationFilter
,该类 继承了AbstractAuthenticationProcessingFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// AbstractAuthenticationProcessingFilter doFilter中执行了该方法,后续会执行一些通过认证或者认证失败后的操作(记住我,保存session信息)
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 封装一个 UsernamePasswordAuthenticationToken 对象,此时的对象没有权限信息,username和password都是前端传递的值
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 通过 authenticate()方法去认证,实现类为 ProviderManager
return this.getAuthenticationManager().authenticate(authRequest);
}
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 执行认证操作的父类接口 AuthenticationProvider 实现类通过 supports方法来判断使用哪个类来认证
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
// 通过for循环去执行判断各个实现类的supports方法,因为我们前面封装的是
//UsernamePasswordAuthenticationToken 对象,查看其中一个实现类的
//AbstractUserDetailsAuthenticationProvider的supports方法可以看出,返回为ture
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
}
}
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// 该方法,传递来UsernamePasswordAuthenticationToken类型的对象,返回为Ture
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
// 真正执行认证的方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
···
// 该方法会调用 userDetailsService.loadUserByUsername(username)方法,即我们自己实现的
// 调用数据库查询,并且有权限信息的 UserDetails 对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
// 验证账号是否可用,对应接口 UserDetails类中的四个可用属性判断
preAuthenticationChecks.check(user);
// 验证账号,密码是否正确
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
···
// 如果验证正确,返回 一个 UsernamePasswordAuthenticationToken对象,此时的对象会有权限信息
return createSuccessAuthentication(principalToReturn, authentication, user)
}
}
- 此时,如果认证成功,并且返回一个带权限信息的 UsernamePasswordAuthenticationToken对象到了
AbstractAuthenticationProcessingFilter
拦截器的doFilter方法中,执行successfulAuthentication(request, response, chain, authResult)
, successfulAuthentication
将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是
常见的“记住我”功能。- 如果认证失败,抛出的议程也会被doFilter方法捕捉,执行
unsuccessfulAuthentication(request, response, failed);
四、杂七杂八
- 关闭basic认证
- springSecurity加密认证,默认使用SCryptPasswordEncoder
- X-Frame-Options头:主要为了防止被别的站点劫持,做iframe引入
- . CSRF:跨站请求伪造&CORS:跨站资源共享
- CORS配置:
https://blog.csdn.net/niugang0920/article/details/79817763
https://www.cnblogs.com/famary/p/10336223.html - JWT跨域认证
- 系列讲解
- addFilterAt() & addFilterBefore()
- Security限制多个用户登陆