首先需要配置好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">
<!--登陆相关,主要设置登陆页面的用户名和密码的属性名称,以及提交登陆的action名称-->
<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>
<!--登陆成功的处理类,可以进行session的封装等-->
<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<SimpleGrantedAuthority> grantedAuthorityCollection = new ArrayList<>();
if (secUserInfo.getId() == 1) {
List<SysFeatureSource> sourceList = sysFeatureSourceService.findAllEnableRecords();
for (SysFeatureSource sysFeatureSource : sourceList) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(sysFeatureSource.getId().toString());
grantedAuthorityCollection.add(simpleGrantedAuthority);
}
} else {
//非admin用户加载相应权限
List<SysFeatureSource> 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.
* <p>
* 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. <code>null</code> is a valid
* value, meaning the <code>DaoAuthenticationProvider</code> will present
* <code>null</code> to the relevant <code>PasswordEncoder</code>.
* <p>
* 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
* <code>PasswordEncoder</code>
*/
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<Integer, Collection<ConfigAttribute>> resourceMap = new HashMap<>();
private Map<Integer, Pattern> urlPatternMap = new HashMap<>();
public AleiyeSecurityMetadataSource() {
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
List<SysFeatureSource> sourceList = sysFeatureSourceService.findAllEnableRecords();
Collection<ConfigAttribute> collection = new ArrayList<>();
for (SysFeatureSource sysFeatureSource : sourceList) {
ConfigAttribute configAttribute = new SecurityConfig(sysFeatureSource.getId().toString());
collection.add(configAttribute);
//缓存访问路径与权限的映射关系
Collection<ConfigAttribute> 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<ConfigAttribute> getAttributes(Object arg0) throws IllegalArgumentException {
//返回请求的资源需要的权限
Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
FilterInvocation fi = (FilterInvocation) arg0;
for (Map.Entry<Integer, Pattern> 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<ConfigAttribute> 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,然后页面上只展示这些标签。但是此项目在加权限管理的时候第一版已经开发完成,用删除没权限标签的方式更方便实现。)