spring-security实现权限管理

首先需要配置好spring-security:[spring-security简单配置](http://blog.csdn.net/qq_28890731/article/details/51013366)

整体思路是将权限管理分为两部分:资源控制和页面展示控制,首先确保没有权限的资源不可访问,其次是将没权限操作的页面标签隐藏。

数据库涉及到6张表,分别是:
1,用户表;2,角色表;3,资源(权限)表;
4,用户-角色关联表;5,角色-资源关联表;6,用户-资源关联表
目标:可以直接给用户关联资源;也可以将资源封装给角色,然后将角色关联给用户。

spring-security配置文件(下面有配置文件中相关的bean):

<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="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.xsd">


    
    <http pattern="/static/**" security="none"/>
    <http pattern="/login" security="none"/>
    
    <http use-expressions="true" auto-config="true" entry-point-ref="authenticationProcessingFilterEntryPoint">
        
        <form-login login-page="/login" password-parameter="password" username-parameter="userName"
                    login-processing-url="/j_spring_security_check"
                    default-target-url="/index" always-use-default-target="true"
                    authentication-failure-handler-ref="aleiyeAuthenticationFailureHandler"
                    authentication-success-handler-ref="aleiyeAuthenticationSuccessHandler"/>
        <custom-filter ref="authenticationProcessingFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
        <custom-filter ref="loginFilter" after="FORM_LOGIN_FILTER"/>
        <logout invalidate-session="true" logout-success-url="/login" logout-url="/j_spring_security_logout"/>
        <session-management invalid-session-url="/login" session-authentication-error-url="/login"/>
        <csrf disabled="true"/>
    http>

    <beans:bean id="authenticationProcessingFilterEntryPoint"
                class="com.aleiye.web.system.security.filter.AleiyeAuthenticationEntryPoint">
        <beans:constructor-arg name="loginFormUrl" value="/login"/>
    beans:bean>
    
    <beans:bean id="aleiyeAuthenticationFailureHandler"
                class="com.aleiye.web.system.security.handler.AleiyeAuthenticationFailureHandler">
        <beans:constructor-arg name="defaultFailureUrl" value="/login"/>
    beans:bean>
    
    <beans:bean id="aleiyeAuthenticationSuccessHandler"
                class="com.aleiye.web.system.security.handler.AleiyeAuthenticationSuccessHandler">
        <beans:constructor-arg name="defaultTargetUrl" value="/index"/>
    beans:bean>
    
    <beans:bean id="authenticationProcessingFilter"
                class="com.aleiye.web.system.security.filter.AuthenticationProcessingFilter">
        
        <beans:property name="authenticationManager" ref="aleiyeAuthenticationManager"/>
        
        <beans:property name="accessDecisionManager" ref="aleiyeAccessDecisionManager"/>
        
        <beans:property name="securityMetadataSource" ref="aleiyeSecurityMetadataSource"/>
    beans:bean>
    
    <beans:bean id="loginFilter" class="com.aleiye.web.system.security.filter.AleiyeLoginFilter">
        <beans:constructor-arg name="loginUrl" value="/login"/>
        <beans:constructor-arg name="indexUrl" value="/index"/>
    beans:bean>
    
    <beans:bean id="aleiyeAccessDecisionManager"
                class="com.aleiye.web.system.security.filter.AleiyeAccessDecisionManager"/>
    <beans:bean id="aleiyeSecurityMetadataSource"
                class="com.aleiye.web.system.security.filter.AleiyeSecurityMetadataSource"/>
    
    <beans:bean id="daoUserProvider" class="com.aleiye.web.system.security.Provider.AleiyeAuthenticationProvider">
        <beans:property name="userDetailsService" ref="userDetailService" />
        <beans:property name="passwordEncoder" ref="md5PasswordEncoder" />
    beans:bean>
    
    <authentication-manager alias="aleiyeAuthenticationManager">
        <authentication-provider ref="daoUserProvider" />
    authentication-manager>
    <beans:bean id="md5PasswordEncoder"
                class="org.springframework.security.authentication.encoding.Md5PasswordEncoder"/>
    <beans:bean id="userDetailService" class="com.aleiye.web.system.security.service.UserDetailService"/>
beans:beans>

第一部分:资源控制主要包括4个java类:
1:UserDetailService,AleiyeAuthenticationProvider封装用户拥有的权限
目的:将该用户能关联到的所有资源id集合赋值给用户实体

package com.aleiye.web.system.security.service;

import com.aleiye.client.service.security.model.RUserStatusEnum;
import com.aleiye.web.system.security.entity.AleiyeUserDetails;
import com.aleiye.web.system.security.entity.SysFeatureSource;
import com.aleiye.web.system.user.entity.SecUserInfo;
import com.aleiye.web.system.user.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @project aleiye-web
 * @auth wentao.yu
 * @Date 16/1/9PM4:19
 */
public class UserDetailService implements UserDetailsService {

    @Autowired
    private IUserService userService;
    @Autowired
    private ISysFeatureSourceService sysFeatureSourceService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if (username != null && !username.isEmpty()) {
            SecUserInfo secUserInfo = userService.getUser(username);
            if (secUserInfo == null) {
                throw new UsernameNotFoundException("用户[" + username + "]不存在!");
            }
            //加载用户权限
            //此处特殊处理,对admin用户的权限为强制全部打开
            Collection grantedAuthorityCollection = new ArrayList<>();
            if (secUserInfo.getId() == 1) {
                List sourceList = sysFeatureSourceService.findAllEnableRecords();
                for (SysFeatureSource sysFeatureSource : sourceList) {
                    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(sysFeatureSource.getId().toString());
                    grantedAuthorityCollection.add(simpleGrantedAuthority);
                }
            } else {
                //非admin用户加载相应权限
                List sources = sysFeatureSourceService.findSourceByUserId(secUserInfo.getId());
                for(SysFeatureSource source:sources){
                    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(source.getId().toString());
                    grantedAuthorityCollection.add(simpleGrantedAuthority);
                }
            }
            AleiyeUserDetails ud = new AleiyeUserDetails(secUserInfo, secUserInfo.getStatus().intValue() == RUserStatusEnum.NORMAL.getValue(), grantedAuthorityCollection);
            return ud;
        }
        return null;
    }
}
package com.aleiye.web.system.security.Provider;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.authentication.dao.SaltSource;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;

