spring security使用分類:
如何使用spring security,相信百度過的都知道,總共有四種用法,從簡到深爲:1、不用數據庫,全部數據寫在配置文件,這個也是官方文檔裏面的demo;2、使用數據庫,根據spring security默認實現代碼設計數據庫,也就是說數據庫已經固定了,這種方法不靈活,而且那個數據庫設計得很簡陋,實用性差;3、spring
security和Acegi不同,它不能修改默認filter了,但支持插入filter,所以根據這個,我們可以插入自己的filter來靈活使用;4、暴力手段,修改源碼,前面說的修改默認filter只是修改配置文件以替換filter而已,這種是直接改了裏面的源碼,但是這種不符合OO設計原則,而且不實際,不可用。
本文面向讀者:
因爲本文準備介紹第三種方法,所以面向的讀者是已經具備了spring security基礎知識的。不過不要緊,讀者可以先看一下
這個教程,看完應該可以使用第二種方法開發了。
spring security的簡單原理:
使用衆多的攔截器對url攔截,以此來管理權限。但是這麼多攔截器,筆者不可能對其一一來講,主要講裏面核心流程的兩個。
首先,權限管理離不開登陸驗證的,所以登陸驗證攔截器AuthenticationProcessingFilter要講;
還有就是對訪問的資源管理吧,所以資源管理攔截器AbstractSecurityInterceptor要講;
但攔截器裏面的實現需要一些組件來實現,所以就有了AuthenticationManager、accessDecisionManager等組件來支撐。
現在先大概過一遍整個流程,用戶登陸,會被AuthenticationProcessingFilter攔截,調用AuthenticationManager的實現,而且AuthenticationManager會調用ProviderManager來獲取用戶驗證信息(不同的Provider調用的服務不同,因爲這些信息可以是在數據庫上,可以是在LDAP服務器上,可以是xml配置文件上等),如果驗證通過後會將用戶的權限信息封裝一個User放到spring的全局緩存SecurityContextHolder中,以備後面訪問資源時使用。
訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截,其中會調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,在調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的全部權限,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。
雖然講得好像好複雜,讀者們可能有點暈,不過不打緊,真正通過代碼的講解在後面,讀者可以看完後面的代碼實現,再返回看這個簡單的原理,可能會有不錯的收穫。
javaEE的入口:web.xml:
-
<?xml version="1.0" encoding="UTF-8"?>
-
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
-
-
<context-param>
-
<param-name>contextConfigLocation</param-name>
-
<param-value> classpath:securityConfig.xml </param-value>
-
</context-param>
-
-
<filter>
-
<filter-name>springSecurityFilterChain</filter-name>
-
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
-
</filter>
-
<filter-mapping>
-
<filter-name>springSecurityFilterChain</filter-name>
-
<url-pattern>/*</url-pattern>
-
</filter-mapping>
-
-
<listener>
-
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
-
</listener>
-
-
<welcome-file-list>
-
<welcome-file>index.jsp</welcome-file>
-
</welcome-file-list>
-
</web-app>
上面那個配置不用多說了吧
直接上spring security的配置文件securityConfig.xml:
-
<?xml version="1.0" encoding="UTF-8"?>
-
<b:beans xmlns="http://www.springframework.org/schema/security"
-
xmlns:b="http://www.springframework.org/schema/beans"
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
-
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
-
-
-
<http pattern="/login.jsp" security="none" />
-
<http access-denied-page="/accessDenied.jsp">
-
<form-login login-page="/login.jsp" />
-
-
-
-
-
<session-management>
-
<concurrency-control max-sessions="1"
-
error-if-maximum-exceeded="false" />
-
</session-management>
-
-
<custom-filter ref="myFilter" before="FILTER_SECURITY_INTERCEPTOR" />
-
</http>
-
<!--一個自定義的filter,必須包含 authenticationManager,accessDecisionManager,securityMetadataSource三個屬性,
-
我們的所有控制將在這三個類中實現,解釋詳見具體配置 -->
-
<b:bean id="myFilter"
-
class="com.erdangjiade.spring.security.MyFilterSecurityInterceptor">
-
<b:property name="authenticationManager" ref="authenticationManager" />
-
<b:property name="accessDecisionManager" ref="myAccessDecisionManagerBean" />
-
<b:property name="securityMetadataSource" ref="securityMetadataSource" />
-
</b:bean>
-
-
<authentication-manager alias="authenticationManager">
-
<authentication-provider user-service-ref="myUserDetailService">
-
-
</authentication-provider>
-
</authentication-manager>
-
-
<b:bean id="myUserDetailService" class="com.erdangjiade.spring.security.MyUserDetailService" />
-
-
<b:bean id="myAccessDecisionManagerBean"
-
class="com.erdangjiade.spring.security.MyAccessDecisionManager">
-
</b:bean>
-
-
<b:bean id="securityMetadataSource"
-
class="com.erdangjiade.spring.security.MyInvocationSecurityMetadataSource" />
-
-
</b:beans>
其實所有配置都在<http></http>裏面,首先這個版本的spring security不支持了filter=none的配置了,改成了獨立的<http pattern="/login.jsp" security="none"/>,裏面你可以配登陸頁面、權限不足的返回頁面、註銷頁面等,上面那些配置,我
註銷了一些資源和權限的對應關係,筆者這裏不需要在這配死它,可以自己寫攔截器來獲得資源與權限的對應關係。
session-management是用來防止多個用戶同時登陸一個賬號的。
最重要的是筆者自己寫的攔截器myFilter(終於講到重點了),首先這個攔截器會加載在FILTER_SECURITY_INTERCEPTOR之前(配置文件上有說),最主要的是這個攔截器裏面配了三個處理類,第一個是authenticationManager,這個是處理驗證的,這裏需要特別說明的是:這個類不單隻這個攔截器用到,還有驗證攔截器AuthenticationProcessingFilter也用到
了,而且實際上的登陸驗證也是AuthenticationProcessingFilter攔截器調用authenticationManager來處理的,我們這個攔截器只是爲了拿到驗證用戶信息而已(這裏不太清楚,因爲authenticationManager筆者設了斷點,用戶登陸後再也沒調用這個類了,而且調用這個類時不是筆者自己寫的那個攔截器調用的,看了spring技術內幕這本書才知道是AuthenticationProcessingFilter攔截器調用的)。
securityMetadataSource這個用來加載資源與權限的全部對應關係的,並提供一個通過資源獲取所有權限的方法。
accessDecisionManager這個也稱爲授權器,通過登錄用戶的權限信息、資源、獲取資源所需的權限來根據不同的授權策略來判斷用戶是否有權限訪問資源。
authenticationManager類可以有許多provider(提供者)提供用戶驗證信息,這裏筆者自己寫了一個類myUserDetailService來獲取用戶信息。
MyUserDetailService:
-
package com.erdangjiade.spring.security;
-
-
import java.util.ArrayList;
-
import java.util.Collection;
-
-
import org.springframework.dao.DataAccessException;
-
import org.springframework.security.core.GrantedAuthority;
-
import org.springframework.security.core.authority.GrantedAuthorityImpl;
-
import org.springframework.security.core.userdetails.User;
-
import org.springframework.security.core.userdetails.UserDetails;
-
import org.springframework.security.core.userdetails.UserDetailsService;
-
import org.springframework.security.core.userdetails.UsernameNotFoundException;
-
-
public class MyUserDetailService implements UserDetailsService {
-
-
-
-
public UserDetails loadUserByUsername(String username)
-
throws UsernameNotFoundException, DataAccessException {
-
Collection<GrantedAuthority> auths=new ArrayList<GrantedAuthority>();
-
-
GrantedAuthorityImpl auth2=new GrantedAuthorityImpl("ROLE_ADMIN");
-
GrantedAuthorityImpl auth1=new GrantedAuthorityImpl("ROLE_USER");
-
-
if(username.equals("lcy")){
-
auths=new ArrayList<GrantedAuthority>();
-
auths.add(auth1);
-
auths.add(auth2);
-
}
-
-
User user = new User(username, "lcy", true, true, true, true, auths);
-
return user;
-
}
-
}
其中UserDetailsService接口是spring提供的,必須實現的。別看這個類只有一個方法,而且這麼簡單,其中內涵玄機。
讀者看到這裏可能就大感疑惑了,不是說好的用數據庫嗎?對,但別急,等筆者慢慢給你們解析。
首先,筆者爲什麼不用數據庫,還不是爲了讀者們測試方便,並簡化spring security的流程,讓讀者抓住主線,而不是還要煩其他事(導入數據庫,配置數據庫,寫dao等)。
這裏筆者只是用幾個數據模擬了從數據庫中拿到的數據,也就是說ROLE_ADMIN、ROLE_USER、lcy(第一個是登陸賬號)、lcy(第二個是密碼)是從數據庫拿出來的,這個不難實現吧,如果需要數據庫時,讀者可以用自己寫的dao通過參數username來查詢出這個用戶的權限信息(或是角色信息,就是那個ROLE_*,對必須是ROLE_開頭的,不然spring security不認賬的,其實是spring security裏面做了一個判斷,必須要ROLE_開頭,讀者可以百度改一下),再返回spring自帶的數據模型User即可。
這個寫應該比較清晰、靈活吧,總之數據讀者們通過什麼方法獲取都行,只要返回一個User對象就行了。(這也是筆者爲什麼要重寫這個類的原因)
通過MyUserDetailService拿到用戶信息後,authenticationManager對比用戶的密碼(即驗證用戶),然後這個AuthenticationProcessingFilter攔截器就過咯。
下面要說的是另外一個攔截器,就是筆者自己寫的攔截器MyFilterSecurityInterceptor:
-
package com.erdangjiade.spring.security;
-
-
import java.io.IOException;
-
-
import javax.servlet.Filter;
-
import javax.servlet.FilterChain;
-
import javax.servlet.FilterConfig;
-
import javax.servlet.ServletException;
-
import javax.servlet.ServletRequest;
-
import javax.servlet.ServletResponse;
-
-
import org.springframework.security.access.SecurityMetadataSource;
-
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
-
import org.springframework.security.access.intercept.InterceptorStatusToken;
-
import org.springframework.security.web.FilterInvocation;
-
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
-
-
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
-
-
-
private FilterInvocationSecurityMetadataSource securityMetadataSource;
-
-
-
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
-
FilterInvocation fi = new FilterInvocation(request, response, chain);
-
invoke(fi);
-
}
-
-
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
-
return this.securityMetadataSource;
-
}
-
-
public Class<? extends Object> getSecureObjectClass() {
-
return FilterInvocation.class;
-
}
-
-
public void invoke(FilterInvocation fi) throws IOException, ServletException {
-
-
-
-
InterceptorStatusToken token = super.beforeInvocation(fi);
-
try {
-
-
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
-
} finally {
-
super.afterInvocation(token, null);
-
}
-
}
-
public SecurityMetadataSource obtainSecurityMetadataSource() {
-
return this.securityMetadataSource;
-
}
-
public void setSecurityMetadataSource(
-
FilterInvocationSecurityMetadataSource newSource)
-
{
-
this.securityMetadataSource = newSource;
-
}
-
public void destroy() {
-
-
}
-
public void init(FilterConfig arg0) throws ServletException {
-
-
}
-
}
繼承AbstractSecurityInterceptor、實現Filter是必須的。
首先,登陸後,每次訪問資源都會被這個攔截器攔截,會執行doFilter這個方法,這個方法調用了invoke方法,其中fi斷點顯示是一個url(可能重寫了toString方法吧,但是裏面還有一些方法的),最重要的是beforeInvocation這個方法,它首先會調用MyInvocationSecurityMetadataSource類的getAttributes方法獲取被攔截url所需的權限,在調用MyAccessDecisionManager類decide方法判斷用戶是否夠權限。弄完這一切就會執行下一個攔截器。
再看一下這個MyInvocationSecurityMetadataSource的實現:
-
package com.erdangjiade.spring.security;
-
-
import java.util.ArrayList;
-
import java.util.Collection;
-
import java.util.HashMap;
-
import java.util.Iterator;
-
import java.util.Map;
-
-
import org.springframework.security.access.ConfigAttribute;
-
import org.springframework.security.access.SecurityConfig;
-
import org.springframework.security.web.FilterInvocation;
-
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
-
-
import com.erdangjiade.spring.security.tool.AntUrlPathMatcher;
-
import com.erdangjiade.spring.security.tool.UrlMatcher;
-
-
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
-
private UrlMatcher urlMatcher = new AntUrlPathMatcher();
-
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
-
-
-
public MyInvocationSecurityMetadataSource() {
-
loadResourceDefine();
-
}
-
-
private void loadResourceDefine() {
-
resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
-
Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();
-
ConfigAttribute ca = new SecurityConfig("ROLE_USER");
-
atts.add(ca);
-
resourceMap.put("/index.jsp", atts);
-
Collection<ConfigAttribute> attsno =new ArrayList<ConfigAttribute>();
-
ConfigAttribute cano = new SecurityConfig("ROLE_NO");
-
attsno.add(cano);
-
resourceMap.put("/other.jsp", attsno);
-
}
-
-
-
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
-
-
String url = ((FilterInvocation)object).getRequestUrl();
-
Iterator<String>ite = resourceMap.keySet().iterator();
-
while (ite.hasNext()) {
-
String resURL = ite.next();
-
if (urlMatcher.pathMatchesUrl(resURL, url)) {
-
return resourceMap.get(resURL);
-
}
-
}
-
return null;
-
}
-
public boolean supports(Class<?>clazz) {
-
return true;
-
}
-
public Collection<ConfigAttribute> getAllConfigAttributes() {
-
return null;
-
}
-
}
實現FilterInvocationSecurityMetadataSource接口也是必須的。
首先,這裏也是模擬了從數據庫中獲取信息。
其中loadResourceDefine方法不是必須的,這個只是加載所有的資源與權限的對應關係並緩存起來,避免每次獲取權限都訪問數據庫(提高性能),然後getAttributes根據參數(被攔截url)返回權限集合。
這種緩存的實現其實有一個缺點,因爲loadResourceDefine方法是放在構造器上調用的,而這個類的實例化只在web服務器啓動時調用一次,那就是說loadResourceDefine方法只會調用一次,如果資源和權限的對應關係在啓動後發生了改變,那麼緩存起來的就是髒數據,而筆者這裏使用的就是緩存數據,那就會授權錯誤了。但如果資源和權限對應關係是不會改變的,這種方法性能會好很多。
現在說回有數據庫的靈活實現,讀者看到這,可能會說,這還不簡單,和上面MyUserDetailService類一樣使用dao靈活獲取數據就行啦。
如果讀者這樣想,那只想到了一半,想一下spring的機制(依賴注入),dao需要依賴注入吧,但這是在啓動時候,那個dao可能都還沒加載,所以這裏需要讀者自己寫sessionFactory,自己寫hql或sql,對,就在loadResourceDefine方法裏面寫(這個應該會寫吧,基礎來的)。那如果說想用第二種方法呢(就是允許資源和權限的對應關係改變的那個),那更加簡單,根本不需要loadResourceDefine方法了,直接在getAttributes方法裏面調用dao(這個是加載完,後來纔會調用的,所以可以使用dao),通過被攔截url獲取數據庫中的所有權限,封裝成Collection<ConfigAttribute>返回就行了。(靈活、簡單)
注意:接口UrlMatcher和實現類AntUrlPathMatcher是筆者自己寫的,這本來是spring以前版本有的,現在沒有了,但是覺得好用就用會來了,直接上代碼(讀者也可以自己寫正則表達式驗證被攔截url和緩存或數據庫的url是否匹配):
-
package com.erdangjiade.spring.security.tool;
-
-
public interface UrlMatcher{
-
Object compile(String paramString);
-
boolean pathMatchesUrl(Object paramObject, String paramString);
-
String getUniversalMatchPattern();
-
boolean requiresLowerCaseUrl();
-
}
-
package com.erdangjiade.spring.security.tool;
-
import org.springframework.util.AntPathMatcher;
-
import org.springframework.util.PathMatcher;
-
-
public class AntUrlPathMatcher implements UrlMatcher {
-
private boolean requiresLowerCaseUrl;
-
private PathMatcher pathMatcher;
-
public AntUrlPathMatcher() {
-
this(true);
-
-
}
-
public AntUrlPathMatcher(boolean requiresLowerCaseUrl)
-
{
-
this.requiresLowerCaseUrl = true;
-
this.pathMatcher = new AntPathMatcher();
-
this.requiresLowerCaseUrl = requiresLowerCaseUrl;
-
}
-
-
public Object compile(String path) {
-
if (this.requiresLowerCaseUrl) {
-
return path.toLowerCase();
-
}
-
return path;
-
}
-
-
public void setRequiresLowerCaseUrl(boolean requiresLowerCaseUrl){
-
-
this.requiresLowerCaseUrl = requiresLowerCaseUrl;
-
}
-
-
public boolean pathMatchesUrl(Object path, String url) {
-
if (("/**".equals(path)) || ("**".equals(path))) {
-
return true;
-
}
-
-
return this.pathMatcher.match((String)path, url);
-
}
-
-
public String getUniversalMatchPattern() {
-
return"/**";
-
}
-
-
public boolean requiresLowerCaseUrl() {
-
return this.requiresLowerCaseUrl;
-
}
-
-
public String toString() {
-
return super.getClass().getName() + "[requiresLowerCase='"
-
+ this.requiresLowerCaseUrl + "']";
-
}
-
}
然後MyAccessDecisionManager類的實現:
-
package com.erdangjiade.spring.security;
-
-
import java.util.Collection;
-
import java.util.Iterator;
-
-
import org.springframework.security.access.AccessDecisionManager;
-
import org.springframework.security.access.AccessDeniedException;
-
import org.springframework.security.access.ConfigAttribute;
-
import org.springframework.security.access.SecurityConfig;
-
import org.springframework.security.authentication.InsufficientAuthenticationException;
-
import org.springframework.security.core.Authentication;
-
import org.springframework.security.core.GrantedAuthority;
-
-
public class MyAccessDecisionManager implements AccessDecisionManager {
-
-
-
-
-
-
public void decide(Authentication authentication, Object object,
-
Collection<ConfigAttribute> configAttributes)
-
throws AccessDeniedException, InsufficientAuthenticationException {
-
if(configAttributes == null){
-
return;
-
}
-
-
Iterator<ConfigAttribute> ite=configAttributes.iterator();
-
while(ite.hasNext()){
-
ConfigAttribute ca=ite.next();
-
String needRole=((SecurityConfig)ca).getAttribute();
-
for(GrantedAuthority ga : authentication.getAuthorities()){
-
if(needRole.equals(ga.getAuthority())){
-
-
return;
-
}
-
}
-
}
-
-
throw new AccessDeniedException("no right");
-
}
-
public boolean supports(ConfigAttribute attribute) {
-
return true;
-
}
-
public boolean supports(Class<?>clazz) {
-
return true;
-
}
-
}
接口AccessDecisionManager也是必須實現的。
decide方法裏面寫的就是授權策略了,筆者的實現是,沒有明說需要權限的(即沒有對應的權限的資源),可以訪問,用戶具有其中一個或多個以上的權限的可以訪問。這個就看需求了,需要什麼策略,讀者可以自己寫其中的策略邏輯。通過就返回,不通過拋異常就行了,spring security會自動跳到權限不足頁面(配置文件上配的)。
就這樣,整個流程過了一遍。
剩下的頁面代碼
本來想給這個demo的源碼出來的,但是筆者覺得,通過這個教程一步一步讀下來,並自己敲一遍代碼,會比直接運行一遍demo印象更深刻,並且更容易理解裏面的原理。
而且我的源碼其實都公佈出來了:
login.jsp:
-
<%@page language="java" import="java.util.*" pageEncoding="UTF-8"%>
-
<!DOCTYPEhtmlPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
-
<html>
-
<head>
-
<title>登錄</title>
-
</head>
-
<body>
-
<form action ="j_spring_security_check" method="POST">
-
<table>
-
<tr>
-
<td>用戶:</td>
-
<td><input type ='text' name='j_username'></td>
-
</tr>
-
<tr>
-
<td>密碼:</td>
-
<td><input type ='password' name='j_password'></td>
-
</tr>
-
<tr>
-
<td><input name ="reset" type="reset"></td>
-
<td><input name ="submit" type="submit"></td>
-
</tr>
-
</table>
-
</form>
-
</body>
-
</html>
index.jsp:
-
<%@page language="java" import="java.util.*" pageEncoding="UTF-8"%>
-
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
-
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
-
-
<html>
-
-
<head>
-
-
<title>My JSP 'index.jsp' starting page</title>
-
</head>
-
-
<body>
-
<h3>這是首頁</h3>歡迎
-
<sec:authentication property ="name"/> !
-
-
<br>
-
<a href="admin.jsp">進入admin頁面</a>
-
<a href="other.jsp">進入其它頁面</a>
-
</body>
-
-
-
</html>
-
admin.jsp:
-
<%@page language="java" import="java.util.*" pageEncoding="utf-8"%>
-
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
-
<html>
-
<head>
-
<title>My JSP 'admin.jsp' starting page</title>
-
</head>
-
<body>
-
歡迎來到管理員頁面.
-
<br>
-
</body>
-
</html>
accessDenied.jsp:
-
<%@page language="java" import="java.util.*" pageEncoding="utf-8"%>
-
<!DOCTYPEHTMLPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN">
-
<html>
-
<head>
-
<title>My JSP 'admin.jsp' starting page</title>
-
</head>
-
<body>
-
歡迎來到管理員頁面.
-
<br>
-
</body>
-
</html>
other.jsp:
-
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
-
<%
-
String path = request.getContextPath();
-
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
-
%>
-
-
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-
<html>
-
<head>
-
<base href="<%=basePath%>">
-
-
<title>My JSP 'other.jsp' starting page</title>
-
-
<meta http-equiv="pragma" content="no-cache">
-
<meta http-equiv="cache-control" content="no-cache">
-
<meta http-equiv="expires" content="0">
-
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
-
<meta http-equiv="description" content="This is my page">
-
-
-
-
-
</head>
-
-
<body>
-
<h3>這裏是Other頁面</h3>
-
</body>
-
</html>
項目圖:
最後的話:
雖然筆者沒給讀者們demo,但是所有源碼和jar包都在這個教程裏面,爲什麼不直接給?筆者的目的是讓讀者跟着教程敲一遍代碼,使印象深刻(相信做這行的都知道,同樣一段代碼,看過和敲過的區別是多麼的大),所以不惜如此來強迫大家了。
由於筆者有經常上csdn博客的習慣,所以讀者有什麼不懂的(或者指教的),筆者盡力解答。
補充:
(2014年11月21日第一次補充):
第一點:
MyUserDetailService這個類負責的是隻是獲取登陸用戶的詳細信息(包括密碼、角色等),不負責和前端傳過來的密碼對比,只需返回User對象,後會有其他類根據User對象對比密碼的正確性(框架幫我們做)。
第二點:
記得MyInvocationSecurityMetadataSource這個類是負責的是獲取角色與url資源的所有對應關係,並根據url查詢對應的所有角色。
今天爲一個項目搭安全架構時,第一,發現上面MyInvocationSecurityMetadataSource這個類的代碼有個bug:
上面的代碼中,將所有的對應關係緩存到resourceMap,key是url,value是這個url對應所有角色。
getAttributes方法中,只要匹配到一個url就返回這個url對應所有角色,不再匹配後面的url,問題來了,當url有交集時,就有可能漏掉一些角色了:如有兩個 url ,第一個是 /** ,第二個是 /role1/index.jsp ,第一個當然需要很高的權限了(因爲能匹配所有 url ,即可以訪問所有 url
),假設它需要的角色是 ROLE_ADMIN (不是一般人擁有的),第二個所需的角色是 ROLE_1 。 當我用 ROLE_1 這個角色訪問 /role1/index.jsp 時,在getAttributes方法中,當先迭代了 /** 這個url,它就能匹配 /role1/index.jsp 這個url,並直接返回 /** 這個url對應的所有角色(在這,也就ROLE_ADMIN)給MyAccessDecisionManager這個投票類, MyAccessDecisionManager這個類中再對比
用戶的角色 ROLE_1 ,就會發現不匹配。 最後,明明可以有權訪問的 url ,卻不能訪問了。
第二,之前不是說緩存所有對應關係,需要讀者自己寫sessionFactory(因爲在實例化這個類時,配置的sessionFactory可能還沒實例化或dao還沒加載好),既然這樣,那筆者可以不在構造方法中加載對應關係,可以在第一次調用getAttributes方法時再加載(用靜態變量緩存起來,第二次就不用再加載了,
注:其實這樣不是很嚴謹,不過筆者這裏的對應關係是不變的,單例性不需很強,更嚴謹的請參考筆者另一篇博文
設計模式之單件模式)。
修改過的MyInvocationSecurityMetadataSource類:
-
package com.lcy.bookcrossing.springSecurity;
-
-
import java.util.ArrayList;
-
import java.util.Collection;
-
import java.util.HashMap;
-
import java.util.HashSet;
-
import java.util.Iterator;
-
import java.util.List;
-
import java.util.Map;
-
import java.util.Set;
-
-
import javax.annotation.Resource;
-
-
import org.springframework.security.access.ConfigAttribute;
-
import org.springframework.security.access.SecurityConfig;
-
import org.springframework.security.web.FilterInvocation;
-
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
-
-
import com.lcy.bookcrossing.bean.RoleUrlResource;
-
import com.lcy.bookcrossing.dao.IRoleUrlResourceDao;
-
import com.lcy.bookcrossing.springSecurity.tool.AntUrlPathMatcher;
-
import com.lcy.bookcrossing.springSecurity.tool.UrlMatcher;
-
-
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
-
private UrlMatcher urlMatcher = new AntUrlPathMatcher();
-
-
-
-
private static List<RoleUrlResource> rus = null;
-
-
@Resource
-
private IRoleUrlResourceDao roleUrlDao;
-
-
-
public MyInvocationSecurityMetadataSource() {
-
-
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
-
-
String url = ((FilterInvocation)object).getRequestUrl();
-
-
-
if(rus == null){
-
rus = roleUrlDao.findAll();
-
}
-
-
-
Set<String> roles = new HashSet<String>();
-
for(RoleUrlResource ru : rus){
-
if (urlMatcher.pathMatchesUrl(ru.getUrlResource().getUrl(), url)) {
-
roles.add(ru.getRole().getRoleName());
-
}
-
}
-
Collection<ConfigAttribute> cas = new ArrayList<ConfigAttribute>();
-
for(String role : roles){
-
ConfigAttribute ca = new SecurityConfig(role);
-
cas.add(ca);
-
}
-
return cas;
-
-
-
-
-
-
-
-
-
-
}
-
public boolean supports(Class<?>clazz) {
-
return true;
-
}
-
public Collection<ConfigAttribute> getAllConfigAttributes() {
-
return null;
-
}
-
}
以上代碼,在getAttributes方法中緩存起所有的對應關係(可以
使用依賴注入了),並匹配
所有 url ,對角色進行去重(因爲多個url可能有重複的角色),這樣就能修復那個bug了。