Spring Boot實(shí)戰(zhàn)之Spring Security權(quán)限管理

本文使用springboot+mybatis+SpringSecurity 實(shí)現(xiàn)用戶權(quán)限數(shù)據(jù)庫(kù)管理實(shí)現(xiàn)用戶和角色用數(shù)據(jù)庫(kù)存儲(chǔ),而資源(url)和權(quán)限的對(duì)應(yīng)采用硬編碼配置。 也就是角色可以訪問(wèn)的權(quán)限通過(guò)硬編碼控制。
角色和用戶的關(guān)系通過(guò)數(shù)據(jù)庫(kù)配置控制,本文用戶和角色的關(guān)系是多對(duì)多的關(guān)系。

SpringSecurity 驗(yàn)證帳號(hào)密碼

首先在usernamePasswordAuthenticationFilter中來(lái)攔截登錄請(qǐng)求,并調(diào)用AuthenticationManager。AuthenticationManager調(diào)用Provider,provider調(diào)用userDetaisService來(lái)根據(jù)username獲取真實(shí)的數(shù)據(jù)庫(kù)信息。 最終驗(yàn)證帳號(hào)密碼的類是org.springframework.security.authentication.dao.DaoAuthenticationProvider這個(gè)流程雖然沒(méi)多么復(fù)雜,但是花費(fèi)我不少時(shí)間給理解到了。。。

本文結(jié)構(gòu):

  1. 數(shù)據(jù)庫(kù)表設(shè)計(jì)
  2. springboot+mybatis 配置
  3. 業(yè)務(wù)實(shí)現(xiàn)
  4. springSecurity整合
  5. 頁(yè)面實(shí)現(xiàn)
  6. 測(cè)試驗(yàn)證

spring security的簡(jiǎn)單原理:

使用眾多的攔截器對(duì)url攔截,以此來(lái)管理權(quán)限。但是這么多攔截器,筆者不可能對(duì)其一一來(lái)講,主要講里面核心流程的兩個(gè)。

首先,權(quán)限管理離不開登陸驗(yàn)證的,所以登陸驗(yàn)證攔截器AuthenticationProcessingFilter要講; 還有就是對(duì)訪問(wèn)的資源管理吧,所以資源管理攔截器AbstractSecurityInterceptor要講;但攔截器里面的實(shí)現(xiàn)需要一些組件來(lái)實(shí)現(xiàn),所以就有了AuthenticationManager、accessDecisionManager等組件來(lái)支撐。

現(xiàn)在先大概過(guò)一遍整個(gè)流程:
  • 用戶登陸,會(huì)被AuthenticationProcessingFilter攔截,調(diào)用AuthenticationManager的實(shí)現(xiàn),而且AuthenticationManager會(huì)調(diào)用ProviderManager來(lái)獲取用戶驗(yàn)證信息(不同的Provider調(diào)用的服務(wù)不同,因?yàn)檫@些信息可以是在數(shù)據(jù)庫(kù)上,可以是在LDAP服務(wù)器上,可以是xml配置文件上等),如果驗(yàn)證通過(guò)后會(huì)將用戶的權(quán)限信息封裝一個(gè)User放到spring的全局緩存SecurityContextHolder中,以備后面訪問(wèn)資源時(shí)使用。
  • 訪問(wèn)資源(即授權(quán)管理),訪問(wèn)url時(shí),會(huì)通過(guò)AbstractSecurityInterceptor攔截器攔截,其中會(huì)調(diào)用FilterInvocationSecurityMetadataSource的方法來(lái)獲取被攔截url所需的全部權(quán)限,在調(diào)用授權(quán)管理器AccessDecisionManager,這個(gè)授權(quán)管理器會(huì)通過(guò)spring的全局緩存SecurityContextHolder獲取用戶的權(quán)限信息,還會(huì)獲取被攔截的url和被攔截url所需的全部權(quán)限,然后根據(jù)所配的策略(有:一票決定,一票否定,少數(shù)服從多數(shù)等),如果權(quán)限足夠,則返回,權(quán)限不夠則報(bào)錯(cuò)并調(diào)用權(quán)限不足頁(yè)面。