/** 
* @author weiwentao: 
* @version 创建时间:2015年10月21日 下午5:47:03 
* 类说明 
*/
public class AleiyeAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    // ~ Static fields/initializers
    // =====================================================================================

    /**
     * The plaintext password used to perform
     * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is
     * not found to avoid SEC-2056.
     */
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    // ~ Instance fields
    // ================================================================================================

    private PasswordEncoder passwordEncoder;

    /**
     * The password used to perform
     * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is
     * not found to avoid SEC-2056. This is necessary, because some
     * {@link PasswordEncoder} implementations will short circuit if the password is not
     * in a valid format.
     */
    private String userNotFoundEncodedPassword;

    private SaltSource saltSource;

    private UserDetailsService userDetailsService;

    public AleiyeAuthenticationProvider() {
        setPasswordEncoder(new PlaintextPasswordEncoder());
    }

    // ~ Methods
    // ========================================================================================================

    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.isPasswordValid(userDetails.getPassword().toLowerCase(),
                presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        //成功之后,封装session
        authentication.setDetails(userDetails);
    }

    protected void doAfterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

    protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        catch (UsernameNotFoundException notFound) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                        presentedPassword, null);
            }
            throw notFound;
        }
        catch (Exception repositoryProblem) {
            throw new InternalAuthenticationServiceException(
                    repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }

    /**
     * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
     * not set, the password will be compared as plain text.
     * 

* For systems which are already using salted password which are encoded with a * previous release, the encoder should be of type * {@code org.springframework.security.authentication.encoding.PasswordEncoder}. * Otherwise, the recommended approach is to use * {@code org.springframework.security.crypto.password.PasswordEncoder}. * * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder} * types. */ public void setPasswordEncoder(Object passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); if (passwordEncoder instanceof PasswordEncoder) { setPasswordEncoder((PasswordEncoder) passwordEncoder); return; } if (passwordEncoder instanceof org.springframework.security.crypto.password.PasswordEncoder) { final org.springframework.security.crypto.password.PasswordEncoder delegate = (org.springframework.security.crypto.password.PasswordEncoder) passwordEncoder; setPasswordEncoder(new PasswordEncoder() { public String encodePassword(String rawPass, Object salt) { checkSalt(salt); return delegate.encode(rawPass); } public boolean isPasswordValid(String encPass, String rawPass, Object salt) { checkSalt(salt); return delegate.matches(rawPass, encPass); } private void checkSalt(Object salt) { Assert.isNull(salt, "Salt value must be null when used with crypto module PasswordEncoder"); } }); return; } throw new IllegalArgumentException( "passwordEncoder must be a PasswordEncoder instance"); } private void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); this.userNotFoundEncodedPassword = passwordEncoder.encodePassword( USER_NOT_FOUND_PASSWORD, null); this.passwordEncoder = passwordEncoder; } protected PasswordEncoder getPasswordEncoder() { return passwordEncoder; } /** * The source of salts to use when decoding passwords. null is a valid * value, meaning the DaoAuthenticationProvider will present * null to the relevant PasswordEncoder. *

* Instead, it is recommended that you use an encoder which uses a random salt and * combines it with the password field. This is the default approach taken in the * {@code org.springframework.security.crypto.password} package. * * @param saltSource to use when attempting to decode passwords via the * PasswordEncoder */ public void setSaltSource(SaltSource saltSource) { this.saltSource = saltSource; } protected SaltSource getSaltSource() { return saltSource; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } protected UserDetailsService getUserDetailsService() { return userDetailsService; } }

