Spring Secutity 自定義許可權配置
Srping Security網上也有很多例子,但基本都是所資源直接配置在XML檔案裡,限制太大,不夠靈活。我們需要的是可以在後臺修改資源訪問許可權,實時生效,才能符合現在大多數系統的需求。
需要引入的依賴
<!-- Spring security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.2.RELEASE</version>
</dependency >
<!--Spring Security end-->
使用者身份認證
我們自定義一個實現類MUserDetailsService 來實現UserDetailsService介面。
其中需要實現一個loadUserByUsername方法,用來讀取使用者的角色。
在這裡需要從資料庫中通過使用者名稱來查詢使用者的資訊和使用者所屬的角色
其中MGrantedAuthority實現了GrantedAuthority介面,用於構建使用者許可權。
MUserDeatils實現了UserDeatils介面,用於存放使用者資訊與許可權
UserDetailsService在身份認證中的作用
Spring Security中進行身份驗證的是AuthenticationManager介面,ProviderManager是它的一個預設實現,但它並不用來處理身份認證,而是委託給配置好的AuthenticationProvider,每個AuthenticationProvider會輪流檢查身份認證。檢查後或者返回Authentication物件或者丟擲異常。驗證身份就是載入響應的UserDetails,看看是否和使用者輸入的賬號、密碼、許可權等資訊匹配。此步驟由實現AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService驗證使用者名稱、密碼和授權)處理。包含 GrantedAuthority 的 UserDetails物件在構建 Authentication物件時填入資料。
MUserDetailsService.class中的loadUserByUsername方法
/**
* 根據使用者名稱載入使用者密碼與許可權資訊
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查詢使用者資訊
User user = userMapper.selectByName(username);
List<Role> roleList = null;
MUserDeatils userDeatils = null;
if (user != null){
//查詢使用者的角色
roleList = roleMapper.queryByUser(user.getId());
System.out.println("user" + user.getUsername() + "----" + user.getPassword());
// 構建許可權
Set<MGrantedAuthority> authorities = new HashSet<MGrantedAuthority>();
if (roleList.size() != 0){
for (Role role: roleList){
authorities.add(new MGrantedAuthority(role.getName()));
System.out.println(role.getName());
}
userDeatils = new MUserDeatils(user.getUsername(),user.getPassword(),authorities);
}
}
return userDeatils;
}
public class MGrantedAuthority implements GrantedAuthority {
private String authority;
public MGrantedAuthority(String authority){
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
}
MUserDeatils.class
實現UserDetails介面定義好變數即可
讀取資源與所屬角色
需要自定義實現類實現FilterInvocationSecurityMetadataSource介面。通過loadResourceDefine方法可以實現資源與許可權的對應關係。
要使我們自定義的MFilterInvocationSecurityMetadataSource生效,我們還需要定義一個MyFilterSecurityInterceptor類。
這裡的資料需要從資料庫中取得。另外自定義介面UrlMatcher,實現類為AntUrlPathMatcher。
坑
網上有教程是把loadResourceDefine方法放在了建構函式裡。但我經過多次試驗均出現service,mapper無法注入的問題,然後就會報一個空指標的導異常,經debug發現是service沒有注入。經多次查詢得知:原因是構造方法會先於注入執行,所以loadResourceDefine方法放入構造中執行時函式內的service與mapper還未執行注入,因此報 java.lang.NullPointerException的異常。解決方法是將loadResourceDefine方法放在getAttributes方法中執行。
MFilterInvocationSecurityMetadataSource.class
@Component
public class MFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
public IRescAndRoleService iRescAndRoleService ;
@Autowired
private IUserService iUserService ;
private UrlMatcher urlMatcher = new AntUrlPathMatcher();
// 資源許可權集合
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
public void loadResourceDefine(){
resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
//取得使用者資訊
List<User> userList = iUserService.query();
//取得資源與角色列表
List<RescAndRole> resourceList = iRescAndRoleService.query();
System.out.println(resourceList);
for (RescAndRole resource : resourceList) {
Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();
atts.add(new SecurityConfig(resource.getRoleName() ));
resourceMap.put(resource.getResString(), atts);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
loadResourceDefine();//防止無法注入問題
// guess object is a URL.
String url = ((FilterInvocation) o).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;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
AntUrlPathMatcher.class
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 + "']";
}
}
MyFilterSecurityInterceptor.class
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 {
}
}
決策管理器
自定義一個決策管理器MyAccessDecisionManager實現AccessDecisionManager介面。其中的decide方法,決定某一個使用者是否有許可權訪問某個url
/* (non-Javadoc)
* @see org.springframework.security.access.AccessDecisionManager#decide(org.springframework.security.core.Authentication, java.lang.Object, java.util.Collection)
* 該方法決定該許可權是否有許可權訪問該資源,其實object就是一個資源的地址,authentication是當前使用者的
* 對應許可權,如果沒登陸就為遊客,登陸了就是該使用者對應的許可權
*/
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if(configAttributes == null) {
return;
}
System.out.println(object.toString()); // object is a URL.
//所請求的資源擁有的許可權(一個資源對多個許可權)
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while(iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
//訪問所請求資源所需要的許可權
String needPermission = configAttribute.getAttribute();
System.out.println("訪問"+object.toString()+"需要的許可權是:" + needPermission);
//使用者所擁有的許可權authentication
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for(GrantedAuthority ga : authorities) {
if(needPermission.equals(ga.getAuthority())) {
return;
}
}
}
//沒有許可權
throw new AccessDeniedException(" 沒有許可權訪問! ");
}
@Override
public boolean supports(ConfigAttribute attribute) {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return true;
}
配置XML
web.xml
新增Seucrity的過濾器,將攔截所有資源訪問
注意
只能配置成 /*
<!--載入Security配置檔案與mybatis配置檔案-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INF/config/security.xml
WEB-INF/config/spring-mybatis.xml
</param-value>
</context-param>
<!-- spring security 的過濾器配置 -->
<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-security.xml
<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.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!--登陸頁面不驗證-->
<http pattern="/userLogin.html" security="none" />
<!--靜態檔案請求不驗證-->
<http pattern="/js/**" security="none" />
<http pattern="/css/**" security="none" />
<!--restful請求-->
<http pattern="/login" security="none" />
<http pattern="/getGrid" security="none" />
<!--瀏覽器會自動請求網站圖示:favicon.ico -不驗證 -->
<http pattern="/favicon.ico" security="none" />
<http >
<!--自定義許可權不足時顯示的頁面-->
<access-denied-handler error-page="/accessHint.html"></access-denied-handler>
<!-- 自定義登入介面 -->
<form-login
authentication-failure-url="/userLogin.html?error=true"
login-page="/userLogin.html"
default-target-url="/index.html"
login-processing-url="/j_spring_security_check" />
<logout invalidate-session="true"
logout-success-url="/userLogin.html"
logout-url="/j_spring_security_logout"/>
<!-- 通過配置custom-filter來增加過濾器,before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity預設的過濾器之前執行。 -->
<custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" />
<csrf disabled="true" />
</http>
<!-- 認證過濾器 -->
<b:bean id="filterSecurityInterceptor"
class="com.hand.security.utils.MyFilterSecurityInterceptor">
<b:property name="rejectPublicInvocations" value="true"/>
<!-- 使用者擁有的許可權 -->
<b:property name="accessDecisionManager" ref="accessDecisionManager" />
<!-- 使用者是否擁有所請求資源的許可權 -->
<b:property name="authenticationManager" ref="authenticationManager" />
<!-- 資源與許可權對應關係 -->
<b:property name="securityMetadataSource" ref="securityMetadataSource" />
</b:bean>
<!-- 2、更改驗證資訊載入方式 -->
<authentication-manager alias="authenticationManager">
<authentication-provider
user-service-ref="mUserDetailsService">
<!--如果使用者的密碼採用加密的話 <password-encoder hash="md5" /> -->
</authentication-provider>
</authentication-manager>
<!-- 1、配置自定義類MUserDetailsService -->
<b:bean id="mUserDetailsService" class="com.hand.security.service.impl.MUserDetailsService" />
<!--訪問決策器,決定某個使用者具有的角色,是否有足夠的許可權去訪問某個資源 -->
<b:bean id="accessDecisionManager" class="com.hand.security.utils.MyAccessDecisionManager"></b:bean>
<!--資源源資料定義,將所有的資源和許可權對應關係建立起來,即定義某一資源可以被哪些角色訪問 -->
<b:bean id="securityMetadataSource" class="com.hand.security.utils.MFilterInvocationSecurityMetadataSource" ></b:bean>
</b:beans>