Spring Boot实战之Spring Security权限管理

本文使用springboot+mybatis+SpringSecurity 实现用户权限数据库管理实现用户和角色用数据库存储,而资源(url)和权限的对应采用硬编码配置。 也就是角色可以访问的权限通过硬编码控制。
角色和用户的关系通过数据库配置控制,本文用户和角色的关系是多对多的关系。

SpringSecurity 验证帐号密码

首先在usernamePasswordAuthenticationFilter中来拦截登录请求,并调用AuthenticationManager。AuthenticationManager调用Provider,provider调用userDetaisService来根据username获取真实的数据库信息。 最终验证帐号密码的类是org.springframework.security.authentication.dao.DaoAuthenticationProvider这个流程虽然没多么复杂,但是花费我不少时间给理解到了。。。

本文结构:

  1. 数据库表设计
  2. springboot+mybatis 配置
  3. 业务实现
  4. springSecurity整合
  5. 页面实现
  6. 测试验证

spring security的简单原理:

使用众多的拦截器对url拦截,以此来管理权限。但是这么多拦截器,笔者不可能对其一一来讲,主要讲里面核心流程的两个。

首先,权限管理离不开登陆验证的,所以登陆验证拦截器AuthenticationProcessingFilter要讲; 还有就是对访问的资源管理吧,所以资源管理拦截器AbstractSecurityInterceptor要讲;但拦截器里面的实现需要一些组件来实现,所以就有了AuthenticationManager、accessDecisionManager等组件来支撑。

现在先大概过一遍整个流程:
  • 用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
  • 访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。

本文目录

  1. 数据库表设计
  2. 权限表的业务
  3. springSecurity 配置修改
  4. 修改home.html 文件
  5. 修改HomeController.Java文件
  6. 测试检验

数据库表设计

本文的数据库表为5个分别是: 用户表、角色表、权限表、用户角色中间表、角色权限中间表


Spring Boot实战之Spring Security权限管理_第1张图片
这里写图片描述
初始化数据
注意:Sys_permission 表的url通配符为两颗星,比如说 /user下的所有url,应该写成/user/**; 权限的名字可以随意起名

insert into SYS_USER (id,username, password) values (1,'admin', 'admin');
insert into SYS_USER (id,username, password) values (2,'abel', 'abel');
insert into SYS_ROLE(id,name) values(1,'ROLE_ADMIN');
insert into SYS_ROLE(id,name) values(2,'ROLE_USER');
insert into SYS_ROLE_USER(SYS_USER_ID,ROLES_ID) values(1,1);
insert into SYS_ROLE_USER(SYS_USER_ID,ROLES_ID) values(2,2);
BEGIN;
INSERT INTO `Sys_permission` VALUES ('1', 'ROLE_HOME', 'home', '/', null), ('2', 'ROLE_ADMIN', 'ABel', '/admin', null);
COMMIT;
BEGIN;
INSERT INTO `Sys_permission_role` VALUES ('1', '1', '1'), ('2', '1', '2'), ('3', '2', '1');
COMMIT;

权限表的业务代码

model

Permission.java
package com.us.example.domain;
/** * Created by yangyibo on 17/1/20. */
public class Permission { 
  private int id; 
  //权限名称 
  private String name; 
  //权限描述 
  private String descritpion; 
  //授权链接 
  private String url; 
  //父节点id 
  private int pid; 
  public int getId() { return id; }
  public void setId(int id) { this.id = id; } 
  public String getName() { return name; } 
  public void setName(String name) { this.name = name; } 
  public String getDescritpion() { return descritpion; } 
  public void setDescritpion(String descritpion) { this.descritpion = descritpion; } 
  public String getUrl() { return url; } 
  public void setUrl(String url) { this.url = url; } 
  public int getPid() { return pid; } 
  public void setPid(int pid) { this.pid = pid; }
}

mapper

