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