针对 SpringSecurity 做了一个详细分析,让你明白它是如何执行的
纸上得来终觉浅,绝知此事要躬行。
不知道你们在使用SpringSecurity安全框架的时候,有没有想过 debug 一步一步看它是如何实现判断是否可以访问的?
如下:
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/role/admin1")
String admin() {return "role: ROLE_ADMIN";
}
为什么我们写上这个注解可以了呢?如何进行判断的呢?
前面写过一次 SpringSecurity 登录流程分析,写那篇文章是为了写 SpringSecurity 实现多种登录方式做铺垫。
那么这次写这个文章的原因呢?
在网上看到了他人写的 SpringSecurity 动态鉴权流程分析,才发觉用注解其实也不是个非常好的事情,直接固定在项目,无法做到动态的更改,是个要不得的事情(捂脸),之前只考虑到这么写蛮好的,看完文章才恍然大悟。这两天也准备实现一下Security的动态鉴权的小demo。
xdm,一定要记得,纸上得来终觉浅,绝知此事要躬行,尤其是一路 debug 的文章,亲身踩坑。
对于一门技术,会使用是说明我们对它已经有了一个简单了解,把脉络、细节都掌握清楚,我们才能更好的使用。
接下来就让来带大家一起看看吧。
二、流程图:
下图是在百度找的一张关于 Security 原理图
我接下来画的流程图是基于用户已经登录的状态下的画的。
整个认证的过程其实一直在围绕图中过滤链的绿色部分,而我们今天要说的鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor。
这也就是我流程图的开始,如下图:
上图如有不妥之处,请大家批正,在此郑重感谢。
关于上图的粗略解释,后文再一一道来:
1、登录后,用户访问一个需要权限的接口,经过一连串过滤器,到达 FilterSecurityInterceptor, FilterSecurityInterceptor 的invoke()方法执行具体拦截行为,具体是 beforeInvocation、finallyInvocation、afterInvocation 这三个方法,这三个方法是定义在父类
AbstractSecurityInterceptor 中。
2、调用
AbstractSecurityInterceptor 的 beforeInvocation 方法。AbstractSecurityInterceptor将确保安全拦截器的正确启动配置。它还将实现对安全对象调用的正确处理,即:
- 获取访问当前资源所需要的权限SecurityMetadataSource..getAttributes(object);返回个 Collection< ConfigAttribute > attributes
- 从SecurityContextHolder获取Authentication对象。 `Authentication authenticated = authenticateIfRequired();
- 尝试授权 attemptAuthorization(object, attributes, authenticated); 调用 AccessDecisionManager 接口 decide 方法,执行鉴权,鉴权不成功,会直接抛异常。
- 返回一个InterceptorStatusToken。
3、经过千辛万苦后,到达MethodSecurityInterceptor,由它再次重新调用起
AbstractSecurityInterceptor.beforeInvocation(mi) 方法,来进行权限的验证
- 鉴权的时候,投票者会换成 PreInvocationAuthorizationAdviceVoter
进入正题前先放张图片缓一缓:
当乌云和白云相遇
三、前半部分
前半部分作用是在检测用户的状态,并非就是执行鉴权,不过两次都十分相近。关于方法上注解的检测是在后半部分。
1)入口:FilterSecurityInterceptor
第一步:FilterSecurityInterceptor void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
//过滤器链实际调用的方法。 简单地委托给invoke(FilterInvocation)方法。
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {invoke(new FilterInvocation(request, response, chain));
}
接着看 void invoke(FilterInvocation filterInvocation)
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {if (isApplied(filterInvocation) && this.observeOncePerRequest) {//过滤器已应用于此请求,用户希望我们观察每个请求处理一次,因此不要重新进行安全检查filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// 第一次调用这个请求,所以执行安全检查if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}//调用 beforeInvocation(filterInvocation) 方法 跟着这个方法往下看InterceptorStatusToken token = super.beforeInvocation(filterInvocation) ;try {//每个过滤器都有这么一步 filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally {//在安全对象调用完成后清理AbstractSecurityInterceptor的工作。//无论安全对象调用是否成功返回,都应该在安全对象调用之后和 afterInvocation 之前调用此方法(即它应该在 finally 块中完成)。super.finallyInvocation(token);}//当调用afterInvocation(InterceptorStatusToken,Object)时,AbstractSecurityInterceptor不会采取进一步的操作。super.afterInvocation(token, null);
}
2)进入:AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
第二步:super.beforeInvocation(filterInvocation); 一些打印信息被精简了,太长不适合阅读
protected InterceptorStatusToken beforeInvocation(Object object) {//检查操作Assert.notNull(object, "Object was null");if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {//....}//这里获取的信息看下图示1 ://object 就是调用处传过来的参数 FilterInvocation filterInvocation,它本身其实就是 HttpServletRequest 和 HttpServletResponse 的增强//object :filter invocation [GET /role/admin1] "//然后我们获取到的就是受保护调用的列表 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);if (CollectionUtils.isEmpty(attributes)) {//...return null; // no further work post-invocation}//在 SecurityContext 中未找到身份验证对象,会发事件抛异常if (SecurityContextHolder.getContext().getAuthentication() == null) {credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"), object, attributes);}//在这里拿到了 Authentication 对象登录的信息 ,后文会简单说是如何拿到的Authentication authenticated = authenticateIfRequired();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));}// Attempt authorization : 尝试授权 这步本文重点,用我的话来说,这就是鉴权的入口 重点关注,下文继续attemptAuthorization(object, attributes, authenticated);//...// Attempt to run as a different userAuthentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);if (runAs != null) {//...}// 无后续动作return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);}
关于 Collection< ConfigAttribute > attributes =
this.obtainSecurityMetadataSource().getAttributes(object);这段代码。
第一次访问这里的时候,FilterSecurityInterceptor是从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取到当前的是这样的数据。它和我们第二次来执行这里有很大的区别。这里的表达式是 authenticated,翻译过来就是认证过的。
在后文会进行比较的。
我们接着往下看:Authentication authenticateIfRequired() 获取身份信息
//如果Authentication.isAuthenticated()返回 false 或属性alwaysReauthenticate已设置为 true,
//则检查当前的身份验证令牌并将其传递给 AuthenticationManager进行身份验证
private Authentication authenticateIfRequired() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {return authentication;}authentication = this.authenticationManager.authenticate(authentication);SecurityContextHolder.getContext().setAuthentication(authentication);return authentication;
}
3)尝试授权: attemptAuthorization()
第三步:尝试授权: attemptAuthorization()
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) {try {//接着套娃 我们去看 AccessDecisionManager 下的 decide() 方法this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException ex) {if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,attributes, this.accessDecisionManager));}else if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));}publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));throw ex;}
}
AccessDecisionManager 决策器说明:
this.accessDecisionManager 其实是个接口。我们一起看看它的源码
public interface AccessDecisionManager {/**
为传递的参数解析访问控制决策。
参数:
身份验证 - 调用方法的调用者(非空)
object – 被调用的安全对象
configAttributes – 与被调用的安全对象关联的配置属性*/void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException;// 下面这两个方法主要起辅助作用的。大都执行检查操作boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);}
我们先看看这个接口结构,之后再看它的实现类内部鉴权机制是如何执行的,需要获取那些信息,又是如何判断它是否可以通过的。
我们可以看到这个 AccessDecisionManager 接口,接口下有一个抽象类,然后再有了三个实现类。
他们分别代表不同的机制。
- AffirmativeBased:如果任何AccessDecisionVoter返回肯定响应,则授予访问权限。即有一票同意,就可以通过,默认是它。
- ConsensusBased:少数服从于多数。多数票同意通过,即可以通过。如民主选举制一样。
- UnanimousBased:要求所有选民弃权或授予访问权限。简称一票反对。只要有一票反对就不能通过。
一起看看默认用的 AffirmativeBased:
public class AffirmativeBased extends AbstractAccessDecisionManager {public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {super(decisionVoters);}/**这个具体的实现只是轮询所有配置的AccessDecisionVoter并在任何AccessDecisionVoter投赞成票时授予访问权限。 仅当存在拒绝投票且没有赞成票时才拒绝访问。
如果每个AccessDecisionVoter放弃投票,则决策将基于isAllowIfAllAbstainDecisions()属性(默认为 false)。*/@Override@SuppressWarnings({ "rawtypes", "unchecked" })public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException {int deny = 0;for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}}if (deny > 0) {throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}// To get this far, every AccessDecisionVoter abstainedcheckAllowIfAllAbstainDecisions();}
}
到这里又会牵扯到 AccessDecisionVoter 出来,也就是能够投票的选民们。
AccessDecisionVoter 投票观众接口
我们先一起来看它的源码,再看看它的实现类:
//表示一个类负责对授权决定进行投票。
//投票的协调(即轮询AccessDecisionVoter ,统计他们的响应,并做出最终授权决定)由AccessDecisionManager执行。
public interface AccessDecisionVoter<S> {int ACCESS_GRANTED = 1;int ACCESS_ABSTAIN = 0;int ACCESS_DENIED = -1;//这两个用来执行check操作,判断参数是否合法等等boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);/**
指示是否授予访问权限。
决定必须是肯定的 ( ACCESS_GRANTED )、否定的 ( ACCESS_DENIED ) 或者 AccessDecisionVoter可以弃权 ( ACCESS_ABSTAIN ) 投票。
在任何情况下,实现类都不应返回任何其他值。 如果需要对结果进行加权,则应改为在自定义AccessDecisionManager处理。
除非AccessDecisionVoter由于传递的方法调用或配置属性参数而专门用于对访问控制决策进行投票,否则它必须返回ACCESS_ABSTAIN 。
这可以防止协调AccessDecisionManager计算来自那些AccessDecisionVoter的选票,而这些AccessDecisionVoter对访问控制决策没有合法利益。
虽然安全对象(例如MethodInvocation )作为参数传递以最大限度地提高访问控制决策的灵活性,但实现类不应修改它或导致所表示的调用发生(例如,通过调用MethodInvocation.proceed() ) .*/int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
我们看看它的结构:
- RoleVoter 主要用来判断当前请求是否具备该接口所需要的角色
- RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承
- WebExpressionVoter 这是一个基于表达式权限控制的投票器
- Jsr250Voter 处理 Jsr-250 权限注解的投票器,如 @PermitAll,@DenyAll 等。
- AuthenticatedVoter 用于判断 ConfigAttribute 上是否拥有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三种角色。
- AbstractAclVoter 提供编写域对象 ACL 选项的帮助方法,没有绑定到任何特定的 ACL 系统。
- PreInvocationAuthorizationAdviceVoter 使用 @PreFilter 和 @PreAuthorize 注解处理的权限,通过 PreInvocationAuthorizationAdvice 来授权。
AffirmativeBased默认传入的构造器只有一个 WebExpressionVoter,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。
所以我们在执行第一次循环时,也是在这里处理的。
public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();@Overridepublic int vote(Authentication authentication, FilterInvocation filterInvocation,Collection<ConfigAttribute> attributes) {//...执行的一些检查//WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);if (webExpressionConfigAttribute == null) {return ACCESS_ABSTAIN;}//允许对EvaluationContext进行后处理。 实现可能会返回一个新的EvaluationContext实例或修改传入的EvaluationContext 。EvaluationContext ctx = webExpressionConfigAttribute.postProcess(//调用内部模板方法来创建StandardEvaluationContext和SecurityExpressionRoot对象。this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);//针对指定的根对象评估默认上下文中的表达式。 如果评估结果与预期结果类型不匹配(并且无法转换为),则将返回异常。boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);// 投赞同票,返回if (granted) {return ACCESS_GRANTED;}return ACCESS_DENIED;}//循环判断private WebExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {for (ConfigAttribute attribute : attributes) {if (attribute instanceof WebExpressionConfigAttribute) {return (WebExpressionConfigAttribute) attribute;}}return null;}//...
}
在这里的数据也是如此,和我们上文就互相对应上了。
4)返回过程
4.1、先返回至AffirmativeBased.decide()方法处,投票通过,继续 retrun
for (AccessDecisionVoter voter : getDecisionVoters()) {int result = voter.vote(authentication, object, configAttributes);switch (result) {case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny++;break;default:break;}
}
4.2、返回至
AbstractSecurityInterceptor 方法调用处,这里是无返回值,直接回到 beforeInvocation 方法中。
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) {try {this.accessDecisionManager.decide(authenticated, object, attributes);}
}
4.3、再返回至beforeInvocation 方法中,
protected InterceptorStatusToken beforeInvocation(Object object) {// 返回到这里,我们再顺着往下看,看如何执行 attemptAuthorization(object, attributes, authenticated);// Attempt to run as a different user :尝试以其他用户身份运行Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);if (runAs != null) {SecurityContext origCtx = SecurityContextHolder.getContext();SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());SecurityContextHolder.getContext().setAuthentication(runAs);// 需要恢复到 token.Authenticated 调用后 true的意思是:如果能以其他用户运行 就执行刷新return new InterceptorStatusToken(origCtx, true, attributes, object);}return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
4.4、回到了我们梦开始的地方了:
FilterSecurityInterceptor.invoke() 方法
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {if (isApplied(filterInvocation) && this.observeOncePerRequest) {// 过滤器已应用于此请求,用户希望我们观察每个请求处理一次,因此不要重新进行安全检查filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());return;}// 第一次调用这个请求,所以执行安全检查if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}//返回至此处 //InterceptorStatusToken类上的doc注释说://AbstractSecurityInterceptor子类接收的返回对象。//这个类反映了安全拦截的状态,以便最终调用AbstractSecurityInterceptor.afterInvocation(InterceptorStatusToken, Object)可以正确整理。InterceptorStatusToken token = super.beforeInvocation(filterInvocation);try {//每个过滤器的必备代码filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}
四、后半部分
对方法注解的鉴权,是真的一步一步看它如何执行的,一直扒,真的是历经千辛万苦。
默认大家都能看的懂这个图了,我们直接转到 MethodSecurityInterceptor 里来看看它做了什么吧
4.1、入口:MethodSecurityInterceptor
//提供对基于 AOP 联盟的方法调用的安全拦截。
//此安全拦截器所需的SecurityMetadataSource是MethodSecurityMetadataSource类型。 这与基于 AspectJ 的安全拦截器 ( AspectJSecurityInterceptor ) 共享,因为两者都与 Java Method 。
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements MethodInterceptor {private MethodSecurityMetadataSource securityMetadataSource;//此方法应用于对MethodInvocation强制实施安全性。@Overridepublic Object invoke(MethodInvocation mi) throws Throwable {//beforeInvocation 这个有没有似曾相识 ,莫错哈 就是我们之前在 FilterSecurityInterceptor 看到的那个 //需要注意到的是 之前我们传的参是一个 FilterInvocation ,这里则是一个 MethodInvocation 。InterceptorStatusToken token = super.beforeInvocation(mi);Object result;try {result = mi.proceed();}finally {super.finallyInvocation(token);}return super.afterInvocation(token, result);}//...
}
MethodInvocation :doc注释是"方法调用的描述,在方法调用时提供给拦截器。方法调用是一个连接点,可以被方法拦截器拦截".
4.2、进入 AbstractSecurityInterceptor
授权检查 beforeInvocation() 方法
另外在这里debug获取到的值也是不一样的,这点上文我刚刚也说过了。
获取资源访问策略:FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection< ConfigAttribute >。 SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
protected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/r/r1").hasAuthority("r1").antMatchers("/r/r2").hasAuthority("r2")....
}
中间的过程同上半部分差不多,就不多说了。我们直接看 AffirmativeBased 情况如何。
4.3、转战:AffirmativeBasedl;
attemptAuthorization(object, attributes, authenticated);
this.accessDecisionManager.decide(authenticated, object, attributes);
接着往下,到此处就同之前稍有不同了,我们之前用到的是 WebExpressionVoter,在这里我们使用的是:
PreInvocationAuthorizationAdviceVoter
我们接着进入:
PreInvocationAuthorizationAdviceVoter,它的类上的doc注释如下:
Voter 使用从 @PreFilter 和 @PreAuthorize 注释生成的
PreInvocationAuthorizationAdvice 实现来执行操作。 在实践中,如果使用这些注解,它们通常会包含所有必要的访问控制逻辑,因此基于投票者的系统并不是真正必要的,包含相同逻辑的单个AccessDecisionManager就足够了。 然而,这个类很容易与 Spring Security 使用的传统的基于投票者的AccessDecisionManager实现相适应。
我们可以很容易的看出,这个就是处理方法上注解的那个类。接着看下它的源码。
public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> {private final PreInvocationAuthorizationAdvice preAdvice;public PreInvocationAuthorizationAdviceVoter(PreInvocationAuthorizationAdvice pre) {this.preAdvice = pre;}//...一些检查方法@Overridepublic int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {// 查找 prefilter 和 preauth(或组合)属性,如果两者都为 null,则弃权使用它们调用建议PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes);if (preAttr == null) {// 没有基于表达式的元数据,所以弃权return ACCESS_ABSTAIN;}//在这里又委托给 PreInvocationAuthorizationAdvice接口的before方法来做判断return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED;}private PreInvocationAttribute findPreInvocationAttribute(Collection<ConfigAttribute> config) {for (ConfigAttribute attribute : config) {if (attribute instanceof PreInvocationAttribute) {return (PreInvocationAttribute) attribute;}}return null;}
}
简单看一下
PreInvocationAuthorizationAdvice接口的before方法的默认实现:
before方法的说明是:应该执行的“before”建议以执行任何必要的过滤并决定方法调用是否被授权。
我们先说说它的参数:(Authentication authentication,MethodInvocation mi,PreInvocationAttribute attr),第一个就是当前登录的用户,二就是要执行的方法,三就是方法上的注解信息。 我们可以很简单的看出这段代码的含义,就是在比较已经登录的用户,是否拥有这个方法上所需要的权限。
另外简单说明一下:
- createEvaluationContext 的dco注释:提供评估上下文,在其中评估调用类型的安全表达式(即 SpEL 表达式)。我个人对这块没有特别深入过,没法说清楚,大家可以查一查。
- 另外我们看一下debug的详细信息,大家应该就差不多能懂啦。
接下来就是一步一步返回啦
最后就是:
这里的 result 就是方法执行的返回结果。紧接着就是一步一步返回过滤器链啦。
对于这里 proceed 方法就不再深入了。这个点拉出来说,怕是直接可以写上一篇完整的文章啦。
内部很多动态代理啊、反射啊这些相关的,一层套一层的,不是咱研究重点。溜啦溜啦。
五、小结
这张图是在百度上搜到的,大致流程其实就是如此。
其实内部还有很多很多值得推敲的东西,不是在这一篇简单的文章中能够写出来的。
六、自我感受
还记得我第一次说要看源码是在准备研究 Mybatis 的时候,那时候上头看了大概几天吧,看着看着就看不下去了,找不到一个合适的方法,什么都想看,没有一个非常具体的目标,导致连续受挫,结果就是不了了之了。
第二次真正意义看源码就是看 Security 。原因是当时在写项目的时,我的前端小伙伴说,现在大部分网站都有多种登录方式,你能实现不?
男人肯定是不能说不行,然后我就一口答应下来了。结果就是疯狂百度、google,到处看博客。互联网这么庞大,当然也有找到非常多的例子,也有源码解析。但是找到的文章,要么只贴出了核心代码,要么就是不合适(庞大,难以抽取),总之一句话没法运行。就很烦操。
不过文章中都提到了要理解 Security 的登录过程,然后进行仿写,俗称抄作业。最后,真就是一步一步 debug 去看 Security 的登录过程,写出了 第一篇 Security登录认证流程分析,紧接着又去用 SpringSecurity实现多种登录方式,如邮件验证码、电话号码登录。这次即是机缘巧合,也是心有所念,耗费不少时间写出了这篇文章。感觉还是非常不错的。
希望大家能够喜欢,如果 xdm 对此也感兴趣,希望大家在有时间的情况,debug 几次,记忆会深刻很多。并竟 纸上得来终觉浅,绝知此事要躬行。
针对 SpringSecurity 做了一个详细分析,让你明白它是如何执行的相关推荐
- 详细分析开源软件 ExifTool 的任意代码执行漏洞 (CVE-2021-22204)
聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士 本文作者详述了自己如何从 ExifTool 发现漏洞的过程. 背景 在查看我最喜欢的漏洞奖励计划时,我发现他们使用ExifTool 从所上 ...
- uClinux 启动过程详细分析
uclinux启动的详细过程有着诸多的信息可以给我们巨大的启发,我们在这里讨论的就是要对这些信息做一个具体细致的分析,通过我们的讨论,大家会对uclinux启动过程中出现的.以前感觉熟悉的.但却又似是 ...
- JPEG 原理详细分析
一 JPEG 概述 JPEG 是 Joint Photographic Experts Group 的缩写,即 ISO 和 IEC 联合图像专家组,负责静态图像压缩标准的制定,这个专家组开发的算法就被 ...
- jmap 文件解析_使用jmap dump出来的java堆镜像文件,可以用什么打开做详细分析
匿名用户 1级 2018-11-15 回答 其中jmap是java自带的工具 查看整个JVM内存状态 jmap -heap [pid] 要注意的是在使用CMS GC 情况下,jmap -heap的执行 ...
- [Revit教程]斑马:分享一个用Revit自适应构件做安全疏散距离分析的方法#S007
摘要:本文分享的是自适应构件在Revit正向设计中的一种应用方式.涉及知识点:自适应族.共享参数.报告参数. 图1.自适应构件使用演示GIF,斑马自绘 1.需求来源 在建筑施工图设计中,消防设计是非常 ...
- 入职链家前自己给自己做的一个竞品分析
一个"码农"在要从四线城市到魔都去打拼前给自己做的竞品分析--最终选择了链家. 序号 房地产O2O平台 /频道 业务范围 业务1 业务2 业务3 业务4 业务5 业务6 业务7 业 ...
- (六)一个交易者的资金管理系统:做交易记录并分析盈利和亏损
作者:chen_h 微信号 & QQ:862251340 微信公众号:coderpai (一)一个交易者的资金管理系统(上) (二)一个交易者的资金管理系统(下) (三)一个交易者的资金管理系 ...
- 有一个设计时钟的题目,进行详细分析(一)
先对题目要求及元器件介绍 一.要求:设计一个能实现从 00:00--23:59 计数的数字电子钟: 二.所用芯片: 1.辅助芯片:74LS00 两片,用于分频器和计数器的复位: 劲非 74LS08 两 ...
- HashMap 源码详细分析(JDK1.8)
1. 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap.HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现.HashMap 允许 null 键和 null 值 ...
最新文章
- 参加第十六届智能车竞赛学生提出的问题-05-10
- 微信WeixinJSBridge API
- 前端笔记-vue cli中v-bind动态数据实时更新
- Jquery 同个类名中点击的显示其他隐藏的效果
- 内容推荐策略产品经理的方法和实践
- 本机不装Oracle,使用plsql连接远程Oracle的方法
- BGP-13 配置BGP多路径发布
- 讯飞语音报错:未经授权的语音应用.(错误码:11210)
- 计算机 桌面上的文件怎么发送,文本文件如何发送到QQ邮箱里?
- LaTeX 相对于 Word 有什么优势?
- 工作之余,请IT人员开怀大笑吧
- python panda3d从入门_panda3d 入门
- 【OpenCV 学习笔记】—— 基于拉普拉斯金字塔的图像融合原理以及C++实现【或许是全网最通俗易懂的讲解】
- 图像分析之曲率滤波(困惑篇)
- 以太坊联盟链-多节点私链搭建手册
- 使用计算机打印汉字文档是汉字(),浙江财经学院本科社会调查汇报撰写规范.doc...
- PDF.js使用心得
- 秒,毫秒,微秒,纳秒,皮秒,飞秒
- (一)我要偷偷学习html,然后惊艳我们班的人!(标签学习)
- Windows Server 2019 DC 上修改域名