角色权限实现方案

1、背景

本系统要求不同用户登陆后,可以操作不同的系统功能。所以要求每个登录用户具有不同的角色,每个角色具有不同的权限;同时要求后期添加角色、权限时,不需要修改系统实现逻辑。

2、角色权限方案

角色权限实现框架有Spring Security与Shiro两种,因Jhispter安全认证采用的Spring Security框架,因此选择Spring Security。

Spring Securit权限认证方式有:

  • 表达式控制 URL 路径权限;
  • 表达式控制方法权限;
  • 使用过滤注解;
  • 动态权限;

、表达式控制 URL 路径权限:通过表达式控制 URL 路径权限。 

Spring Security 支持在 URL 和方法权限控制时使用 SpEL 表达式,如果表达式返回值为 true 则表示需要对应的权限,否则表示不需要对应的权限。提供表达式的类是 SecurityExpressionRoot,该类对应的表达式有:

表达式

备注

hasRole

用户具备某个角色即可访问资源

hasAnyRole

用户具备多个角色中的任意一个即可访问资源

hasAuthority

类似于 hasRole

hasAnyAuthority

类似于 hasAnyRole

permitAll

统统允许访问

denyAll

统统拒绝访问

isAnonymous

判断是否匿名用户

isAuthenticated

判断是否认证成功

isRememberMe

判断是否通过记住我登录的

isFullyAuthenticated

判断是否用户名/密码登录的

principle

当前用户

authentication

从 SecurityContext 中提取出来的用户对象

 

 

使用配置:

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
           .antMatchers("/admin/**").hasRole("admin")
           .antMatchers("/user/**").hasAnyRole("admin","user")
           .anyRequest().authenticated()
           .and()
           ...
}
二、表达式控制方法权限:通过在方法上添加注解来控制权限。
在方法上添加注解控制权限,需要我们首先开启注解的使用,在 Spring Security 配置类上添加如下内容:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    ...
}
这个配置开启了三个注解,分别是:
1. @PreAuthorize:方法执行前进行权限检查;
2. @PostAuthorize:方法执行后进行权限检查;
3. @Secured:类似于 @PreAuthorize;
@Service
public class HelloService {
    @PreAuthorize("principal.username.equals('javaboy')")
    public String hello() {
        return "hello";
    }
 
    @PreAuthorize("hasRole('admin')")
    public String admin() {
        return "admin";
    }
 
    @Secured({"ROLE_user"})
    public String user() {
        return "user";
    }
 
    @PreAuthorize("#age>98")
    public String getAge(Integer age) {
        return String.valueOf(age);
    }
三、使用过滤注解:
Spring Security 中还有两个过滤函数 @PreFilter 和 @PostFilter,可以根据给出的条件,自动移除集合中的元素。
@PostFilter("filterObject.lastIndexOf('2')!=-1")
public List<String> getAllUser() {
    List<String> users = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        users.add("javaboy:" + i);
    }
    return users;
}
@PreFilter(filterTarget = "ages",value = "filterObject%2==0")
public void getAllAge(List<Integer> ages,List<String> users) {
    System.out.println("ages = " + ages);
    System.out.println("users = " + users);
}
四、动态权限:动态权限主要通过重写拦截器和决策器来实现。
 
主要使用的组件:
AuthenticationProcessingFilter
AbstractSecurityInterceptor
AuthenticationManager
AccessDecisionManager等组件来支撑。

整个流程,用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。

访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。

 

 

3、实施方案选择

根据四个方案的不同以及业务需求,动态权限更加符合系统需求,因此采用动态权限来实现本系统的角色权限控制功能。

4、基于Restful风格权限实现及设计

数据库设计

角色权限设计架构 角色权限控制实现_Spring Security

权限表的业务代码

PermissionDao.java

public interface PermissionDao {
    public List<Permission> findAll();
    public List<Permission> findByAdminUserId(int userId);
}
UserDao.java
public interface UserDao {
    public User findByUserName(String username);
}
SpringSecurity 配置修改
1. 修改 WebSecurityConfig.java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
    @Bean
    UserDetailsService customUserService(){ //注册UserDetailsService 的bean
return new CustomUserService();
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //user Details Service验证
 
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() //任何请求,登录后可以访问
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login?error")
                .permitAll() //登录页面用户任意访问
                .and()
                .logout().permitAll(); //注销行为任意访问
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
    }
}
2、实现 GrantedAuthority 接口
public class MyGrantedAuthority implements GrantedAuthority {
    private String url;
private String method;
 
3、修改CustomUserService
@Service
public class CustomUserService implements UserDetailsService { //自定义UserDetailsService 接口
 
    @Autowired
    UserDao userDao;
    @Autowired
    PermissionDao permissionDao;
 
    public UserDetails loadUserByUsername(String username) {
        User user = userDao.findByUserName(username);
        if (user != null) {
            List<Permission> permissions = permissionDao.findByAdminUserId(user.getId());
            List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
            for (Permission permission : permissions) {
                if (permission != null && permission.getName()!=null) {
 
GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getUrl(), permission.getMethod());
                    grantedAuthorities.add(grantedAuthority);
 
                //1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                grantedAuthorities.add(grantedAuthority);
                }
            }
            return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
        } else {
            throw new UsernameNotFoundException("admin: " + username + " do not exist!");
        }
    }
 
}
4、修改 MyAccessDecisionManager 的decide 方法
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
 
    //decide 方法是判定是否拥有权限的决策方法
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
 
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        String url, method;
        AntPathRequestMatcher matcher;
        for (GrantedAuthority ga : authentication.getAuthorities()) {
 if (ga instanceof MyGrantedAuthority) {
                MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;
                url = urlGrantedAuthority.getPermissionUrl();
                method = urlGrantedAuthority.getMethod();
                matcher = new AntPathRequestMatcher(url);
                if (matcher.matches(request)) {
                    //当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。
                    if (method.equals(request.getMethod()) || "ALL".equals(method)) {
                        return;
                    }
                }
            } else if (ga.getAuthority().equals("ROLE_ANONYMOUS")) {//未登录只允许访问 login 页面
                matcher = new AntPathRequestMatcher("/login");
                if (matcher.matches(request)) {
                    return;
                }
                }
        throw new AccessDeniedException("no right");
    }
 
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}
5、修改MyInvocationSecurityMetadataSourceService 的getAttributes 方法
 
@Service
public class MyInvocationSecurityMetadataSourceService  implements
        FilterInvocationSecurityMetadataSource {
 
    //此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    //因为我不想每一次来了请求,都先要匹配一下权限表中的信息是不是包含此url,
    // 我准备直接拦截,不管请求的url 是什么都直接拦截,然后在MyAccessDecisionManager的decide 方法中做拦截还是放行的决策。
    //所以此方法的返回值不能返回 null 此处我就随便返回一下。
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        Collection<ConfigAttribute> co=new ArrayList<>();
co.add(new SecurityConfig("null"));
        return co;
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

4、角色、权限增删改查功能(省略)