相信任何一个平台都绕不开用户认证和鉴权这两个功能,最近我恰好负责调研Spring Security接第三方认证中心的技术方案。所以借此机会,认真调研学习一番它。
Spring Boot Security分为两部分讲解——认证、授权,这一篇分析认证过程,下一篇分析授权过程。
顺便说一下这次调研过程中的教训:面对这种比较复杂的技术/功能,不能像对待一般功能,copy百度上的代码就行了。了解它的原理之后,再动手效率会高很多。
一、需求介绍
我们有一个公共的认证中心(Auth Server),前端页面在Auth Server登录后,会得到一个token。页面访问业务应用的API时,需要在header里携带“sessionId: token”。业务应用的Spring Security就负责调用Auth Server API验证得到的token是否合法,以及获取对应的用户信息、角色信息。
我们主要讲业务应用里的Spring Security是如何运作的。
二、Security处理过程
认证是通过一系列的filter来实现的,通过debug来梳理Spring Filter运作流程:
- 首先进入 ApplicationFilterChain 类
它负责管理针对request的一系列filter的执行,当所有的filter执行完成后,它最终会调用servlet的service():
/**
* Implementation of javax.servlet.FilterChain
used to manage
* the execution of a set of filters for a particular request. When the
* set of defined filters has all been executed, the next call to
* doFilter()
will execute the servlet's service()
* method itself.
*
* @author Craig R. McClanahan
*/
public final class ApplicationFilterChain implements FilterChain {
如下图所示,我们可以发现它共用6个filter需要执行,其中第5个则是security相关的filter:
- 进入security相关的filter bean:springSecurityFilterChain
它实则类为DelegatingFilterProxy.class
public class DelegatingFilterProxy extends GenericFilterBean {
...
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 本类会委派给真正的filter(默认是FilterChainProxy.class)
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// 让这个被委派的delegateToUse filter去真正执行任务
invokeDelegate(delegateToUse, request, response, filterChain);
}
- 进入真正负责处理security相关事宜的filter:FilterChainProxy.class
public class FilterChainProxy extends GenericFilterBean {
//该对象里有默认的security相关的11个filter
//针对我们的定制化需求,我们需要往这里面增加一个我们自己编写的filter。
private List filterChains;
...
...
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
//得到chain里面的filters
List filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
加上下文我们写的filter,chain里共有12个filter:
- 这些filter都执行完后,会执行
servlet.service(request, response);
三、Demo编写(请认真阅读代码中的注释信息,涉及到细节)
现在我们结合上述原理,来编写代码...
大致思路就是首先配置好spring security,然后写自己的filter,并加入到默认的11个filter中。
- 搭建Spring Boot程序,maven pom.xml主要信息如下:
org.springframework.boot
spring-boot-starter-parent
2.0.6.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
- 写一个简单的Controller
@RestController
//意味着该用户的角色必须包含"ROLE_admin"
@PreAuthorize("hasRole('admin')")
public class TestController {
@RequestMapping({ "/api/test" })
public String user() {
return "success";
}
}
- 配置security
@Configuration
@EnableWebSecurity
//配合Controller层的注解 @PreAuthorize("hasRole('admin')") 使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* filter(生成Authentication对象) -> provider manager -> provider(校验Authentication)
*也可以把全部的校验任务放在filter里实现,provider可以理解为供filter的使用的另一个类
*/
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
/**
* 添加自己的provider,给providerManager管理
* @param auth
*/
@Override
protected void configure(
AuthenticationManagerBuilder auth) {
auth.authenticationProvider(myAuthenticationProvider);
}
@Override
public void configure(HttpSecurity http) throws Exception {
//下面这一行很重要,添加一些基本的配置
super.configure(http);
MyAuthenticationFilter filter = new MyAuthenticationFilter();
//给自己的filter添加provider manager
filter.setAuthenticationManager(super.authenticationManagerBean());
//把自己的Filter添加到FilterChain合适的位置
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
- 自定义包含用户信息的Authentication对象:MyAuthenticationToken
它是用于认证、鉴权的核心对象
public class MyAuthenticationToken extends AbstractAuthenticationToken {
private Object principal;
public MyAuthenticationToken(Collection extends GrantedAuthority> authorities) {
super(authorities);
principal = "my principal";
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
- 实现自己的Filter(可以在这里面写认证的相关代码,也可以放到provider里写)
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 对以/api/开头的请求进行过滤处理
*/
protected MyAuthenticationFilter() {
super(new RegexRequestMatcher("^(/api/).*", null));
}
/**
* 开始认证的核心代码
* 返回null则代表还需要继续认证(其他filter)
* 抛出AuthenticationException的子类,则代表认证失败 --> 401
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//SimpleGrantedAuthority是代表用户的角色
List roleList = new ArrayList<>();
//角色如果是admin,则设置为"ROLE_admin"
roleList.add(new SimpleGrantedAuthority("ROLE_" + request.getHeader("sessionId")));
//MyAuthenticationToken是Authentication的实现类,它是用于security认证处理的对象!!!
MyAuthenticationToken authenticationToken = new MyAuthenticationToken(roleList);
//标志为已认证,但并不代表不会被再次认证,取决于AbstractSecurityInterceptor.class(它的子类就是第十二个filter:FilterSecurityInterceptor)里的alwaysReauthenticate字段
authenticationToken.setAuthenticated(true);
authenticationToken.setDetails(new Object());
return authenticationToken;
//!!!如果需要自定义的AuthenticationProvider来进行后续认证操作,则可以用下一行代码
//由于我打算直接在本Filter里完整认证,则不需要provider
//getAuthenticationManager()是得到ProviderManager,该Manager会调用相关的provider
//return getAuthenticationManager().authenticate(authenticationToken);
//认证失败 --> 401
//throw new FailureAuthenticationException("error");
}
/**
* 认证成功后,默认是重定向到一个系统默认地址
* 所以重载:当认证成功后,继续后续操作,访问Controller层
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
try {
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext();
}
}
}
- 实现自己的provider
不是必须的,它是负责处理 filter里生成的authentication对象,我们可以直接在filter里硬嵌相关代码
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
/**
* 认证filter产生的Authentication对象
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MyAuthenticationToken token = (MyAuthenticationToken) authentication;
return token;
}
/**
* 指定支持的Authentication类型
*/
@Override
public boolean supports(Class> authentication) {
return MyAuthenticationToken.class.isAssignableFrom(authentication);
}
}
我们的认证和鉴权的Demo就此完成!
四、测试
Demo git 地址:https://gitee.com/cherron/spring-security-demo
Security的底层原理实在太复杂,我所写的内容只是冰山一角。如果有什么疑问,可以在评论区一起探讨。
鉴权的原理会在下篇博客里讲解!