在学习Spring Cloud 时,遇到了受权服务oauth 相关内容时,老是只知其一;不知其二,所以决定先把Spring Security 、Spring Security Oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程当中增强印象和理解所撰写的,若有侵权请告知。html
项目环境:java
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
自定义MyUserDetailsUserService类,实现 UserDetailsService 接口的 loadUserByUsername()方法,这里就简单的返回一个Spring Security 提供的 User 对象。为了后面方便演示Spring Security 的权限控制,这里使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin") 设置了user帐号有一个admin的角色权限信息。实际项目中能够在这里经过访问数据库获取到用户及其角色、权限信息。git
@Component
public class MyUserDetailsUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 不能直接使用 建立 BCryptPasswordEncoder 对象来加密, 这种加密方式 没有 {bcrypt} 前缀,
// 会致使在 matches 时致使获取不到加密的算法出现
// java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 问题
// 问题缘由是 Spring Security5 使用 DelegatingPasswordEncoder(委托) 替代 NoOpPasswordEncoder,
// 而且 默认使用 BCryptPasswordEncoder 加密(注意 DelegatingPasswordEncoder 委托加密方法BCryptPasswordEncoder 加密前 添加了加密类型的前缀) https://blog.csdn.net/alinyua/article/details/80219500
return new User("user", PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
复制代码
注意Spring Security 5 开始没有使用 NoOpPasswordEncoder做为其默认的密码编码器,而是默认使用 DelegatingPasswordEncoder 做为其密码编码器,其 encode 方法是经过 密码编码器的名称做为前缀 + 委托各种密码编码器来实现encode的。程序员
public String encode(CharSequence rawPassword) {
return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
}
复制代码
这里的 idForEncode 就是密码编码器的简略名称,能够经过 PasswordEncoderFactories.createDelegatingPasswordEncoder() 内部实现看到默认是使用的前缀是 bcrypt 也就是 BCryptPasswordEncodergithub
public class PasswordEncoderFactories {
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
复制代码
定义SpringSecurityConfig 配置类,并继承WebSecurityConfigurerAdapter覆盖其configure(HttpSecurity http) 方法。web
@Configuration
@EnableWebSecurity //1
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //2
.and()
.authorizeRequests() //3
.antMatchers("/index","/").permitAll() //4
.anyRequest().authenticated(); //6
}
}
复制代码
配置解析:算法
在 resources/static 目录下新建 index.html , 其内部定义一个访问测试接口的按钮spring
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>欢迎</title>
</head>
<body>
Spring Security 欢迎你!
<p> <a href="/get_user/test">测试验证Security 权限控制</a></p>
</body>
</html>
复制代码
建立 rest 风格的获取用户信息接口数据库
@RestController
public class TestController {
@GetMapping("/get_user/{username}")
public String getUser(@PathVariable String username){
return username;
}
}
复制代码
一、访问 localhost:8080 无任何阻拦直接成功浏览器
二、点击测试验证权限控制按钮 被重定向到了 Security默认的登陆页面
三、使用 MyUserDetailsUserService定义的默认帐户 user : 123456 进行登陆后成功跳转到 /get_user 接口
还记得以前讲过 @EnableWebSecurity 引用了 WebSecurityConfiguration 配置类 和 @EnableGlobalAuthentication 注解吗? 其中 WebSecurityConfiguration 就是与受权相关的配置,@EnableGlobalAuthentication 配置了 认证相关的咱们下节再细讨。
首先咱们查看 WebSecurityConfiguration 源码,能够很清楚的发现 springSecurityFilterChain() 方法。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build(); //1
}
复制代码
这个方法首先会判断 webSecurityConfigurers 是否为空,为空加载一个默认的 WebSecurityConfigurerAdapter对象,因为自定义的 SpringSecurityConfig 自己是继承 WebSecurityConfigurerAdapter对象 的,因此咱们自定义的 Security 配置确定会被加载进来的(若是想要了解如何加载进来能够看下WebSecurityConfiguration.setFilterChainProxySecurityConfigurer() 方法)。
咱们看下 webSecurity.build() 方法实现 实际调用的是 AbstractConfiguredSecurityBuilder.doBuild() 方法,其方法内部实现以下:
@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
init();
buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
O result = performBuild(); // 1 建立 DefaultSecurityFilterChain (Security Filter 责任链 )
buildState = BuildState.BUILT;
return result;
}
}
复制代码
咱们把关注点放到 performBuild() 方法,看其实现子类 HttpSecurity.performBuild() 方法,其内部排序 filters 并建立了 DefaultSecurityFilterChain 对象。
@Override
protected DefaultSecurityFilterChain performBuild() throws Exception {
Collections.sort(filters, comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
复制代码
查看DefaultSecurityFilterChain 的构造方法,咱们能够看到有记录日志。
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters); // 按照正常状况,咱们能够看到控制台输出 这条日志
this.requestMatcher = requestMatcher;
this.filters = new ArrayList<>(filters);
}
复制代码
咱们能够回头看下项目启动日志。能够看到下图明显打印了 这条日志,而且把全部 Filter名都打印出来了。==(请注意这里打印的 filter 链,接下来咱们的全部受权过程都是依靠这条filter 链展开 )==
那么还有个疑问: HttpSecurity.performBuild() 方法中的 filters 是怎么加载的呢? 这个时候须要查看 WebSecurityConfigurerAdapter.init() 方法,这个方法内部 调用 getHttp() 方法返回 HttpSecurity 对象(看到这里咱们应该能想到 filters 就是这个方法中添加好了数据),具体如何加载的也就不介绍了。
public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp(); // 1
web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
public void run() {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
}
});
}
复制代码
用了这么长时间解析 @EnableWebSecurity ,其实最关键的一点就是建立了 DefaultSecurityFilterChain 也就是咱们常 security filter 责任链,接下来咱们围绕这个 DefaultSecurityFilterChain 中 的 filters 进行受权过程的解析。
Security的受权过程能够理解成各类 filter 处理最终完成一个受权。那么咱们再看下以前 打印的filter 链,这里为了方便,再次贴出图片
![]()
这里咱们只关注如下几个重要的 filter :
- SecurityContextPersistenceFilter
- UsernamePasswordAuthenticationFilter (AbstractAuthenticationProcessingFilter)
- BasicAuthenticationFilter
- AnonymousAuthenticationFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
SecurityContextPersistenceFilter 这个filter的主要负责如下几件事:
- 经过 (SecurityContextRepository)repo.loadContext() 方法从请求Session中获取 SecurityContext(Security 上下文 ,相似 ApplicaitonContext ) 对象,若是请求Session中没有默认建立一个 authentication(认证的关键对象,因为本节只讲受权,暂不介绍) 属性为 null 的 SecurityContext 对象
- SecurityContextHolder.setContext() 将 SecurityContext 对象放入 SecurityContextHolder进行管理(SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息)
- 因为在 finally 里实现 会在最后经过 SecurityContextHolder.clearContext() 将 SecurityContext 对象 从 SecurityContextHolder中清除
- 因为在 finally 里实现 会在最后经过 repo.saveContext() 将 SecurityContext 对象 放入Session中
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//从Session中获取SecurityContxt 对象,若是Session中没有则建立一个 authtication 属性为 null 的SecurityContext对象
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 将 SecurityContext 对象放入 SecurityContextHolder进行管理 (SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息)
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// 将 SecurityContext 对象 从 SecurityContextHolder中清除
SecurityContextHolder.clearContext();
// 将 SecurityContext 对象 放入Session中
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
复制代码
咱们在 SecurityContextPersistenceFilter 中打上断点,启动项目,访问 localhost:8080 , 来debug看下实现:
你会发现这里的SecurityContxt中的 authtication 是一个名为 anonymousUser (匿名用户)的认证信息,这是由于 请求调用到了 AnonymousAuthenticationFilter , Security默认建立了一个匿名用户访问。
看filter字面意思就知道这是一个经过获取请求中的帐户密码来进行受权的filter,按照惯例,整理了这个filter的职责:
- 经过 requiresAuthentication()判断 是否以POST 方式请求 /login
- 调用 attemptAuthentication() 方法进行认证,内部建立了 authenticated 属性为 false(即未受权)的UsernamePasswordAuthenticationToken 对象, 并传递给 AuthenticationManager().authenticate() 方法进行认证,认证成功后 返回一个 authenticated = true (即受权成功的)UsernamePasswordAuthenticationToken 对象
- 经过 sessionStrategy.onAuthentication() 将 Authentication 放入Session中
- 经过 successfulAuthentication() 调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess 接口 进行成功处理( 能够 经过 继承 AuthenticationSuccessHandler 自行编写成功处理逻辑 )successfulAuthentication(request, response, chain, authResult);
- 经过 unsuccessfulAuthentication() 调用AuthenticationFailureHandler 的 onAuthenticationFailure 接口 进行失败处理(能够经过继承AuthenticationFailureHandler 自行编写失败处理逻辑 )
咱们再看下官方源码的处理逻辑:
// 1 AbstractAuthenticationProcessingFilter 的 doFilter 方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 2 判断请求地址是不是 /login 和 请求方式为 POST (UsernamePasswordAuthenticationFilter 构造方法 肯定的)
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 3 调用 子类 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法
// attemptAuthentication 方法内部建立了 authenticated 属性为 false (即未受权)的 UsernamePasswordAuthenticationToken 对象, 并传递给 AuthenticationManager().authenticate() 方法进行认证,
//认证成功后 返回一个 authenticated = true (即受权成功的) UsernamePasswordAuthenticationToken 对象
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 4 将认证成功的 Authentication 存入Session中
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// 5 认证失败后 调用 AuthenticationFailureHandler 的 onAuthenticationFailure 接口 进行失败处理( 能够 经过 继承 AuthenticationFailureHandler 自行编写失败处理逻辑 )
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 5 认证失败后 调用 AuthenticationFailureHandler 的 onAuthenticationFailure 接口 进行失败处理( 能够 经过 继承 AuthenticationFailureHandler 自行编写失败处理逻辑 )
unsuccessfulAuthentication(request, response, failed);
return;
}
......
// 6 认证成功后 调用 AuthenticationSuccessHandler 的 onAuthenticationSuccess 接口 进行失败处理( 能够 经过 继承 AuthenticationSuccessHandler 自行编写成功处理逻辑 )
successfulAuthentication(request, response, chain, authResult);
}
复制代码
从源码上看,整个流程实际上是很清晰的:从判断是否处理,到认证,最后判断认证结果分别做出认证成功和认证失败的处理。
debug 调试下看 结果,此次咱们请求 localhast:8080/get_user/test , 因为没权限会直接跳转到登陆界面,咱们先输入错误的帐号密码,看下认证失败是否与咱们总结的一致。
结果与预想时一致的,也许你会奇怪这里的提示为啥时中文,这就不得不说Security 5 开始支持 中文,说明咋中国程序员在世界上愈来愈有地位了!!!
此次输入正确的密码, 看下返回的Authtication 对象信息:
能够看到此次成功返回一个 authticated = ture ,没有密码的 user帐户信息,并且还包含咱们定义的一个admin权限信息。放开断点,因为Security默认的成功处理器是SimpleUrlAuthenticationSuccessHandler ,这个处理器会重定向到以前访问的地址,也就是 localhast:8080/get_user/test。 至此整个流程结束。不,咱们还差一个,Session,咱们从浏览器Cookie中看到 Session:
BasicAuthenticationFilter 与UsernameAuthticationFilter相似,不过区别仍是很明显,BasicAuthenticationFilter 主要是从Header 中获取 Authorization 参数信息,而后调用认证,认证成功后最后直接访问接口,不像UsernameAuthticationFilter过程同样经过AuthenticationSuccessHandler 进行跳转。这里就不在贴代码了,想了解的同窗能够直接看源码。不过有一点要注意的是,BasicAuthenticationFilter 的 onSuccessfulAuthentication() 成功处理方法是一个空方法。
为了试验BasicAuthenticationFilter, 咱们须要将 SpringSecurityConfig 中的formLogin()更换成httpBasic()以支持BasicAuthenticationFilter,重启项目,一样访问 localhast:8080/get_user/test,这时因为没权限访问这个接口地址,页面上会弹出一个登录框,熟悉Security4的同窗必定很眼熟吧,一样,咱们输入帐户密码后,看下debug数据:
这时,咱们就可以获取到 Authorization 参数,进而解析获取到其中的帐户和密码信息,进行认证,咱们查看认证成功后返回的Authtication对象信息实际上是和UsernamePasswordAuthticationFilter中的一致,最后再次调用下一个filter,因为已经认证成功了会直接进入FilterSecurityInterceptor 进行权限验证。
这里为何要提下 AnonymousAuthenticationFilter呢,主要是由于在Security中不存在没有帐户这一说法(这里可能描述不是很清楚,但大体意思是这样的),针对这个Security官方专门指定了这个AnonymousAuthenticationFilter ,用于前面全部filter都认证失败的状况下,自动建立一个默认的匿名用户,拥有匿名访问权限。还记得 在讲解 SecurityContextPersistenceFilter 时咱们看到得匿名 autication信息么?若是不记得还得回头看下哦,这里就再也不叙述了。
ExceptionTranslationFilter 其实没有作任何过滤处理,但别小看它得做用,它最大也最牛叉之处就在于它捕获AuthenticationException 和AccessDeniedException,若是发生的异常是这2个异常 会调用 handleSpringSecurityException()方法进行处理。 咱们模拟下 AccessDeniedException(无权限,禁止访问异常)状况,首先咱们须要修改下 /get_user 接口:
@RestController
@EnableGlobalMethodSecurity(prePostEnabled =true) // 开启方法级别的权限控制
public class TestController {
@PreAuthorize("hasRole('user')") //只容许user角色访问
@GetMapping("/get_user/{username}")
public String getUser(@PathVariable String username){
return username;
}
}
复制代码
重启项目,从新访问 /get_user 接口,输入正确的帐户密码,发现返回一个 403 状态的错误页面,这与咱们以前将的流程时一致的。debug,看下处理:
能够明显的看到异常对象是 AccessDeniedException ,异常信息是不容许访问,咱们再看下 AccessDeniedException 异常后的处理方法accessDeniedHandler.handle(),进入到了 AccessDeniedHandlerImpl 的handle()方法,这个方法会先判断系统是否配置了 errorPage (错误页面),没有的话直接往 response 中设置403 状态码。
FilterSecurityInterceptor 是整个Security filter链中的最后一个,也是最重要的一个,它的主要功能就是判断认证成功的用户是否有权限访问接口,其最主要的处理方法就是 调用父类(AbstractSecurityInterceptor)的 super.beforeInvocation(fi),咱们来梳理下这个方法的处理流程:
- 经过 obtainSecurityMetadataSource().getAttributes() 获取 当前访问地址所需权限信息
- 经过 authenticateIfRequired() 获取当前访问用户的权限信息
- 经过 accessDecisionManager.decide() 使用 投票机制判权,判权失败直接抛出 AccessDeniedException 异常
protected InterceptorStatusToken beforeInvocation(Object object) {
......
// 1 获取访问地址的权限信息
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
......
return null;
}
......
// 2 获取当前访问用户权限信息
Authentication authenticated = authenticateIfRequired();
try {
// 3 默认调用AffirmativeBased.decide() 方法, 其内部 使用 AccessDecisionVoter 对象 进行投票机制判权,判权失败直接抛出 AccessDeniedException 异常
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
......
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
复制代码
整个流程其实看起来不复杂,主要就分3个部分,首选获取访问地址的权限信息,其次获取当前访问用户的权限信息,最后经过投票机制判断出是否有权。
整个受权流程核心的就在于这几回核心filter的处理,这里我用序列图来概况下这个受权流程
复制代码
本文介绍受权过程的代码能够访问代码仓库中的 security 模块 ,项目的github 地址 : github.com/BUG9/spring…
若是您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!
欢迎继续阅读下一篇 Spring Security 解析(二) —— 认证过程