Spring-Security如何获取当前登录用户信息

在公司项目中的UserContent.getUser()方法中有一段很“神奇”的代码,通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()可得到当前用户的登录信息。

一直很非常好奇,为何一个静态方法可以得到当前登录用户信息呢,它是怎么知道的从浏览器传过来的请求是属于哪个用户?

在网上查了相关资料,终于找到了答案。网上资料只是讲了ThreadLocal类,虽然每次打断点到当前的请求线程,可以每次可以看到Thread.threadLocals中有登录用户信息,却始终没有找到是何时放入的。因为Tomcat启动了线程池来处理用户的请求,跟踪代码时,发现用户的请求一会被http-apr-8082-exec-7线程处理,一会被http-apr-8082-exec-5线程处理,所以在这里被误导了好久。最后灵光一闪,打了一个方法断点在SecurityContextHolder.setContent()上,终于搞明白了。

先来看一下顺序图

SecurityContextHolderStrategy的实现类是ThreadLocalSecurityContextHolderStrategy,定义了静态变量contextHolder:

1
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();

contextHolder赋值是在每次请求的DelegatingFilterProxy过滤器里放入的,具体调用线程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"http-apr-8082-exec-7@8976" daemon prio=5 tid=0x36 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:85)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at com.cnfantasia.server.ms.pub.filter.SecurityServletFilter.doFilter(SecurityServletFilter.java:46)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:218)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:956)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:442)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1082)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:623)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2517)
at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2506)
- locked <0x2a6e> (a org.apache.tomcat.util.net.AprEndpoint$AprSocketWrapper)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

SecurityContextPersistenceFilter中的代码片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (forceEagerSessionCreation) {
HttpSession session = request.getSession();

if (debug && session.isNew()) {
logger.debug("Eagerly created session: " + session.getId());
}
}

HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

try {
SecurityContextHolder.setContext(contextBeforeChainExecution);

chain.doFilter(holder.getRequest(), holder.getResponse());

} finally {
// other code
}

注意contextBeforeChainExecution是从Session中取出的,回归本质用户登录信息还是存储在Session中的。只不过Spring-Security为了方便取出当前用户登录信息,将信息放到线程变量中,可以用静态方法SecurityContextHolder.getContext()取出,而不用再传用户的Request和Response。

PS:另一种获取当前登录用户的方法, 可以与在自定义的Filter中,可以用来记录用户请求日志什么的

1
2
3
4
5
HttpServletRequest req = (HttpServletRequest) request;    
Principal principal = req.getUserPrincipal();
if (principal != null) {
String username = principal.getName();
}