关于Spring Security中无Session和无状态stateless
Spring Security是J2EE领域使用最广泛的权限框架,支持HTTP BASIC, DIGEST, X509, LDAP, FORM-AUTHENTICATION, OPENID, CAS, RMI, JAAS, JOSSO, OPENNMS, GRAIS....关于其详尽的说明,请参考Spring Security官方网页。但它默认的表table比较少,默认只有用户和角色,还有一个group,不能满足RBAC的最高要求,通过扩展User - Role - Resource - Authority,我们可以实现用户,角色,资源,权限和相互关系的7张表,实现对权限的最灵活配置。
常见的Spring Security配置:
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="authenticationManager"/> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="securityMetadataSource"> <security:filter-security-metadata-source> <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/> <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/> </security:filter-security-metadata-source> </property> </bean>
可见只有基本的ROLE什么的(Spring Security仅有的几个表),不能实现对资源和权限的灵活配置。在xml配置中也没有在数据库中灵活。我们可以通过扩展User - Role - Resource - Authority,我们可以实现用户,角色,资源,权限和相互关系的7张表,实现对权限的最灵活配置。
具体扩展Spring Security实现用户,角色,资源,权限和相互关系的7张表的方法我会在另外的blog中说明。最后能实现用户,角色,资源,权限都可以在数据库表里灵活配置,在后台java的过滤器和拦截器可以自动对资源的访问进行拦截和授权。在html前台,也可以通过扩展Spring Security Tag来实现在html里面用tag来配置某html局部的访问授权。关于如何实现扩展Spring Security Tag来实现在html里面用tag来配置某html局部的访问授权, 我会在另外的blog中说明。今天主要讲Spring Security如何配置成没有Session和无状态session?
Spring Secrity 的 create-session
=
"never"
Spring Security默认的行为是每个登录成功的用户会新建一个Session。这也就是下面的配置的效果:
<http create-session="ifRequired">...</http>
这貌似没有问题,但其实对大规模的网站是致命的。用户越多,新建的session越多,最后的结果是JVM内存耗尽,你的web服务器彻底挂了。有session的另外一个严重的问题是scalability能力,用户压力上来了不能马上新建一台Jetty/Tomcat服务器,因为要考虑Session同步的问题。 先来看看Session过多导致的Jetty JVM 内存耗尽:
java.lang.OutOfMemoryError: Java heap space
(注:Tomcat启动脚本必须加上: -XX:+HeapDumpOnOutOfMemoryError
才能获得 Java heap dump)
用VisualVM打开看看:
Heap dump分析说Web服务器使用了大量的ConcurrentHashMaps来存储Session:
OK。既然Session导致了访问量大了内存溢出,解决办法就是Spring Security禁用Session:
<http create-session="never"> <!-- ... --> </http>
注:这里的意思是说Spring Security对登录成功的用户不会创建Session了,但你的application还新建了session,那么Spring Security会用它的。这点注意了。
禁用了以后用VisualVM看看效果:
效果非常好。而且没有多台web服务器的session同步和共享的问题,可以很方便的搭建多台web应用服务器的集群,前面加上Nginx反向代理和负载均衡。
Spring security 3.1的 create-session="stateless"
Spring Security 3.1开始支持stateless authentication(具体查看 What‘s new in Spring Security 3.1?),配置方法是:
<http create-session="stateless"> <!-- ... --> </http>
主要是在RESTful API,无状态的web调用的stateless authentication。
这个配置的意思是:Spring Security对登录成功的用户不会创建Session了,你的application也不会允许新建session,而且Spring Security会跳过所有的 filter chain:HttpSessionSecurityContextRepository, SessionManagementFilter, RequestCacheFilter.
也就是说每个请求都是无状态的独立的,需要被再次认证re-authentication。开销显然是增大了,因为每次请求都必须在服务器端重新认证并建立用户角色和权限的上下文。
大家知道,Spring Security在认证的过程中,Spring Security会运行一个过滤器(SecurityContextPersistenceFilter)来存储请求的Security Context,这个上下文的存储是一个策略模式,但默认的是保存在HTTP Session中的HttpSessionSecurityContextRepository。现在我们设置了 create-session=”stateless”,就会保存在NullSecurityContextRepository,里面没有任何session在上下文中保持。既然没有为何还要调用这个空的filter?因为需要调用这个filter来保证每次请求完了SecurityContextHolder被清空了,下一次请求必须re-authentication。
Stateless的RESTful authentication认证
刚才说了,配置为stateless的使用场景,例如RESTful api,其每个请求都是无状态的独立的,需要被再次认证re-authentication。操作层面,具体做法是:在每一个REST的call的头header(例如:@HeaderParam annotation. 例子: @HeaderParam.)都带user token 和 application ID,然后在服务器端对每一请求进行re-authentication. (注意:把token放在uri中是糟糕的做法,首先是安全的原因,其次是cache的原因,尽量放在head中)可以写一个拦截器来实现:
@Provider @ServerInterceptor public class RestSecurityInterceptor implements PreProcessInterceptor { @Override public ServerResponse preProcess(HttpRequest request, ResourceMethod method) throws UnauthorizedException { String token = request.getHttpHeaders().getRequestHeader("token").get(0); // user not logged-in? if (checkLoggedIn(token)) { ServerResponse response = new ServerResponse(); response.setStatus(HttpResponseCodes.SC_UNAUTHORIZED); MultivaluedMap<String, Object> headers = new Headers<Object>(); headers.add("Content-Type", "text/plain"); response.setMetadata(headers); response.setEntity("Error 401 Unauthorized: " + request.getPreprocessedPath()); return response; } return null; } }
Spring Security配置文件:
<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint"> <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" /> <security:intercept-url pattern="/authenticate" access="permitAll"/> <security:intercept-url pattern="/**" access="isAuthenticated()" /> </security:http> <bean id="CustomAuthenticationEntryPoint" class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" /> <bean class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" id="authenticationTokenProcessingFilter"> <constructor-arg ref="authenticationManager" /> </bean>
CustomAuthenticationEntryPoint:
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." ); } }
AuthenticationTokenProcessingFilter:
@Autowired UserService userService; @Autowired TokenUtils tokenUtils; AuthenticationManager authManager; public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) { this.authManager = authManager; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @SuppressWarnings("unchecked") Map<String, String[]> parms = request.getParameterMap(); if(parms.containsKey("token")) { String token = parms.get("token")[0]; // grab the first "token" parameter // validate the token if (tokenUtils.validate(token)) { // determine the user based on the (already validated) token UserDetails userDetails = tokenUtils.getUserFromToken(token); // build an Authentication object with the user's info UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request)); // set the authentication into the SecurityContext SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication)); } } // continue thru the filter chain chain.doFilter(request, response); } }
TokenUtils:
public interface TokenUtils { String getToken(UserDetails userDetails); String getToken(UserDetails userDetails, Long expiration); boolean validate(String token); UserDetails getUserFromToken(String token); }
Spring Security其它方面
其他的比如concurrent Session,意思是同一个用户允许同时在线(不同地点)的数量。还有Session劫持的防止, auto-remember等,具体参考这个网页。
reference pages:
- Session,有没有必要使用它?(Fisher Li)
- Spring Security集成SSO单点登录
- Spring Security 3.1.x API document
- Wiki: Digest Access Authentication
- Wiki: Basic Access Authentication
- Amazon S3: Authenticating REST Request
- Stackoverflow: RESTful authentication
- Best Practices for securing a REST API / web service
- Stackoverflow: RESTful Authentication via Spring