SpringSecurity源码流程与JWT应用
2021-01-23 13:52:27 21 举报AI智能生成
SpringSecurity逐行源码流程与JWT应用
springSecurity
jwt
springsecurity源码
springsecurity流程图
模版推荐
作者其他创作
大纲/内容
一些重要的Filter类
GenericFilterBean
OncePerRequestFilter<br>
过滤链
ChannelProcessingFilter<br>
处理https,没配置就没有?
WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成
SecurityContextPersistenceFilter<br>
new HttpRequestResponseHolder(request,response)<br>
HttpSessionSecurityContextRepository#loadContext<br>
从request中获取session,从Session中取出已认证用户的信息保存在SecurityContext中,提高效率,<br>避免每一次请求都要解析用户认证信息,方便接下来的filter直接获取当前的用户信息<br>
如果是第一次请求,session没有相关信息,那么会创建一个新的SecurityContext
包装request、response
SecurityContextHolder.setContext(contextBeforeChainExecution);
finally
将上下文保存到HttpSessionSecurityContextRepository
清除Holder中的上下文
HeaderWriterFilter
往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制
CorsFilter
未配置就没有
CsrfFilter
对需要验证的请求验证是否包含csrf的token信息,如果不包含,则报错。<br>这样攻击网站无法获取到token信息,则跨域提交的信息都无法通过过滤器的校验
LogoutFilter
根据request的请求方法和路径匹配判断当前是否为注销URL
默认匹配 POST /logout
this.handler.logout(request, response, auth);
CsrfLogoutHandler<br>
SecurityContextLogoutHandler
使session失效
清除remember me
清除SecurityContextHolder的SecurityContext
LogoutSuccessEventPublishingLogoutHandler
通知注销事件
SimpleUrlLogoutSuccessHandler#onLogoutSuccess(request, response, auth);
重定向,默认为/login?logout
...
UsernamePasswordAuthenticationFilter<br>
AntPathRequestMatcher#matches
是
try {<br> Authentication authResult = UsernamePasswordAuthenticationFilter#attemptAuthentication(request, response);<br> if (authResult == null) {<br> return;<br> }<br> sessionStrategy.onAuthentication(authResult, request, response);<br>}<br>catch (AuthenticationException failed){<br> unsuccessfulAuthentication(request, response, failed);<br> return;<br>}<br>
String username = obtainUsername(request);
String password = obtainPassword(request);
new UsernamePasswordAuthenticationToken(username, password)
setDetails(request, token);<br>
return this.getAuthenticationManager().authenticate(token);
遍历本Manager和父Manager的所有AuthenticationProvider<br>如果有AuthenticationProvider支持处理当前类型的Authentication<br>
try {<br> result = provider.authenticate(authentication);<br> if (result != null) {<br> copyDetails(authentication, result);<br> break;<br> }<br> }<br>
AbstractUserDetailsAuthenticationProvider#authenticate(Authentication authentication)<br>
try {<br> user = retrieveUser(username,authentication);<br>}catch (UsernameNotFoundException notFound){<br> throw new BadCredentialsException(xx)<br>}
try {<br> UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);<br> if (loadedUser == null) {<br> throw new InternalAuthenticationServiceException<br> }<br> return loadedUser;<br> }
preAuthenticationChecks.check(user);
!user.isAccountNonLocked()
!user.isEnabled()
!user.isAccountNonExpired()
additionalAuthenticationChecks(user,authentication)
if (authentication.getCredentials() == null)
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
postAuthenticationChecks.check(user);
if (!user.isCredentialsNonExpired())
转换成新的UsernamePasswordAuthenticationToken返回<br>
如果验证成功
擦除所有地方的密码信息
发布AuthenticationSuccessEvent事件<br>
return result;
如果验证失败
抛出AuthenticationException<br>
catch (AuthenticationException failed){<br> unsuccessfulAuthentication(request, response, failed);<br> return;<br>}
清除SecurityContext<br>
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
if (continueChainBeforeSuccessfulAuthentication) {<br> chain.doFilter(request, response);<br> }
successfulAuthentication(request, response, chain, authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
发布InteractiveAuthenticationSuccessEvent事件<br>
successHandler.onAuthenticationSuccess(request, response, authResult);
重定向
不是
chain.doFilter
return
...
DefaultLoginPageGeneratingFilter
if (isLoginUrlRequest(request) || loginError || logoutSuccess)
硬编码html,response返回输出
String loginPageHtml = generateLoginPageHtml(request, loginError,<br> logoutSuccess);
response.getWriter().write(loginPageHtml);
return
DefaultLogoutPageGeneratingFilter
if (this.matcher.matches(request))
硬编码html,response返回输出
renderLogout(request, response);
BasicAuthenticationFilter
ConcurrentSessionFilter
取出session
若过期,进入注销逻辑,return
没过期,更新session
没session就跳过
...
BasicAuthenticationFilter
没配置就没有
处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolder<br>
RequestCacheAwareFilter
从缓存中寻找是否已经有解析过的请求,若有,替换掉原生请求
否则,继续
SecurityContextHolderAwareRequestFilter
封装Request,丰富API
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,<br> (HttpServletResponse) res), res)
...
RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, <br>如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统
AnonymousAuthenticationFilter
检测 SecurityContextHolder 中的SecurityContext的 Authentication是否为null,若是<br>
生成一个AnonymousAuthenticationToken<br>key随机生成,username为anonymous,权限只有ROLE_Anonymous<br>authenticated为true<br>
否则跳过
...
SessionManagementFilter
sessionAuthenticationStrategy.onAuthentication(authentication,<br> request, response)<br>
securityContextRepository.saveContext(SecurityContextHolder.getContext(),<br> request, response);
ExceptionTranslationFilter<br>
直接chain.doFilter(request, response)<br>
通过catch处理下一个Filter或应用逻辑产生的异常
AuthenticationException
sendStartAuthentication
AccessDeniedException<br>
当前是匿名认证或者是 记住我
sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(xx))<br>
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, respo
authenticationEntryPoint.commence(request, response, reason);
其他
accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);
FilterSecurityInterceptor<br>
InterceptorStatusToken token = AbstractSecurityInterceptor#beforeInvocation<br>
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()<br> .getAttributes(object)
初步判断上下文是否有Authentication对象
没有就直接抛AuthenticationCredentialsNotFoundException<br>
Authentication authenticated = authenticateIfRequired();<br>
首先再次获取到authentication对象
Authentication authentication = SecurityContextHolder.getContext()<br> .getAuthentication()
if (authentication.isAuthenticated())<br>
是则直接返回authentication<br>
authentication = authenticationManager.authenticate(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
try {<br> this.accessDecisionManager.decide(authenticated, object, attributes);<br> }<br> catch (AccessDeniedException accessDeniedException){<br> // 发布AuthorizationFailureEvent事件<br> throw accessDeniedException; // 再次抛出<br>}
for (AccessDecisionVoter voter : getDecisionVoters())
int result = voter.vote(authentication, object, configAttributes);
结果相加,判断是否通过
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,attributes);
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,attributes, object)
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
继续执行Servlet的过滤链
进入dispatcherServlet
AbstractSecurityInterceptor#finallyInvocation<br>
AbstractSecurityInterceptor#afterInvocation
综合应用<br>利用JWT实现前后端分离的认证与授权
思路分析
SpringSecurity自带的过滤器认证原理
请求登录
检验参数是否正确
如果正确,生成sessionId,放在Cookie:JSESSIONID给前端浏览器保存
请求资源
前端每次资源访问带着Cookie,SpringSecurity解析Cookie中的SessionId,在内存中比对判断是否之前登陆过
如果比对成功,会拿出之前解析过的SecurityContext,带有已经解析过的认证信息,之后检验的时候就会直接放行,不需要再检验
前后端分离情景下的分析
但是前后端分离存在跨域,前端需要通过ajax发送登录或者获取资源的请求,那么就需要特殊处理带上Cookie,存在XSS风险<br>如果Cookie设置为HttpOnly,也存在XSRF风险
因此,选择在请求头放入Token,采用JWT认证方式,后端解析Token来进行认证与授权
实现JWT的要求
请求登录
请求体中带有username和password<br>
为了更安全,password可为加密过的
如果正确,运用JWT工具包生成Token,放在响应头给前端浏览器保存
请求资源
前端使用工具,如VUEX,每个资源请求都在请求头带上Token
服务器读取请求头中的Token并解析,若成功,根据Token中的信息生成Authentication对象,放入SecurityContext
成功进入请求的接口,进行对应的逻辑,返回资源
结合SpringSecurity具体实现
我们实现的Filter
结合SpringSecurity具体实现
思路
首先明确,我们需要实现自己的Filter的方法<br>因为默认内置的Filter都是操作Cookie/Session的,无法满足要求
认证流程根据SpringSecurity的方法来<br>Filter委托Manager来具体认证<br>Manager委托所有的Provider来认证<br>每个Provider利用UserdetailsService的方法来具体实现认证。
由于内置的UsernamePasswordAuthenticationFilter中的Manager是通过Configurer类配置的<br>我们的Filter可以通过继承很容易得到,另外再配置Manager、实现具体的UserdetailsService就行了
思路过程
-可能最佳的选择是继承UsernamePasswordAuthentication/AbstractAuthenticationProcessingFilter,<br>因为功能最多,而且可以配合SpringSecurity自身的逻辑。<br>比如异常处理机制。改造相关的方法即可。<br>-也可以选择继承BasicAuthenticationFilter、OncePerRequestFilter,更加简单<br>-最后通过http.addFilter()添加到容器即可。<br>
不同的父类,具体操作方法会有较大不同!
在右边源码流程可以知道,添加我们自定义的Filter是在Manager初始化之后的<br>所以我们在配置类中添加Filter的时候就可以为Filter指定当前容器内的Manger(调用WebSecurityConfigurerAdapter#authenticationManager方法)<br>
所以唯一需要具体分情况编写的就是UserdetailsService/Provider
若只实现UserdetailsService,则会自动生成默认Provider包装
注入到Manager的方法
直接使用@Bean注入容器,会被自动检测到
使用http.userdetailsService()
若自定义Provider和UserdetailsService,还可以自定义缓存等其他配置
注入到Manager的方法
直接使用@Bean注入容器,会被自动检测到
具体运作流程
JWTutils
引入io.jsonwebtoken:jjwt依赖
引入javax.xml.bind:jaxb-api
需要提供的方法
根据UserDetails生成Token
根据Token解析生成UserDetails<br>
如果Token瞎写的或者过期会报异常
根据旧Token,刷新Token的过期时间,返回新的Token
一次请求
进入自定义的UsernamePasswordAuthenticationFilter
路径为登陆路径,如/login
否则进入验证登录的逻辑
从requestbody里提取username,password
通过new UsernamePasswordAuthenticationToken<br>(username, password)获建造一个Authentication
调用authenticationManager.authenticate(Authentication)
若验证成功,返回生成的Token
路径为其他路径,请求资源
检测是否携带token
没有Token或者Token瞎写的或者Token过期,返回对应的提示信息
Token解析正确,进入Controller执行对应逻辑,返回资源和刷新后的Token
解决CORS跨域问题
http.cors()
#addCorsMappings
@CrossOrigin(allowCredentials = "true", allowedHeaders = "*")
配置CorsConfigurationSource的Bean
服务器端权限控制
方法权限注解控制
JSR
@Rolesallowed
@secured
表达式
@EnableGlobalMethodSecurity
SPEL
@PreAuthorize
@PreAuthorize("hasRole('USER')")<br>
@PreAuthorize("hasPermission(#contact, 'admin')")
@PreAuthorize("#c.name == authentication.name")
@P
@Param
@PreAuthorize(“authentication.principal.username.equals(#username)”)
@PostAuthorize
@PostAuthorize("returnObject.id%2==0")
源码流程
自动配置类初始化
SecurityAutoConfiguration<br>
@import
SpringBootWebSecurityConfiguration<br>
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
如果有手动的配置类则跳过
负责未自定义的情况下提供一个默认的WebSecurityConfigurerAdapter,配置了基本信息、基本账户
WebSecurityEnablerConfiguration<br>
@EnableWebSecurity
@EnableGlobalAuthentication
@Import(AuthenticationConfiguration.class)<br>
@Import(ObjectPostProcessorConfiguration.class)
AutowireBeanFactoryObjectPostProcessor<br>
postProcess(T object)
对object调用bean工厂的initializeBean方法
回调Aware接口
回调afterPropertiesSet
调用postProcessBeforeInitialization<br>
对object调用bean工厂的autowireBean方法
populateBean
AuthenticationManagerBuilder
实现类为DefaultPasswordEncoderAuthenticationManagerBuilder
初始化3个认证管理器的configurerBean类,如果是(非直接手动操控authManagerBuilder)的配置,后面会取来用<br>
EnableGlobalAuthenticationAutowiredConfigurer<br>
使得优先初始化@EnableGlobalAuthentication的的BEAN
InitializeAuthenticationProviderBeanManagerConfigurer<br>
InitializeUserDetailsBeanManagerConfigurer<br>
作用:在之后检测上下文中是否有自定义的UserDetailsService,如果有,进行自动化配置<br>
@Import
WebSecurityConfiguration<br>
#setFilterChainProxySecurityConfigurer()<br>
使用objectPostProcessor加工new出WebSecurity对象<br>
读取上下文中所有webSecurityConfigurer类,排序
将所有webSecurityConfigurer加入webSecurity<br>
#springSecurityFilterChain()<br>
return webSecurity.build()<br>--doBuild()<br>
beforeInit()<br>
init();
调用所有的WebSecurityConfigurerAdapter#init(final WebSecurity web)<br>
HttpSecurity http = getHttp();
AuthenticationManager authenticationManager = authenticationManager();<br>--getAuthenticationManager();
WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder auth)<br>
注意!父类这里会默认设置disableLocalConfigureAuthenticationBldr属性为true,<br>即关闭了手动调用过此方法操控AuthenticationManagerBuilder的功能,子类覆盖此方法即可打破此规则。<br>所以SpringSecurity提供了两种配置方法:<br>
authenticationConfiguration+authenticationBuilder<br>采用此方法,只能使用@Bean来操控Manager Build过程
AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);<br>
将globalAuthConfigurers的三个configurer的Bean加入builder
authBuilder.apply(config);
authenticationManager = authBuilder.build();
beforeInit()<br>
init();
添加InitializeAuthenticationProviderManagerConfigurer
添加InitializeUserDetailsManagerConfigurer
beforeConfigure
configure();
InitializeAuthenticationProviderManagerConfigurer
寻找容器内自定义的AuthenticationProvider类,有就添加到bulider
InitializeUserDetailsBeanManagerConfigurer
寻找容器内自定义的UserDetailsService类,有就处理后添加到bulider<br>
容器内寻找PasswordEncoder<br>
容器内寻找UserDetailsPasswordService
<span style="font-size: inherit;">new出DaoAuthenticationProvider(),并应用上面三个属性</span><br>
调用DaoAuthenticationProvider#afterPropertiesSet<br>
return performBuild();
new ProviderManager(authenticationProviders,parentAuthenticationManager:null)
配置eraseCredentials
配置eventPublisher
调用AutowireBeanFactoryObjectPostProcessor#postProcess后置处理认证管理器<br>
localConfigureAuthenticationBldr<br>采用此方法,只能覆盖configure方法来操控Manager Build过程<br>
return localConfigureAuthenticationBldr.build()<br>
两种方法功能几乎一样,不过只能选其一。
authenticationBuilder.parentAuthenticationManager(authenticationManager);<br>
持有引用
新建并把所有认证相关的类放入sharedObjects<br>
UserDetailsServiceDelegator(Arrays.asList(localConfigureAuthenticationBldr, globalAuthBuilder)
ApplicationContext
ContentNegotiationStrategy
AuthenticationTrustResolver
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,sharedObjects)
对http对象应用默认配置
链式配置的原理就是给http(HttpSecurity)对象添加了各种configurer对象
从META-INF/spring.factories获取所有AbstractHttpConfigurer并加入HttpSecurity<br>
configure(HttpSecurity http)<br>
web<br> .addSecurityFilterChainBuilder(http)<br> .postBuildAction(() -> {<br> FilterSecurityInterceptor securityInterceptor = http<br> .getSharedObject(FilterSecurityInterceptor.class);<br> web.securityInterceptor(securityInterceptor);<br> })<br><font color="#c41230">将HttpSecurity对象作为SecurityFilterChainBuilder对象加入webSecurity</font>
从HttpSecurity对象中获取一个postBuildAction的Runnable对象,添加到webSecurity<br>
作用是建造完成后为WebSecurity添加FilterSecurityInterceptor对象
beforeConfigure();
configure();
调用所有configurer的configurer.configure(WebSecurity web);
可调用web#ignoring()方法忽略对一些url的验证<br>
可开启调试模式
return performBuild();
List securityFilterChains = new ArrayList<>(chainSize)
对于所有的ignoredRequests,各新建一条DefaultSecurityFilterChain<br>
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
对于所有的securityFilterChainBuilder,开始构造,结果放入securityFilterChains<br>
securityFilterChains.add(securityFilterChainBuilder.build());<br>
#init
SessionManagementConfigurer
RequestCacheConfigurer
AnonymousConfigurer
添加了一个AnonymousAuthenticationProvider
FormLoginConfigurer
#beforeConfigure
建造AuthenticationManager
#configure<br>
ExceptionHandlingConfigurer
ExceptionTranslationFilter
ExpressionUrlAuthorizationConfigurer
FilterSecurityInterceptor<br>
FormLoginConfigurer
HttpSecurity#build返回的是DefaultSecurityFilterChain对象,即所说的SpringSecurity过滤链对象,包括了所需要的所有Filter
return new DefaultSecurityFilterChain(requestMatcher, filters);
自定义的ObjectProcessor在这里生效
new FilterChainProxy(securityFilterChains)
postBuildAction.run();
return filterChainProxy
WebSecurity#build返回的是FilterChainProxy对象
#webSecurityExpressionHandler()
SpringWebMvcImportSelector<br>
WebMvcSecurityConfiguration
OAuth2ImportSelector
SecurityDataConfiguration
new DefaultAuthenticationEventPublisher()
SecurityFilterAutoConfiguration<br>
new DelegatingFilterProxyRegistrationBean(“springSecurityFilterChain”)<br>
new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()<br>
UserDetailsServiceAutoConfiguration<br>
注入一个InMemoryUserDetailsManager
读取SecurityProperties
提供一个随机生成的帐号密码
可在配置文件中手动配置
处理一次请求
FilterChainProxy#doFilter
doFilterInternal(request, response, chain);
防火墙包装request、response,并校验一部分属性
根据request路径选出一个match的SecurityFilterChain,取出它的过滤链<br>
<b>如果取出的SecurityFilterChain是之前配置过的ignore,则没有过滤链,直接进行下一个Servlet 的Filter</b>
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
包装一层
VirtualFilterChain#doFilter<br>
利用currentPosition计数,先执行SpringSecurity的过滤链
见左边过滤链
然后继续执行Servelet容器其他的Filter
SecurityContextHolder.clearContext();
Collect
Get Started
Collect
Get Started
Collect
Get Started
Collect
Get Started
评论
0 条评论
下一页