相信任何一個(gè)平臺(tái)都繞不開(kāi)用戶認(rèn)證和鑒權(quán)這兩個(gè)功能设预,最近我恰好負(fù)責(zé)調(diào)研Spring Security接第三方認(rèn)證中心的技術(shù)方案默辨。所以借此機(jī)會(huì),認(rèn)真調(diào)研學(xué)習(xí)一番它兢孝。
Spring Boot Security分為兩部分講解——認(rèn)證浇坐、授權(quán)睬捶,這一篇分析認(rèn)證過(guò)程,下一篇分析授權(quán)過(guò)程近刘。
順便說(shuō)一下這次調(diào)研過(guò)程中的教訓(xùn):面對(duì)這種比較復(fù)雜的技術(shù)/功能擒贸,不能像對(duì)待一般功能,copy百度上的代碼就行了觉渴。了解它的原理之后介劫,再動(dòng)手效率會(huì)高很多。
一案淋、需求介紹
我們有一個(gè)公共的認(rèn)證中心(Auth Server)座韵,前端頁(yè)面在Auth Server登錄后,會(huì)得到一個(gè)token哎迄。頁(yè)面訪問(wèn)業(yè)務(wù)應(yīng)用的API時(shí),需要在header里攜帶“sessionId: token”隆圆。業(yè)務(wù)應(yīng)用的Spring Security就負(fù)責(zé)調(diào)用Auth Server API驗(yàn)證得到的token是否合法漱挚,以及獲取對(duì)應(yīng)的用戶信息、角色信息渺氧。
我們主要講業(yè)務(wù)應(yīng)用里的Spring Security是如何運(yùn)作的旨涝。
二、Security處理過(guò)程
認(rèn)證是通過(guò)一系列的filter來(lái)實(shí)現(xiàn)的侣背,通過(guò)debug來(lái)梳理Spring Filter運(yùn)作流程:
- 首先進(jìn)入 ApplicationFilterChain 類
它負(fù)責(zé)管理針對(duì)request的一系列filter的執(zhí)行白华,當(dāng)所有的filter執(zhí)行完成后,它最終會(huì)調(diào)用servlet的service():
/**
* Implementation of <code>javax.servlet.FilterChain</code> 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
* <code>doFilter()</code> will execute the servlet's <code>service()</code>
* method itself.
*
* @author Craig R. McClanahan
*/
public final class ApplicationFilterChain implements FilterChain {
如下圖所示贩耐,我們可以發(fā)現(xiàn)它共用6個(gè)filter需要執(zhí)行弧腥,其中第5個(gè)則是security相關(guān)的filter:
- 進(jìn)入security相關(guān)的filter bean:springSecurityFilterChain
它實(shí)則類為DelegatingFilterProxy.class
public class DelegatingFilterProxy extends GenericFilterBean {
...
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 本類會(huì)委派給真正的filter(默認(rèn)是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;
}
}
// 讓這個(gè)被委派的delegateToUse filter去真正執(zhí)行任務(wù)
invokeDelegate(delegateToUse, request, response, filterChain);
}
- 進(jìn)入真正負(fù)責(zé)處理security相關(guān)事宜的filter:FilterChainProxy.class
public class FilterChainProxy extends GenericFilterBean {
//該對(duì)象里有默認(rèn)的security相關(guān)的11個(gè)filter
//針對(duì)我們的定制化需求,我們需要往這里面增加一個(gè)我們自己編寫的filter潮太。
private List<SecurityFilterChain> 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<Filter> 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個(gè)filter:
- 這些filter都執(zhí)行完后,會(huì)執(zhí)行
servlet.service(request, response);
三铡买、Demo編寫(請(qǐng)認(rèn)真閱讀代碼中的注釋信息更鲁,涉及到細(xì)節(jié))
現(xiàn)在我們結(jié)合上述原理,來(lái)編寫代碼...
大致思路就是首先配置好spring security奇钞,然后寫自己的filter澡为,并加入到默認(rèn)的11個(gè)filter中。
- 搭建Spring Boot程序景埃,maven pom.xml主要信息如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<!-- 必要的組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- 寫一個(gè)簡(jiǎn)單的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對(duì)象) -> provider manager -> provider(校驗(yàn)Authentication)
*也可以把全部的校驗(yàn)任務(wù)放在filter里實(shí)現(xiàn)媒至,provider可以理解為供filter的使用的另一個(gè)類
*/
@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對(duì)象:MyAuthenticationToken
它是用于認(rèn)證塘慕、鑒權(quán)的核心對(duì)象
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;
}
}
- 實(shí)現(xiàn)自己的Filter(可以在這里面寫認(rèn)證的相關(guān)代碼筋夏,也可以放到provider里寫)
public class MyAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 對(duì)以/api/開(kāi)頭的請(qǐng)求進(jìn)行過(guò)濾處理
*/
protected MyAuthenticationFilter() {
super(new RegexRequestMatcher("^(/api/).*", null));
}
/**
* 開(kāi)始認(rèn)證的核心代碼
* 返回null則代表還需要繼續(xù)認(rèn)證(其他filter)
* 拋出AuthenticationException的子類,則代表認(rèn)證失敗 --> 401
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//SimpleGrantedAuthority是代表用戶的角色
List<SimpleGrantedAuthority> roleList = new ArrayList<>();
//角色如果是admin图呢,則設(shè)置為"ROLE_admin"
roleList.add(new SimpleGrantedAuthority("ROLE_" + request.getHeader("sessionId")));
//MyAuthenticationToken是Authentication的實(shí)現(xiàn)類条篷,它是用于security認(rèn)證處理的對(duì)象!8蛑赴叹!
MyAuthenticationToken authenticationToken = new MyAuthenticationToken(roleList);
//標(biāo)志為已認(rèn)證,但并不代表不會(huì)被再次認(rèn)證指蚜,取決于AbstractSecurityInterceptor.class(它的子類就是第十二個(gè)filter:FilterSecurityInterceptor)里的alwaysReauthenticate字段
authenticationToken.setAuthenticated(true);
authenticationToken.setDetails(new Object());
return authenticationToken;
//F蚯伞!摊鸡!如果需要自定義的AuthenticationProvider來(lái)進(jìn)行后續(xù)認(rèn)證操作绽媒,則可以用下一行代碼
//由于我打算直接在本Filter里完整認(rèn)證,則不需要provider
//getAuthenticationManager()是得到ProviderManager免猾,該Manager會(huì)調(diào)用相關(guān)的provider
//return getAuthenticationManager().authenticate(authenticationToken);
//認(rèn)證失敗 --> 401
//throw new FailureAuthenticationException("error");
}
/**
* 認(rèn)證成功后是辕,默認(rèn)是重定向到一個(gè)系統(tǒng)默認(rèn)地址
* 所以重載:當(dāng)認(rèn)證成功后,繼續(xù)后續(xù)操作猎提,訪問(wèn)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();
}
}
}
- 實(shí)現(xiàn)自己的provider
不是必須的获三,它是負(fù)責(zé)處理 filter里生成的authentication對(duì)象,我們可以直接在filter里硬嵌相關(guān)代碼
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
/**
* 認(rèn)證filter產(chǎn)生的Authentication對(duì)象
*/
@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);
}
}
我們的認(rèn)證和鑒權(quán)的Demo就此完成锨苏!
四疙教、測(cè)試
Demo git 地址:https://gitee.com/cherron/spring-security-demo
Security的底層原理實(shí)在太復(fù)雜贞谓,我所寫的內(nèi)容只是冰山一角。如果有什么疑問(wèn)葵诈,可以在評(píng)論區(qū)一起探討经宏。
鑒權(quán)的原理會(huì)在下篇博客里講解!