角色权限实现方案
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风格权限实现及设计
数据库设计
权限表的业务代码
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、角色、权限增删改查功能(省略)