2:AleiyeSecurityMetadataSource
1)系统启动时封装所有资源对应的权限;2)访问资源时获取该资源需要的权限
目的:用户访问某资源时 spring-security 会将url注入getAttributes方法,可通过正则关联到所有该url相对应的资源id集合,该方法返回资源id集合

package com.aleiye.web.system.security.filter;

import com.aleiye.web.system.security.entity.SysFeatureSource;
import com.aleiye.web.system.security.service.ISysFeatureSourceService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.*;
import java.util.regex.Pattern;

/**
 * @author weiwentao:
 * @version 创建时间:2015年10月21日 下午12:26:29
 *          类说明
 */
public class AleiyeSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static final Logger _LOG = LoggerFactory.getLogger(AleiyeSecurityMetadataSource.class);

    @Autowired
    private ISysFeatureSourceService sysFeatureSourceService;

    private Map> resourceMap = new HashMap<>();

    private Map urlPatternMap = new HashMap<>();

    public AleiyeSecurityMetadataSource() {
    }

    @Override
    public Collection getAllConfigAttributes() {
        List sourceList = sysFeatureSourceService.findAllEnableRecords();
        Collection collection = new ArrayList<>();
        for (SysFeatureSource sysFeatureSource : sourceList) {
            ConfigAttribute configAttribute = new SecurityConfig(sysFeatureSource.getId().toString());
            collection.add(configAttribute);
            //缓存访问路径与权限的映射关系
            Collection sourceConfig = new ArrayList<>();
            sourceConfig.add(configAttribute);
            resourceMap.put(sysFeatureSource.getId(), sourceConfig);
            //由于资源的路径不需要动态的变更,此处对正则进行缓存,以提高性能
            //只有当为非只读功能时,才需要进行正则过滤
            if (!sysFeatureSource.getReadOnly() && !sysFeatureSource.getUrlPattern().isEmpty()) {
                Pattern p = Pattern.compile(sysFeatureSource.getUrlPattern());
                urlPatternMap.put(sysFeatureSource.getId(), p);
            }else{
                _LOG.warn("the config of featureSource [id=" + sysFeatureSource.getId() + "] incorrect");
            }
        }
        return collection;
    }

    @Override
    public Collection getAttributes(Object arg0) throws IllegalArgumentException {
        //返回请求的资源需要的权限
        Collection configAttributes = new ArrayList();
        FilterInvocation fi = (FilterInvocation) arg0;
        for (Map.Entry entry : urlPatternMap.entrySet()) {
            if (entry.getValue().matcher(fi.getRequestUrl()).matches()) {
                configAttributes.addAll(resourceMap.get(entry.getKey()));
            }
        }
        return configAttributes;
    }

    @Override
    public boolean supports(Class arg0) {
        // TODO Auto-generated method stub
        return true;
    }

}

3:AleiyeAccessDecisionManager 判断用户是否拥有所访问资源需要的权限
目的:将用户拥有的权限id集合与访问的资源所需权限id集合做对比,如果用户权限不满足所访问资源所需权限则抛出AccessDeniedException异常

package com.aleiye.web.system.security.filter;

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;

import java.util.Collection;

/** 
* @author weiwentao: 
* @version 创建时间:2015年10月21日 下午12:24:57 
* 类说明 
*/
public class AleiyeAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object,
            Collection configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException {
        //如果本系统没有需要授权的资源,则直接通过校验
        if(configAttributes == null){
            return;
        }
        for(ConfigAttribute conAtt:configAttributes){
            boolean flag = true;
            String needAuthKey=((SecurityConfig)conAtt).getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()){
                if(needAuthKey.equals(ga.getAuthority())){
                    flag = false;
                    break ;
                }
            }
            if(flag)
                throw new AccessDeniedException("没有进行该操作的权限!");
        }
    }

    @Override
    public boolean supports(ConfigAttribute arg0) {
        return true;
    }

    @Override
    public boolean supports(Class arg0) {
        return true;
    }

}

以上代码可实现资源访问控制。
但仅仅这样的话,页面还是会展示没有权限的资源链接,所以还需要将页面上没有权限访问的资源链接隐藏。

第二部分:页面展示控制
实现思路:页面标签上加一个class名,该名称与资源表中的资源相对应。这样用户所拥有的权限的补集就是所有不需要展示的页面标签class名。然后将不需要展示的class名放到session中,页面加载后将这些标签remove掉。
(更好的方式应该是拿到用户拥有的权限所对应的页面标签class,然后页面上只展示这些标签。但是此项目在加权限管理的时候第一版已经开发完成,用删除没权限标签的方式更方便实现。)

你可能感兴趣的:(java)