spring security使用的方式:1.全配置方式。2.使用數據庫。3.使用過濾器,經過插入filter來靈活的運用。4.改源碼。前端
用戶受權管理流程:攔截器AbstractSecurityInterceptor會攔截請求url,經過調用FilterInvocationSecurityMetadataSource來得到此url所需的所有權限,而後受權管理器AccessDecisionManager會被調用。它會經過spring的全局緩存SecurityContextHolder獲取用戶的權限信息,還會獲取被攔截的url和被攔截url所需的所有權限,而後根據所配的策略(有:一票決定,一票否認,少數服從多數等),若是權限足夠,則返回,權限不夠則報錯並調用權限不足頁面。java
spring security實現:web
web.xmlspring
<?
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"
>
tomcat
<!--加載Spring XML配置文件 -->
安全
<
context-param
>
session
<
param-name
>contextConfigLocation</
param-name
>
<
param-value
> classpath:securityConfig.xml </
param-value
>
</
context-param
>
<!-- Spring Secutiry3.1的過濾器鏈配置 -->
<
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
>
<!-- Spring 容器啓動監聽器 -->
<
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"
/>
<!--訪問/admin.jsp資源的用戶必須具備ROLE_ADMIN的權限 -->
<!-- <intercept-url pattern=
"/admin.jsp"
access=
"ROLE_ADMIN"
/> -->
<!--訪問/**資源的用戶必須具備ROLE_USER的權限 -->
<!-- <intercept-url pattern=
"/**"
access=
"ROLE_USER"
/> -->
<session-management>
<concurrency-control max-sessions=
"1"
error-
if
-maximum-exceeded=
"false"
/>
</session-management>
<!--增長一個filter,這點與 Acegi是不同的,不能修改默認的filter了, 這個filter位於FILTER_SECURITY_INTERCEPTOR以前 -->
<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>
<!--驗證配置,認證管理器,實現用戶認證的入口,主要實現UserDetailsService接口便可 -->
<authentication-manager alias=
"authenticationManager"
>
<authentication-provider user-service-ref=
"myUserDetailService"
>
<!--若是用戶的密碼採用加密的話 <password-encoder hash=
"md5"
/> -->
</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>
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 {
//登錄驗證時,經過username獲取用戶的全部權限信息,
//並返回User放到spring的全局緩存SecurityContextHolder中,以供受權器使用
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提供的,必須實現的。
經過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 {
//fi裏面有一個被攔截的url
//裏面調用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的全部權限
//再調用MyAccessDecisionManager的decide方法來校驗用戶的權限是否足夠
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是必須的。
中fi斷點顯示是一個url(可能重寫了toString方法吧,可是裏面還有一些方法的),最重要的是beforeInvocation這個方法,它首先會調用MyInvocationSecurityMetadataSource類的getAttributes方法獲取被攔截url所需的權限,在調用MyAccessDecisionManager類decide方法判斷用戶是否夠權限。弄完這一切就會執行下一個攔截器。
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
;
//tomcat啓動時實例化一次
public
MyInvocationSecurityMetadataSource() {
loadResourceDefine();
}
//tomcat開啓時加載一次,加載全部url和權限(或角色)的對應關係
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);
}
//參數是要訪問的url,返回這個url對於的全部權限(或角色)
public
Collection<ConfigAttribute> getAttributes(Object object)
throws
IllegalArgumentException {
// 將參數轉爲url
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
;
}
}
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 {
//檢查用戶是否夠權限訪問資源
//參數authentication是從spring的全局緩存SecurityContextHolder中拿到的,裏面是用戶的權限信息
//參數object是url
//參數configAttributes所需的權限
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
;
}
}
}
//注意:執行這裏,後臺是會拋異常的,可是界面會跳轉到所配的access-denied-page頁面
throw
new
AccessDeniedException(
"no right"
);
}
public
boolean
supports(ConfigAttribute attribute) {
return
true
;
}
public
boolean
supports(Class<?>clazz) {
return
true
;
}
}
接口AccessDecisionManager也是必須實現的。
decide方法裏面寫的就是受權策略了,筆者的實現是,沒有明說須要權限的(即沒有對應的權限的資源),能夠訪問,用戶具備其中一個或多個以上的權限的能夠訪問。這個就看需求了,須要什麼策略,讀者能夠本身寫其中的策略邏輯。經過就返回,不經過拋異常就好了,spring security會自動跳到權限不足頁面(配置文件上配的)。
就這樣,整個流程過了一遍。
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 Map<String, Collection<ConfigAttribute>> resourceMap = null;
//將全部的角色和url的對應關係緩存起來
private
static
List<RoleUrlResource> rus =
null
;
private
IRoleUrlResourceDao roleUrlDao;
//tomcat啓動時實例化一次
public
MyInvocationSecurityMetadataSource() {
// loadResourceDefine();
}
//tomcat開啓時加載一次,加載全部url和權限(或角色)的對應關係
/*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);
} */
//參數是要訪問的url,返回這個url對於的全部權限(或角色)
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 將參數轉爲url
String url = ((FilterInvocation)object).getRequestUrl();
//查詢全部的url和角色的對應關係
if(rus == null){
rus = roleUrlDao.findAll();
}
//匹配全部的url,並對角色去重
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;
/*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
;
}
}