PermissionDao.java
package com.us.example.dao;
import com.us.example.config.MyBatisRepository;
import com.us.example.domain.Permission;
import java.util.List;
/** * Created by yangyibo on 17/1/20. */
  public interface PermissionDao { 
  public List findAll(); 
  public List findByAdminUserId(int userId);
}

mapper.xml

PermissionDaoMapper.xml


 
 

springSecurity 配置修改

修改 WebSecurityConfig.java

package com.us.example.config;
import com.us.example.service.CustomUserService;
import com.us.example.service.MyFilterSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/** * Created by yangyibo on 17/1/18. */
@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);     
  }
}

修改CustomUserService

package com.us.example.service;
import com.us.example.dao.PermissionDao;
import com.us.example.dao.UserDao;
import com.us.example.domain.Permission;
import com.us.example.domain.SysRole;
import com.us.example.domain.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

/** * Created by yangyibo on 17/1/18. */
@Service
public class CustomUserService implements UserDetailsService { 
  //自定义UserDetailsService 接口 
  @Autowired 
  UserDao userDao; 
  @Autowired 
  PermissionDao permissionDao; 
  public UserDetails loadUserByUsername(String username) { 
    SysUser user = userDao.findByUserName(username); 
    if (user != null) { 
        List permissions = permissionDao.findByAdminUserId(user.getId()); 
        List grantedAuthorities = new ArrayList <>(); 
        for (Permission permission : permissions) { 
            if (permission != null && permission.getName()!=null) { 
                  GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName()); 
                  //1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。 
                  grantedAuthorities.add(grantedAuthority); 
            }
         } 
        return new User(user.getUsername(), user.getPassword(), grantedAuthorities); 
    } else { 
    throw new UsernameNotFoundException("admin: " + username + " do not exist!");     
    } 
  }
}

新增MyAccessDecisionManager

package com.us.example.service;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Iterator;