本文目錄

  1. 數(shù)據(jù)庫(kù)表設(shè)計(jì)
  2. 權(quán)限表的業(yè)務(wù)
  3. springSecurity 配置修改
  4. 修改home.html 文件
  5. 修改HomeController.Java文件
  6. 測(cè)試檢驗(yàn)

數(shù)據(jù)庫(kù)表設(shè)計(jì)

本文的數(shù)據(jù)庫(kù)表為5個(gè)分別是: 用戶表、角色表、權(quán)限表、用戶角色中間表、角色權(quán)限中間表


這里寫圖片描述
初始化數(shù)據(jù)
注意:Sys_permission 表的url通配符為兩顆星,比如說(shuō) /user下的所有url,應(yīng)該寫成/user/**; 權(quán)限的名字可以隨意起名

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;

權(quán)限表的業(yè)務(wù)代碼

model

Permission.java
package com.us.example.domain;
/** * Created by yangyibo on 17/1/20. */
public class Permission { 
  private int id; 
  //權(quán)限名稱 
  private String name; 
  //權(quán)限描述 
  private String descritpion; 
  //授權(quán)鏈接 
  private String url; 
  //父節(jié)點(diǎn)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<Permission> findAll(); 
  public List<Permission> findByAdminUserId(int userId);
}

mapper.xml

PermissionDaoMapper.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.us.example.dao.PermissionDao">
<select id="findAll" resultType="com.us.example.domain.Permission"> 
  SELECT * from Sys_permission ;
</select> 
<select id="findByAdminUserId" parameterType="int" resultType="com.us.example.domain.Permission"> 
  select p.* from Sys_User u LEFT JOIN sys_role_user sru on u.id= 
  sru.Sys_User_id LEFT JOIN Sys_Role r on sru.Sys_Role_id=r.id LEFT JOIN 
  Sys_permission_role spr on spr.role_id=r.id LEFT JOIN Sys_permission p on p.id 
  =spr.permission_id where u.id=#{userId} 
