一、前言
本項目默認是用session認證用戶的,但是源於要開放某些接口給其他系統調用,故想在保留原先session認證的基礎上,對部分接口使用jwt-token認證。參考了網上的一些資料,針對自己項目實際情況實現如下。
二、解決思路
其實網上很多不是Spring Security做權限框架的,解決思路就是工具生成token,攔截器或過濾器驗證token有效性;還有一些是用了Spring Security權限框架,但是隻用token做權限認證,沒有使用session的,只需要按照這篇文章去自定義認證,然後用過濾器去驗證token。但是這裏面最重要的是理清楚Spring Security是這麼判斷用戶已經登錄的,使用token怎麼讓Spring Security去知道當前已登錄。這就要了解Spring Security的認證流程了。
三、疑惑
首先,我們都知道,用Spring Security獲取當前用戶認證的方法 SecurityContextHolder.getContext().getAuthentication(),這裏大家有沒有思考過,默認情況我們都是用session管理用戶登錄信息的,通過上面的方法是怎麼跟session聯繫起來的,我們看下SecurityContextHolder跟SpringContext實現類的源碼
public class SecurityContextHolder {
// ~ Static fields/initializers
// =====================================================================================
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
static {
initialize();
}
// ~ Methods
// ========================================================================================================
/**
* Explicitly clears the context value from the current thread.
*/
public static void clearContext() {
strategy.clearContext();
}
/**
* Obtain the current <code>SecurityContext</code>.
*
* @return the security context (never <code>null</code>)
*/
public static SecurityContext getContext() {
return strategy.getContext();
}
/**
* Associates a new <code>SecurityContext</code> with the current thread of execution.
*
* @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
*/
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private Authentication authentication;
public SecurityContextImpl() {}
public SecurityContextImpl(Authentication authentication) {
this.authentication = authentication;
}
// ~ Methods
// ========================================================================================================
@Override
public boolean equals(Object obj) {
if (obj instanceof SecurityContextImpl) {
SecurityContextImpl test = (SecurityContextImpl) obj;
if ((this.getAuthentication() == null) && (test.getAuthentication() == null)) {
return true;
}
if ((this.getAuthentication() != null) && (test.getAuthentication() != null)
&& this.getAuthentication().equals(test.getAuthentication())) {
return true;
}
}
return false;
}
@Override
public Authentication getAuthentication() {
return authentication;
}
@Override
public int hashCode() {
if (this.authentication == null) {
return -1;
}
else {
return this.authentication.hashCode();
}
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
if (this.authentication == null) {
sb.append(": Null authentication");
}
else {
sb.append(": Authentication: ").append(this.authentication);
}
return sb.toString();
}
}
我們只看重點部分,SpringContext是通過 SecurityContextHolderStrategy 取出來的,而Authentication對象是它的一個屬性,這裏看起來也沒跟session有什麼關聯,看來重點應該在 SecurityContextHolderStrategy 裏了,我們找到了它的一個實現ThreadLocalSecurityContextHolderStrategy
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
// ~ Static fields/initializers
// =====================================================================================
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
// ~ Methods
// ========================================================================================================
public void clearContext() {
contextHolder.remove();
}
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder.set(context);
}
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
看出來是從線程局部變量裏獲取的,但是是什麼時候放進去的呢?查找了Spring Security的資料後,找到了一個攔截SecurityContextPersistenceFilter
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
//安全上下文存儲的倉庫
private SecurityContextRepository repo;
public SecurityContextPersistenceFilter() {
//HttpSessionSecurityContextRepository是SecurityContextRepository接口的一個實現類
//使用HttpSession來存儲SecurityContext
this(new HttpSessionSecurityContextRepository());
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (request.getAttribute(FILTER_APPLIED) != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//包裝request,response
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
response);
//從Session中獲取安全上下文信息
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
//請求開始時,設置安全上下文信息,這樣就避免了用戶直接從Session中獲取安全上下文信息
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
//請求結束後,清空安全上下文信息
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
}
每次請求過來都會先進入這個攔截器,然後通過HttpSessionSecurityContextRepository來獲取SpringContext上下文,而SpringContext則是放在session裏面的,結束的時候又將SpringContext放回session,此處終於找到關聯session的地方。
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
// 'SPRING_SECURITY_CONTEXT'是安全上下文默認存儲在Session中的鍵值
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
...
private final Object contextObject = SecurityContextHolder.createEmptyContext();
private boolean allowSessionCreation = true;
private boolean disableUrlRewriting = false;
private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
//從當前request中取出安全上下文,如果session爲空,則會返回一個新的安全上下文
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
HttpServletResponse response = requestResponseHolder.getResponse();
HttpSession httpSession = request.getSession(false);
SecurityContext context = readSecurityContextFromSession(httpSession);
if (context == null) {
context = generateNewContext();
}
...
return context;
}
...
public boolean containsContext(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
return session.getAttribute(springSecurityContextKey) != null;
}
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
return null;
}
...
// Session存在的情況下,嘗試獲取其中的SecurityContext
Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
if (contextFromSession == null) {
return null;
}
...
return (SecurityContext) contextFromSession;
}
//初次請求時創建一個新的SecurityContext實例
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
}
四、如何驗證是否登錄
FilterSecurityInterceptor此攔截器是用來判斷用戶是否登錄以及有哪些資源的權限的,這個攔截器最後會找到你配置的未登錄表單路徑,重定向到該路徑,這個我會單獨拿出來講一下。