/**
 * Created by yangyibo on 17/1/19.
 */
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

  // decide 方法是判定是否拥有权限的决策方法,
  //authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
  //object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
  //configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        if(null== configAttributes || configAttributes.size() <=0) {
            return;
        }
        ConfigAttribute c;
        String needRole;
        for(Iterator iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
                if(needRole.trim().equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("no right");
    }

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

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

新增 MyFilterSecurityInterceptor

package com.us.example.service;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * Created by yangyibo on 17/1/19.
 */
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {


    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }


    public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

新增 MyInvocationSecurityMetadataSourceService

package com.us.example.service;

import com.us.example.dao.PermissionDao;
import com.us.example.domain.Permission;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * Created by yangyibo on 17/1/19.
 */
@Service
public class MyInvocationSecurityMetadataSourceService  implements
        FilterInvocationSecurityMetadataSource {

    @Autowired
    private PermissionDao permissionDao;

    private HashMap> map =null;

    /**
     * 加载权限表中所有权限
     */
    public void loadResourceDefine(){
        map = new HashMap<>();
        Collection array;
        ConfigAttribute cfg;
        List permissions = permissionDao.findAll();
        for(Permission permission : permissions) {
            array = new ArrayList<>();
            cfg = new SecurityConfig(permission.getName());
            //此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
            array.add(cfg);
            //用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
            map.put(permission.getUrl(), array);
        }

    }

//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        if(map ==null) loadResourceDefine();
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for(Iterator iter = map.keySet().iterator(); iter.hasNext(); ) {
            resUrl = iter.next();
            matcher = new AntPathRequestMatcher(resUrl);
            if(matcher.matches(request)) {
                return map.get(resUrl);
            }
        }
        return null;
    }

    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

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

修改home.html 文件










     


     

恭喜您,您有 ROLE_ADMIN 权限

修改HomeController.java 文件

package com.us.example.controller;

import com.us.example.domain.Msg;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Created by yangyibo on 17/1/18.
 */
@Controller
public class HomeController {

    @RequestMapping("/")
    public String index(Model model){
        Msg msg =  new Msg("测试标题","测试内容","欢迎来到HOME页面,您拥有 ROLE_HOME 权限");
        model.addAttribute("msg", msg);
        return "home";
    }
    @RequestMapping("/admin")
    @ResponseBody
    public String hello(){
        return "hello admin";
    }
}

测试检验

启动访问 http://localhost:8080/ 到登录页面
由于数据库的配置 admin 用户拥有 访问 home和admin 页面的权限。 abel 用户只有访问 home 的权限

使用admin 登录


Spring Boot实战之Spring Security权限管理_第2张图片
这里写图片描述

参考资料: http://www.tuicool.com/articles/jq6fuur#c-23220 http://blog.csdn.net/u012367513/article/details/38866465

题外篇

记录下耗费我两天时间的一个问题

sec:authorize="hasRole('ROLE_ADMIN')"无作用的问题

查了很多资料都没有定位到问题

我的环境准备,首先,加入ThymeleafSecurity4依赖,如下:

  
      org.thymeleaf.extras
      thymeleaf-extras-springsecurity4
  

然后在页面文件中引入thymeleaf security


此时,在需要的地方同时增加以下两处sec:authorize标签

  • This will only be displayed if authenticated user has role ROLE_ADMIN.
  • This will only be displayed if authenticated user has role ROLE_ADMIN.
  • 数据库中的role和permission
    Spring Boot实战之Spring Security权限管理_第3张图片
    数据库中的role和permission
    开始调试分析问题

    查看thymeleaf security代码,发现上述标签的显示与否是由这个地方控制,AuthorizeAttrProcessor类中的isVisible方法

        @Override
        protected boolean isVisible(final Arguments arguments, final Element element,
                final String attributeName) {
    
            final String attributeValue = element.getAttributeValue(attributeName);
            
            if (attributeValue == null || attributeValue.trim().equals("")) {
                return false;
            }
            
            final IContext context = arguments.getContext();
            if (!(context instanceof IWebContext)) {
                throw new ConfigurationException(
                        "Thymeleaf execution context is not a web context (implementation of " +
                        IWebContext.class.getName() + ". Spring Security integration can only be used in " +
                        "web environements.");
            }
            final IWebContext webContext = (IWebContext) context;
            
            final HttpServletRequest request = webContext.getHttpServletRequest();
            final HttpServletResponse response = webContext.getHttpServletResponse();
            final ServletContext servletContext = webContext.getServletContext();
            
            final Authentication authentication = AuthUtils.getAuthenticationObject();
    
            if (authentication == null) {
                return false;
            }
            
            return AuthUtils.authorizeUsingAccessExpression(
                    arguments, attributeValue, authentication, request, response, servletContext);
            
        }
    

    进一步分析代码,发现调用到了AuthUtils中的authorizeUsingAccessExpression方法

        public static boolean authorizeUsingAccessExpression(
                final IProcessingContext processingContext,
                final String accessExpression, final Authentication authentication, 
                final HttpServletRequest request, final HttpServletResponse response,
                final ServletContext servletContext) {
            /*省略无关代码*/
            if (ExpressionUtils.evaluateAsBoolean(expressionObject, wrappedEvaluationContext)) {
    
                if (logger.isTraceEnabled()) {
                    logger.trace("[THYMELEAF][{}] Checked authorization using access expression \"{}\" for user \"{}\". Access GRANTED.",
                            new Object[] {TemplateEngine.threadIndex(), accessExpression, (authentication == null? null : authentication.getName())});
                }
                
                return true;
                
            }
    
            if (logger.isTraceEnabled()) {
                logger.trace("[THYMELEAF][{}] Checked authorization using access expression \"{}\" for user \"{}\". Access DENIED.",
                        new Object[] {TemplateEngine.threadIndex(), accessExpression, (authentication == null? null : authentication.getName())});
            }
            
            return false;
    
    public final class ExpressionUtils {
    
        public static boolean evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
            try {
                return ((Boolean) expr.getValue(ctx, Boolean.class)).booleanValue();
            }
            catch (EvaluationException e) {
                throw new IllegalArgumentException("Failed to evaluate expression '"
                        + expr.getExpressionString() + "'", e);
            }
        }
    }
    

    然后到了SpelExpression中的getValue方法

        @SuppressWarnings("unchecked")
        @Override
        public  T getValue(Object rootObject, Class expectedResultType) throws EvaluationException {
            if (this.compiledAst != null) {
                try {
                    Object result = this.compiledAst.getValue(rootObject, null);
                    if (expectedResultType == null) {
                        return (T)result;
                    }
                    else {
                        return ExpressionUtils.convertTypedValue(getEvaluationContext(), new TypedValue(result), expectedResultType);
                    }
                }
                catch (Throwable ex) {
                    // If running in mixed mode, revert to interpreted
                    if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
                        this.interpretedCount = 0;
                        this.compiledAst = null;
                    }
                    else {
                        // Running in SpelCompilerMode.immediate mode - propagate exception to caller
                        throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
                    }
                }
            }
            ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration);  
            ********TypedValue typedResultValue = this.ast.getTypedValue(expressionState);***********
            checkCompile(expressionState);
            return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType);
        }
    

    问题出在加*号的一行,返回的对象内容为

    false, Boolean
    

    经过反复调试,尝试了sec:authorize="hasRole('ROLE_ADMIN')"、sec:authorize="hasRole('ADMIN')"、sec:authorize="ROLE_ADMIN"等多种组合方式,发现没有任何变化。

    然后无意中看到了一篇文章,上面提到了sec:authorize="hasAuthority('ROLE_ADMIN')",尝试后发现生效。此时说明整体thymeleaf security配置是没有任何问题的,问题出在哪里?

    进一步分析发现hasAuthority判定的是CustomUserService.java的loadUserByUsername方法返回的grantedAuthorities值。

        public UserDetails loadUserByUsername(String username) {
            SysUser user = userDao.findByUserName(username);
            if (user != null) {
                List permissions = permissionDao.findByAdminUserId(user.getId());
                List grantedAuthorities = new ArrayList <>();
                for (Permission permission : permissions) {
                    if (permission != null && permission.getName()!=null) {
    
                    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
                    //1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                    grantedAuthorities.add(grantedAuthority);
                    }
                }
                return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
            } else {
                throw new UsernameNotFoundException("admin: " + username + " do not exist!");
            }
        }
    

    查看代码,发现此处只给增加了perimission,而没有增加role,根据这种猜想调试验证,果然如此,OK,修改代增加role,至此问题解决。

    修改后的代码

        public UserDetails loadUserByUsername(String username) {
            SysUserBO user = sysUserService.getUserBO(username);
            if (user != null) {
                List grantedAuthorities = new ArrayList<>();
    
                // 用于添加用户的权限。只要把用户权限添加到authorities 就万事大吉。
                SysRoleBO role = user.getRoleBO();
                if(null != role){
                    grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
                }
                
                // 获取角色所拥有的所有权限,并添加到authorities
                List permissions = role.getPermissions();
                for (SysPermission permission : permissions) {
                    if (permission != null && permission.getName() != null) {
                        // 1:此处将权限信息添加到 GrantedAuthority
                        // 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                        grantedAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
                    }
                }
                return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
            } else {
                throw new UsernameNotFoundException("admin: " + username + " do not exist!");
            }
        }
    

    结论

    1. hasRole('ROLE_ADMIN')无效的问题,在于grantedAuthorities中不存在role值,同理如果不存在permission值,那么hasAuthority将会无效。
    2. spring security在处理role和permission的值时,默认会区ROLE_作为role的前缀。此处需要特别留意。

    问题搞定,下班O(∩_∩)O哈哈~

    你可能感兴趣的:(Spring Boot实战之Spring Security权限管理)