</select> 
</mapper>

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(){ 
  //注冊(cè)UserDetailsService 的bean 
  return new CustomUserService(); 
  } 
  
  @Override 
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {     
  auth.userDetailsService(customUserService()); 
  //user Details Service驗(yàn)證 
  } 
  
  @Override 
  protected void configure(HttpSecurity http) throws Exception { 
  http.authorizeRequests() 
      .anyRequest().authenticated() //任何請(qǐng)求,登錄后可以訪問(wèn) 
   .and() .formLogin()
      .loginPage("/login") .failureUrl("/login?error") .permitAll() //登錄頁(yè)面用戶任意訪問(wèn)     
   .and() .logout().permitAll(); //注銷行為任意訪問(wèn) 
    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<Permission> permissions = permissionDao.findByAdminUserId(user.getId()); 
        List<GrantedAuthority> grantedAuthorities = new ArrayList <>(); 
        for (Permission permission : permissions) { 
            if (permission != null && permission.getName()!=null) { 
                  GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName()); 
                  //1:此處將權(quán)限信息添加到 GrantedAuthority 對(duì)象中,在后面進(jìn)行全權(quán)限驗(yàn)證時(shí)會(huì)使用GrantedAuthority 對(duì)象。 
                  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 方法是判定是否擁有權(quán)限的決策方法,
  //authentication 是釋CustomUserService中循環(huán)添加到 GrantedAuthority 對(duì)象中的權(quán)限信息集合.
  //object 包含客戶端發(fā)起的請(qǐng)求的requset信息,可轉(zhuǎn)換為 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
  //configAttributes 為MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法返回的結(jié)果,此方法是為了判定用戶請(qǐng)求的url 是否在權(quán)限表中,如果在權(quán)限表中,則返回給 decide 方法,用來(lái)判定用戶是否有此權(quán)限。如果不在權(quán)限表中則放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        if(null== configAttributes || configAttributes.size() <=0) {
            return;
        }
        ConfigAttribute c;
        String needRole;
        for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 為在注釋1 中循環(huán)添加到 GrantedAuthority 對(duì)象中的權(quán)限信息集合
                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里面有一個(gè)被攔截的url
//里面調(diào)用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法獲取fi對(duì)應(yīng)的所有權(quán)限
//再調(diào)用MyAccessDecisionManager的decide方法來(lái)校驗(yàn)用戶的權(quán)限是否足夠
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//執(zhí)行下一個(gè)攔截器
            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<String, Collection<ConfigAttribute>> map =null;

    /**
     * 加載權(quán)限表中所有權(quán)限
     */
    public void loadResourceDefine(){
        map = new HashMap<>();
        Collection<ConfigAttribute> array;
        ConfigAttribute cfg;
        List<Permission> permissions = permissionDao.findAll();
        for(Permission permission : permissions) {
            array = new ArrayList<>();
            cfg = new SecurityConfig(permission.getName());
            //此處只添加了用戶的名字,其實(shí)還可以添加更多權(quán)限的信息,例如請(qǐng)求方法到ConfigAttribute的集合中去。此處添加的信息將會(huì)作為MyAccessDecisionManager類的decide的第三個(gè)參數(shù)。
            array.add(cfg);
            //用權(quán)限的getUrl() 作為map的key,用ConfigAttribute的集合作為 value,
            map.put(permission.getUrl(), array);
        }

    }

//此方法是為了判定用戶請(qǐng)求的url 是否在權(quán)限表中,如果在權(quán)限表中,則返回給 decide 方法,用來(lái)判定用戶是否有此權(quán)限。如果不在權(quán)限表中則放行。
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if(map ==null) loadResourceDefine();
        //object 中包含用戶請(qǐng)求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for(Iterator<String> 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<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

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

修改home.html 文件

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" 
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta content="text/html;charset=UTF-8"/>
<title sec:authentication="name"></title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<style type="text/css">
body {
  padding-top: 50px;
}
.starter-template {
  padding: 40px 15px;
  text-align: center;
}
</style>
</head>
<body>
     <nav class="navbar navbar-inverse navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="#">Spring Security演示</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
          <ul class="nav navbar-nav">
           <li><a th:href="@{/}"> 首頁(yè) </a></li>
              <li><a th:href="@{/admin}"> admin </a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>


     <div class="container">

      <div class="starter-template">
        <h1 th:text="${msg.title}"></h1>

        <p class="bg-primary" th:text="${msg.content}"></p>

        <div sec:authorize="hasRole('ROLE_HOME')"> <!-- 用戶類型為ROLE_ADMIN 顯示 -->
            <p class="bg-info" th:text="${msg.etraInfo}"></p>
        </div>
          <div sec:authorize="hasRole('ROLE_ADMIN')"> <!-- 用戶類型為ROLE_ADMIN 顯示 -->
              <p class="bg-info">恭喜您,您有 ROLE_ADMIN 權(quán)限 </p>
          </div>

          <form th:action="@{/logout}" method="post">
            <input type="submit" class="btn btn-primary" value="注銷"/>
        </form>
      </div>

    </div>


</body>
</html>

修改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("測(cè)試標(biāo)題","測(cè)試內(nèi)容","歡迎來(lái)到HOME頁(yè)面,您擁有 ROLE_HOME 權(quán)限");
        model.addAttribute("msg", msg);
        return "home";
    }
    @RequestMapping("/admin")
    @ResponseBody
    public String hello(){
        return "hello admin";
    }
}

測(cè)試檢驗(yàn)

啟動(dòng)訪問(wèn) http://localhost:8080/ 到登錄頁(yè)面
由于數(shù)據(jù)庫(kù)的配置 admin 用戶擁有 訪問(wèn) home和admin 頁(yè)面的權(quán)限。 abel 用戶只有訪問(wèn) home 的權(quán)限

使用admin 登錄


這里寫圖片描述

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

題外篇

記錄下耗費(fèi)我兩天時(shí)間的一個(gè)問(wèn)題

sec:authorize="hasRole('ROLE_ADMIN')"無(wú)作用的問(wèn)題

查了很多資料都沒(méi)有定位到問(wèn)題

我的環(huán)境準(zhǔn)備,首先,加入ThymeleafSecurity4依賴,如下:

  <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity4</artifactId>
  </dependency>

然后在頁(yè)面文件中引入thymeleaf security

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

此時(shí),在需要的地方同時(shí)增加以下兩處sec:authorize標(biāo)簽

