浅析 Spring Security 的认证过程及相关过滤器
前言
上一篇文章 浅析 Spring Security 核心组件 中介绍了Spring Security的基本组件,有了前面的基础,这篇文章就来详细分析下Spring Security的认证过程。
Spring Security 的核心之一就是它的过滤器链,我们就从它的过滤器链入手,下图是Spring Security 过滤器链的一个执行过程,本文将依照该过程来逐步的剖析其认证过程。
核心过滤器链简介
Spring Security 中的过滤器有很多,一般正常的项目中都有十几个过滤器,有时候还包含自定义的过滤器,当然我们不可能对每一个过滤器都进行分析,我们需要抓住重点,找比较关键的几个过滤器,它们在认证过程中扮演着重要角色,下面列举几个核心的过滤器:
SecurityContextPersistenceFilter:整个Spring Security 过滤器链的开端,它有两个作用:一是当请求到来时,检查
Session
中是否存在SecurityContext
,如果不存在,就创建一个新的SecurityContext
。二是请求结束时将SecurityContext
放入Session
中,并清空SecurityContextHolder
。UsernamePasswordAuthenticationFilter:继承自抽象类
AbstractAuthenticationProcessingFilter
,当进行表单登录时,该Filter将用户名和密码封装成一个UsernamePasswordAuthentication
进行验证。AnonymousAuthenticationFilter: 匿名身份过滤器,当前面的Filter认证后依然没有用户信息时,该Filter会生成一个匿名身份——
AnonymousAuthenticationToken
。一般的作用是用于匿名登录。ExceptionTranslationFilter:异常转换过滤器,用于处理
FilterSecurityInterceptor
抛出的异常。FilterSecurityInterceptor:过滤器链最后的关卡,从 SecurityContextHolder中获取 Authentication,比对用户拥有的权限和所访问资源需要的权限。
表单登录认证过程
当我们访问一个受保护的资源时,如果之前没有进行登录认证,那么系统将返回一个登录表单或者一个响应结果提示我们要先进行登录操作。我们这里的分析过程只针对表单登录,所以我们先在表单中填写用户名和密码进行登录验证。
上面已经简述了一堆核心过滤器,这里先从 SecurityContextPersistenceFilter
这个过滤器的开端开始分析整个表单登录的认证过程。
SecurityContextPersistenceFilter
当我们填写表单完毕后,点击登录按钮,请求先经过 SecurityContextPersistenceFilter
过滤器,在前面就曾提到,该Filter有两个作用,其中之一就是在请求到来时,创建 SecurityContext
安全上下文,我们来看看它内部是如何做的,部分源码如下:
public class SecurityContextPersistenceFilter extends GenericFilterBean {static final String FILTER_APPLIED = "__spring_security_scpf_applied";//安全上下文存储的仓库private SecurityContextRepository repo;private boolean forceEagerSessionCreation = false;public SecurityContextPersistenceFilter() {//使用HttpSession来存储 SecurityContextthis(new HttpSessionSecurityContextRepository());}public SecurityContextPersistenceFilter(SecurityContextRepository repo) {this.repo = repo;}public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;// 如果是第一次请求,request中肯定没有 FILTER_APPLIED属性if (request.getAttribute(FILTER_APPLIED) != null) {// 确保每个请求只应用一次过滤器chain.doFilter(request, response);return;}final boolean debug = logger.isDebugEnabled();// 在request 设置 FILTER_APPLIED 属性为 true,这样同一个请求再次访问时,就直接进入后续Filter的操作request.setAttribute(FILTER_APPLIED, Boolean.TRUE);if (forceEagerSessionCreation) {HttpSession session = request.getSession();if (debug && session.isNew()) {logger.debug("Eagerly created session: " + session.getId());}}// 封装 requset 和 response HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,response);// 从存储安全上下文的仓库中载入 SecurityContext 安全上下文,其内部是从 Session中获取上下文信息SecurityContext contextBeforeChainExecution = repo.loadContext(holder);try {//安全上下文信息设置到 SecurityContextHolder 中,以便在同一个线程中,后续访问 SecurityContextHolder 能获取到 SecuritContextSecurityContextHolder.setContext(contextBeforeChainExecution);//进入下一个过滤器操作chain.doFilter(holder.getRequest(), holder.getResponse());}finally {// 请求结束后,清空安全上下文信息SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();// Crucial removal of SecurityContextHolder contents - do this before anything// else.SecurityContextHolder.clearContext();//将安全上下文信息存储到 Session中,相当于登录态的维护repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());request.removeAttribute(FILTER_APPLIED);if (debug) {logger.debug("SecurityContextHolder now cleared, as request processing completed");}}}public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {this.forceEagerSessionCreation = forceEagerSessionCreation;}
}
请求到来时,利用HttpSessionSecurityContextRepository
读取安全上下文。我们这里是第一次请求,读取的安全上下文中是没有 Authentication
身份信息的,将安全上下文设置到 SecurityContextHolder
之后,进入下一个过滤器。
请求结束时,同样利用HttpSessionSecurityContextRepository
该存储安全上下文的仓库将认证后的SecurityContext
放入 Session
中,这也是登录态维护的关键,具体的操作这里就不细说了。
UsernamePasswordAuthenticationFilter
经过 SecurityContextPersistenceFilter
过滤器后来到 UsernamePasswordAuthenticationFilter
过滤器,因为我们假定的是第一次请求,所以 SecurityContext
并没有包含认证过的 Authentication
。从此过滤器开始的操作对于表单登录来说是非常关键的,包含了表单登录的核心认证步骤,下面画了一张在此过滤器中的认证过程图:
filter-process
UsernamePasswordAuthenticationFilter
的父类是 AbstractAuthenticationProcessingFilter
,首先进入父类的 foFilter
方法,部分源码如下:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBeanimplements ApplicationEventPublisherAware, MessageSourceAware {...public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;...Authentication authResult;try {//调用子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法authResult = attemptAuthentication(request, response);if (authResult == null) {// return immediately as subclass has indicated that it hasn't completed// authentication//子类未完成认证,立刻返回return;}sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {logger.error("An internal error occurred while trying to authenticate the user.",failed);unsuccessfulAuthentication(request, response, failed);return;}catch (AuthenticationException failed) {//认证失败unsuccessfulAuthentication(request, response, failed);return;}// 认证成功if (continueChainBeforeSuccessfulAuthentication) {//继续调用下一个 Filterchain.doFilter(request, response);}//将成功认证后的Authentication写入 SecurityContext中successfulAuthentication(request, response, chain, authResult);}
}
该doFilter
方法中一个核心就是调用子类 UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,该方法进入真正的认证过程,并返回认证后的 Authentication
,该方法的源码如下:
public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {//必须是POST请求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();//将用户名和密码封装成一个 UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//核心部分,交给内部的AuthenticationManager去认证,并返回认证后的 Authenticationreturn this.getAuthenticationManager().authenticate(authRequest);}
该方法中有一个关键点就是 his.getAuthenticationManager().authenticate(authRequest)
,调用内部的 AuthenticationManager
去认证,在之前的文章就介绍过AuthenticationManager,它是身份认证的核心接口,它的实现类是 ProviderManager
,而 ProviderManager
又将请求委托给一个 AuthenticationProvider
列表,列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性
下面来分析一个关键的 AuthenticationProvider
,它就是 DaoAuthenticationProvider
,它是框架最早的provider,也是最最常用的 provider。大多数情况下我们会依靠它来进行身份认证,它的父类是 AbstractUserDetailsAuthenticationProvider
,认证过程首先会调用父类的 authenticate
方法,核心源码如下:
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));// Determine usernameString username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {1 //调用子类 DaoAuthenticationProvider 的 retrieveUser()方法获取 UserDetailsuser = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}//没拿到UserDetails会抛出异常信息catch (UsernameNotFoundException notFound) {logger.debug("User '" + username + "' not found");if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}else {throw notFound;}}Assert.notNull(user,"retrieveUser returned null - a violation of the interface contract");}try {2 //对UserDetails的一些属性进行预检查,即判断用户是否锁定,是否可用以及用户是否过期preAuthenticationChecks.check(user);3 //对UserDetails附加的检查,对传入的Authentication与从数据库中获取的UserDetails进行密码匹配additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException exception) {if (cacheWasUsed) {// There was a problem, so try again after checking// we're using latest data (i.e. not from the cache)cacheWasUsed = false;user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}else {throw exception;}}4 //对UserDetails进行后检查,检查UserDetails的密码是否过期postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (forcePrincipalAsString) {principalToReturn = user.getUsername();}5 //上面所有检查成功后,用传入的用户信息和获取的UserDetails生成一个成功验证的Authenticationreturn createSuccessAuthentication(principalToReturn, authentication, user);
}
从上面一大串源码中,提取几个关键的方法:
retrieveUser(…): 调用子类 DaoAuthenticationProvider 的 retrieveUser()方法获取 UserDetails
preAuthenticationChecks.check(user):对从上面获取的UserDetails进行预检查,即判断用户是否锁定,是否可用以及用户是否过期
additionalAuthenticationChecks(user,authentication): 对UserDetails附加的检查,对传入的Authentication与获取的UserDetails进行密码匹配
postAuthenticationChecks.check(user): 对UserDetails进行后检查,即检查UserDetails的密码是否过期
createSuccessAuthentication(principalToReturn, authentication, user):上面所有检查成功后,利用传入的Authentication 和获取的UserDetails生成一个成功验证的Authentication
retrieveUser(…)方法
接下来详细说说 retrieveUser(...)
方法, DaoAuthenticationProvider 的 retrieveUser() 源码如下:
protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {//经过UserDetailsService 获取 UserDetailsUserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}
}
该方法最核心的部分就是调用内部的UserDetailsServices 加载 UserDetails,UserDetailsServices
本质上就是加载UserDetails的接口,UserDetails包含了比Authentication更加详细的用户信息。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户。我们也可以自己实现UserDetailsServices接口,比如我们是如果是基于数据库进行身份认证,那么我们可以手动实现该接口,而不用JdbcDaoImpl。
additionalAuthenticationChecks()
UserDetails的预检查和后检查比较简单,这里就不细说了,下面来看一下密码匹配校验,代码如下:
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {if (authentication.getCredentials() == null) {logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}String presentedPassword = authentication.getCredentials().toString();//利用 PasswordEncoder编码器校验密码if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}}
这个方法实际上是调用DaoAuthenticationProvider
的additionalAuthenticationChecks
方法,内部调用加密解密器进行密码匹配,如果匹配失败,则抛出一个 BadCredentialsException
异常
最后通过createSuccessAuthentication(..)
方法生成一个成功认证的 Authentication,简单说就是组合获取的UserDetails和传入的Authentication,得到一个完全填充的Authentication。
该Authentication最终一步一步向上返回,到AbstractAuthenticationProcessingFilter
过滤器中,将其设置到 SecurityContextHolder
。
AnonymousAuthenticationFilter
匿名认证过滤器,它主要是针对匿名登录,如果前面的Filter,比如UsernamePasswordAuthenticationFilter
执行完毕后,SecurityContext依旧没有用户信息,那么AnonymousAuthenticationFilter
才会起作用,生成一个匿名身份信息——AnonymousAuthenticationToken
ExceptionTranslationFilter
ExceptionTranslationFilter
简单的说就是处理 FilterSecurityInterceptor 抛出的异常,其内部 doFilter
方法源码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;try {//直接进入下一个Filterchain.doFilter(request, response);logger.debug("Chain processed normally");}catch (IOException ex) {throw ex;}//真正的作用在这里,处理抛出的异常catch (Exception ex) {// Try to extract a SpringSecurityException from the stacktraceThrowable[] causeChain = throwableAnalyzer.determineCauseChain(ex);RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);//这里会处理 FilterSecurityInterceptor 抛出的AccessDeniedExceptionif (ase == null) {ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}if (ase != null) {if (response.isCommitted()) {throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);}handleSpringSecurityException(request, response, chain, ase);}else {// Rethrow ServletExceptions and RuntimeExceptions as-isif (ex instanceof ServletException) {throw (ServletException) ex;}else if (ex instanceof RuntimeException) {throw (RuntimeException) ex;}// Wrap other Exceptions. This shouldn't actually happen// as we've already covered all the possibilities for doFilterthrow new RuntimeException(ex);}}
}
FilterSecurityInterceptor
FilterSecurityInterceptor
过滤器是最后的关卡,之前的请求最终会来到这里,它的大致工作流程就是
封装请求信息
从系统中读取配置信息,即资源所需的权限信息
从
SecurityContextHolder
中获取之前认证过的Authentication
对象,即表示当前用户所拥有的权限然后根据上面获取到的三种信息,传入一个权限校验器中,对于当前请求来说,比对用户拥有的权限和资源所需的权限。若比对成功,则进入真正系统的请求处理逻辑,反之,会抛出相应的异常
下面画一张简易的流程图来阐述 FilterSecurityInterceptor
的执行过程,如下:
filter_processs
根据上图内容,我们再来看看 FilterSecurityInterceptor
的源码,
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implementsFilter {...public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {// 封装request、response请求FilterInvocation fi = new FilterInvocation(request, response, chain);//调用核心方法invoke(fi);} ...public void invoke(FilterInvocation fi) throws IOException, ServletException {if ((fi.getRequest() != null)&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)&& observeOncePerRequest) {// filter already applied to this request and user wants us to observe// once-per-request handling, so don't re-do security checkingfi.getChain().doFilter(fi.getRequest(), fi.getResponse());}else {// 判断当前请求之前是否经历过该过滤器if (fi.getRequest() != null && observeOncePerRequest) {// 如果当前请求已经经历过这个安全过滤器判断,那么不再执行后续逻辑,直接往下走,调用请求的处理方法fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);}//调用父类的方法,执行授权判断逻辑InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());}finally {super.finallyInvocation(token);}super.afterInvocation(token, null);}
}}
源码中已经对请求进行了封装,然后进入核心部分, 调用父类的授权判断方法——beforeInvocation(FilterInvocation)
,源码如下:
protected InterceptorStatusToken beforeInvocation(Object object) {Assert.notNull(object, "Object was null");final boolean debug = logger.isDebugEnabled();if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {throw new IllegalArgumentException("Security invocation attempted for object "+ object.getClass().getName()+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "+ getSecureObjectClass());}//读取Spring Security的配置信息,将其封装成 ConfigAttributeCollection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);if (attributes == null || attributes.isEmpty()) {if (rejectPublicInvocations) {throw new IllegalArgumentException("Secure object invocation "+ object+ " was denied as public invocations are not allowed via this interceptor. "+ "This indicates a configuration error because the "+ "rejectPublicInvocations property is set to 'true'");}...return null; // no further work post-invocation}...if (SecurityContextHolder.getContext().getAuthentication() == null) {credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound","An Authentication object was not found in the SecurityContext"),object, attributes);}//从SecurityContextHolder中获取AuthenticationAuthentication authenticated = authenticateIfRequired();// 启动授权匹配try {this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));throw accessDeniedException;}...}
beforeInvocation
的源码比较多,我这里只保留了相对核心的部分,从源码就可以看出,拿到配置信息和用户信息后,连同请求信息一同传入AccessDecisionManager
的 decide(Authentication authentication, Object object,Collection<ConfigAttribute> configAttributes)
方法。该方法是最终执行授权校验逻辑的地方。
AccessDecisionManager 本身是一个接口,它的 实现类是 AbstractAccessDecisionManager
,而 AbstractAccessDecisionManager
也是一个抽象类,它的实现类有三个,常用的是 AffirmativeBased
,最终的授权校验逻辑是 AffirmativeBased 实现的,部分源码如下:
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(messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));}...
}
该方法的逻辑比较简单,就是执行AccessDecisionVoter
的校验逻辑,如果校验失败就抛出AccessDeniedException
异常。对于AccessDecisionVoter的vote
投票逻辑这里就不细说了,在 Spring Security 3.0以后,一般默认使用 AccessDecisionVoter
接口的实现类WebExpressionVoter来完成最终的校验过程。
小结
上面从过滤器出发,对 Spring Security的认证过程做了一个还算详细的分析,当然还存在很多细节问题没有涉及到。
参考资料 & 鸣谢
1. Spring Security(四)–核心过滤器源码分析
https://www.cnkirito.moe/spring-security-4/
2. SPRING SECURITY 4官方文档中文翻译与源码解读
http://www.tianshouzhi.com/api/tutorials/spring_security_4/278
3. Spring Security
https://docs.spring.io/spring-security/site/docs/5.1.0.RELEASE/reference/htmlsingle/
source://pjmike.github.io/2018/10/15/浅析Spring-Security的认证过程及相关过滤器/
记得点「赞」和「在看」↓
爱你们
浅析 Spring Security 的认证过程及相关过滤器相关推荐
- Spring Security用户认证和权限控制(默认实现)
1 背景 实际应用系统中,为了安全起见,一般都必备用户认证(登录)和权限控制的功能,以识别用户是否合法,以及根据权限来控制用户是否能够执行某项操作. Spring Security是一个安全相关的框架 ...
- 最简单易懂的Spring Security 身份认证流程讲解
最简单易懂的Spring Security 身份认证流程讲解 导言 相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确 ...
- spring security 自定义认证登录
spring security 自定义认证登录 1.概要 1.1.简介 spring security是一种基于 Spring AOP 和 Servlet 过滤器的安全框架,以此来管理权限认证等. 1 ...
- java spring 登录验证_浅析Spring Security登录验证流程源码
一.登录认证基于过滤器链 Spring Security的登录验证流程核心就是过滤器链.当一个请求到达时按照过滤器链的顺序依次进行处理,通过所有过滤器链的验证,就可以访问API接口了. SpringS ...
- jwt token注销_【原创精选】OAuth 2.0+JWT+spring security完成认证授权-生产级-附带源码...
前言导读 分析一下为什么要用OAuth2和JWT来做 1. **单点登录(SSO)**方案单击登录方案是最常见的解决方案,但单点登录需要每个与用户交互的服务都必须与认证服务进行通信,这不但会造成重复, ...
- 安全框架Spring Security(认证和授权)
Spring Security 今天从以下几个内容介绍: Spring Security简介 Spring Security的Maven依赖 Spring Security使用 Spring Secu ...
- Spring Security身份认证Authentication
文章目录 Authentication(身份认证框架)的架构 SecurityContextHolder AuthenticationManager AbstractAuthenticationPro ...
- Spring Security 0auth2 认证服务器和资源服务器实现
一,OAuth2开放授权协议/标准 OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者 上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享他们数据的所 ...
- Spring Security CAS认证
13.7 CAS认证 13.7.1概述 JA-SIG生产一种称为CAS的企业级单点登录系统.与其他计划不同,JA-SIG的中央身份验证服务是开源的,广泛使用的,易于理解,独立于平台,并支持代理功能.S ...
最新文章
- 体验cygwin纪实
- 【Linux 内核】进程管理 ( 系统调用简介 | 进程相关系统调用源码 )
- ABAP:FTP Using SAP Functions
- 读完这些论文和代码,你就能在搜狐算法大赛中获得好成绩了
- 论文学习10-Joint entity recognition and relation extraction as a multi-head selection problem(实体关系联合抽取模型
- matlab函数冒号代表的意思,MATLAB中冒号运算符的含义
- 力扣645.错误的集合
- ApacheCN 捐赠名单 2018
- 沉淀再出发:关于java中的AQS理解
- 机械硬盘低级格式化软件_万能硬盘低格工具(支持所有IDE、SATA、移动硬盘)
- 水调歌头·明月几时有
- java打印取消页眉页脚_Javascript页面打印的页眉页脚的清除与设置
- 程序员小灰2021年整理
- 校园网路由器有线中继(针对802.1X)
- VirtualBox安装Ubuntu20.04 + 安装增强功能
- PON、EPON、GPON的区别
- 推荐几款常用的企业网盘软件,你喜欢哪一款?
- hyperf搭建与使用RPC微服务
- Hadoop分布式高可用HA集群搭建笔记(含Hive之构建),java高级架构师视频
- 人脸识别之虹软-登录功能的实现
热门文章
- 填坑——自动驾驶相关坐标系
- 大数据处理技术导论(8) | Datawhale组队学习46期
- 1085: 【入门】百钱百鸡问题
- 互联网摸鱼日报(2023-06-09)
- JSON parse error: Cannot deserialize instance of `com.haha.phb.io.UserArrayIO
- mysql latin1 utf8差别_为什么mysql 默认的test数据库character是latin1不是utf8
- 为什么说:善战者,无赫赫之功
- Visual Studio 代码自动补全
- 国内智慧城市标准是怎样的?
- html5 video播放调研,全屏,伪全屏