<li sec:authorize="hasRole('ROLE_ADMIN')">
    This will only be displayed if authenticated user has role ROLE_ADMIN.
</li>
<li sec:authorize="hasAuthority('admin')">
    This will only be displayed if authenticated user has role ROLE_ADMIN.
</li>
數(shù)據(jù)庫(kù)中的role和permission
數(shù)據(jù)庫(kù)中的role和permission
開始調(diào)試分析問(wèn)題

查看thymeleaf security代碼,發(fā)現(xiàn)上述標(biāo)簽的顯示與否是由這個(gè)地方控制,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);
        
    }

進(jìn)一步分析代碼,發(fā)現(xiàn)調(diào)用到了AuthUtils中的authorizeUsingAccessExpression方法

    public static boolean authorizeUsingAccessExpression(
            final IProcessingContext processingContext,
            final String accessExpression, final Authentication authentication, 
            final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext) {
        /*省略無(wú)關(guān)代碼*/
        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> T getValue(Object rootObject, Class<T> 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);
    }

問(wèn)題出在加*號(hào)的一行,返回的對(duì)象內(nèi)容為

false, Boolean

經(jīng)過(guò)反復(fù)調(diào)試,嘗試了sec:authorize="hasRole('ROLE_ADMIN')"、sec:authorize="hasRole('ADMIN')"、sec:authorize="ROLE_ADMIN"等多種組合方式,發(fā)現(xiàn)沒(méi)有任何變化。

然后無(wú)意中看到了一篇文章,上面提到了sec:authorize="hasAuthority('ROLE_ADMIN')",嘗試后發(fā)現(xiàn)生效。此時(shí)說(shuō)明整體thymeleaf security配置是沒(méi)有任何問(wèn)題的,問(wèn)題出在哪里?

進(jìn)一步分析發(fā)現(xiàn)hasAuthority判定的是CustomUserService.java的loadUserByUsername方法返回的grantedAuthorities值。

    public UserDetails loadUserByUsername(String username) {
        SysUser 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 SimpleGrantedAuthority(permission.getName());
                //1:此處將權(quán)限信息添加到 GrantedAuthority 對(duì)象中,在后面進(jìn)行全權(quán)限驗(yàn)證時(shí)會(huì)使用GrantedAuthority 對(duì)象。
                grantedAuthorities.add(grantedAuthority);
                }
            }
            return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
        } else {
            throw new UsernameNotFoundException("admin: " + username + " do not exist!");
        }
    }

查看代碼,發(fā)現(xiàn)此處只給增加了perimission,而沒(méi)有增加role,根據(jù)這種猜想調(diào)試驗(yàn)證,果然如此,OK,修改代增加role,至此問(wèn)題解決。

修改后的代碼

    public UserDetails loadUserByUsername(String username) {
        SysUserBO user = sysUserService.getUserBO(username);
        if (user != null) {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

            // 用于添加用戶的權(quán)限。只要把用戶權(quán)限添加到authorities 就萬(wàn)事大吉。
            SysRoleBO role = user.getRoleBO();
            if(null != role){
                grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            
            // 獲取角色所擁有的所有權(quán)限,并添加到authorities
            List<SysPermission> permissions = role.getPermissions();
            for (SysPermission permission : permissions) {
                if (permission != null && permission.getName() != null) {
                    // 1:此處將權(quán)限信息添加到 GrantedAuthority
                    // 對(duì)象中,在后面進(jìn)行全權(quán)限驗(yàn)證時(shí)會(huì)使用GrantedAuthority 對(duì)象。
                    grantedAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
                }
            }
            return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
        } else {
            throw new UsernameNotFoundException("admin: " + username + " do not exist!");
        }
    }

結(jié)論

  1. hasRole('ROLE_ADMIN')無(wú)效的問(wèn)題,在于grantedAuthorities中不存在role值,同理如果不存在permission值,那么hasAuthority將會(huì)無(wú)效。
  2. spring security在處理role和permission的值時(shí),默認(rèn)會(huì)區(qū)ROLE_作為role的前綴。此處需要特別留意。

問(wèn)題搞定,下班O(∩_∩)O哈哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評(píng)論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評(píng)論 1 288
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,730評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容