淺談spring security中的權限控制

當咱們在OAuth登錄後,獲取了登錄的令牌,使用該令牌,咱們就有了訪問一些受OAuth保護的接口的能力。具體能夠看本人的這兩篇博客OAuth2.0用戶名,密碼登陸解析 OAuth2.0經過token獲取受保護資源的解析 前端

但如今咱們要區分這些登錄人員的具體分工,哪些接口歸哪些登錄人員能夠訪問,這就要用到了spring security中的權限控制。spring

首先咱們須要有一個權限的對象數據庫

/**
 * 權限標識
 */
@Data
public class SysPermission implements Serializable {

   private static final long serialVersionUID = 280565233032255804L;

   private Long id; //權限id
   private String permission; //具體的權限
   private String name; //權限名稱
   private Date createTime;
   private Date updateTime;

}

對應於數據庫中的權限表express

那麼問題來了,咱們要對權限進行管理須要什麼樣的權限呢,固然咱們須要權限管理權限,這是在系統一開始創建的時候保存進數據庫的mybatis

這四個權限並非經過前端寫入的。app

如今咱們須要經過前端接口增長其餘的權限就須要使用到這四個權限之一。ide

在這裏咱們給出一些權限的增刪改查的mybatis daopost

@Mapper
public interface SysPermissionDao {

   @Options(useGeneratedKeys = true, keyProperty = "id")
   @Insert("insert into sys_permission(permission, name, createTime, updateTime) values(#{permission}, #{name}, #{createTime}, #{createTime})")
   int save(SysPermission sysPermission);

   @Update("update sys_permission t set t.name = #{name}, t.permission = #{permission}, t.updateTime = #{updateTime} where t.id = #{id}")
   int update(SysPermission sysPermission);

   @Delete("delete from sys_permission where id = #{id}")
   int delete(Long id);

   @Select("select * from sys_permission t where t.id = #{id}")
   SysPermission findById(Long id);

   @Select("select * from sys_permission t where t.permission = #{permission}")
   SysPermission findByPermission(String permission);

   int count(Map<String, Object> params);

   List<SysPermission> findData(Map<String, Object> params);

}

如今咱們要在Controller中增長一個新的權限測試

/**
 * 管理後臺添加權限
 * 
 * @param sysPermission
 * @return
 */
@LogAnnotation(module = LogModule.ADD_PERMISSION)
@PreAuthorize("hasAuthority('back:permission:save')")
@PostMapping("/permissions")
public SysPermission save(@RequestBody SysPermission sysPermission) {
   if (StringUtils.isBlank(sysPermission.getPermission())) {
      throw new IllegalArgumentException("權限標識不能爲空");
   }
   if (StringUtils.isBlank(sysPermission.getName())) {
      throw new IllegalArgumentException("權限名不能爲空");
   }

   sysPermissionService.save(sysPermission);

   return sysPermission;
}

咱們能夠看到這個標籤@PreAuthorize("hasAuthority('back:permission:save')"),首先咱們是經過access_token令牌訪問的該接口,系統能夠知道登錄的是哪個用戶,以此看看該用戶是否有back:permission:save的訪問權限ui

咱們來看看用戶角色

@Data
public class SysRole implements Serializable {

   private static final long serialVersionUID = -2054359538140713354L;

   private Long id; //角色id
   private String code; //角色編碼
   private String name; //角色名稱
   private Date createTime;
   private Date updateTime;
}

對應數據庫中的表結構以下

並給定一個管理員角色

該角色對應於哪些權限,這裏能夠看到是全部權限

而咱們的用戶是哪一個角色呢

咱們能夠看到這裏有兩個用戶,他們都屬於管理員角色

若是咱們如今用其中的一個用戶登錄,並獲取該用戶的信息以下

{
"code" : 200 ,
"data" : {
"access_token" : "aaf4cd90-497e-4c33-adde-b580ab0f0c65" ,
"user" : {
"accountNonExpired" : true ,
"accountNonLocked" : true ,
"authorities" : [
{
"authority" : "back:menu:set2role"
},
{
"authority" : "mail:update"
},
{
"authority" : "back:permission:delete"
},
{
"authority" : "role:permission:byroleid"
},
{
"authority" : "back:menu:save"
},
{
"authority" : "back:menu:query"
},
{
"authority" : "ip:black:query"
},
{
"authority" : "ip:black:save"
},
{
"authority" : "file:del"
},
{
"authority" : "ip:black:delete"
},
{
"authority" : "mail:query"
},
{
"authority" : "back:user:query"
},
{
"authority" : "back:role:permission:set"
},
{
"authority" : "sms:query"
},
{
"authority" : "back:role:query"
},
{
"authority" : "back:permission:query"
},
{
"authority" : "back:user:role:set"
},
{
"authority" : "back:role:save"
},
{
"authority" : "log:query"
},
{
"authority" : "file:query"
},
{
"authority" : "back:menu:update"
},
{
"authority" : "back:role:update"
},
{
"authority" : "back:role:delete"
},
{
"authority" : "back:user:password"
},
{
"authority" : "ROLE_SUPER_ADMIN"
},
{
"authority" : "back:menu:delete"
},
{
"authority" : "back:user:update"
},
{
"authority" : "menu:byroleid"
},
{
"authority" : "mail:save"
},
{
"authority" : "user:role:byuid"
},
{
"authority" : "back:permission:save"
},
{
"authority" : "back:permission:update"
}
],
"createTime" : "2018-01-17T16:56:59.000+0800" ,
"credentialsNonExpired" : true ,
"enabled" : true ,
"headImgUrl" : "" ,
"id" : 1 ,
"nickname" : "測試1" ,
"password" : "$2a$10$QpeXBJpWYetNwfWEHnkvLeK0jS0P9R6V8QqCj37zeNGroqYvdvW.C" ,
"permissions" : [
"back:menu:set2role" ,
"mail:update" ,
"back:permission:delete" ,
"role:permission:byroleid" ,
"back:menu:save" ,
"back:menu:query" ,
"ip:black:query" ,
"ip:black:save" ,
"file:del" ,
"ip:black:delete" ,
"mail:query" ,
"back:user:query" ,
"back:role:permission:set" ,
"sms:query" ,
"back:role:query" ,
"back:permission:query" ,
"back:user:role:set" ,
"back:role:save" ,
"log:query" ,
"file:query" ,
"back:menu:update" ,
"back:role:update" ,
"back:role:delete" ,
"back:user:password" ,
"back:menu:delete" ,
"back:user:update" ,
"menu:byroleid" ,
"mail:save" ,
"user:role:byuid" ,
"back:permission:save" ,
"back:permission:update"
],
"phone" : "" ,
"sex" : 1 ,
"sysRoles" : [
{
"code" : "SUPER_ADMIN" ,
"createTime" : "2018-01-19T20:32:16.000+0800" ,
"id" : 1 ,
"name" : "超級管理員" ,
"updateTime" : "2018-01-19T20:32:18.000+0800"
}
],
"type" : "APP" ,
"updateTime" : "2018-01-17T16:57:01.000+0800" ,
"username" : "admin"
}
},
"msg" : "操做成功"
}
經過返回的信息咱們能夠看到他的權限,跟 @PreAuthorize( "hasAuthority('back:permission:save')")比對,咱們知道登錄用戶擁有該權限。能夠訪問該接口。
上面都是前菜,下面進入正題。
咱們來看一下 @PreAuthorize 標籤的源碼,它位於org.springframework.security.access.prepost包下
/**
 * 用於指定將計算爲的方法訪問控制表達式的批註
 * 決定是否容許方法調用。
 *
 * @author Luke Taylor
 * @since 3.0
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
   /**
    * @return 在執行這個受保護的方法前進行Spring EL表達式的解析
    */
   String value();
}

這裏有一個Spring EL表達式都解析,咱們來看一下什麼是Spring EL表達式

public class SpringELTest {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        //解析字符串,該字符串具備一段代碼的味道
        Expression expression = parser.parseExpression("'Hello World'.bytes.length");
        int length = (int)expression.getValue();
        System.out.println(length);
    }
}

這個"'Hello World'.bytes.length"就是一段Spring EL表達式,雖然是一段字符串,但它有一段代碼的含義,能夠被解析執行

運行結果

11

那麼很明顯"hasAuthority('back:permission:save')"就是一段Spring EL表達式,它是能夠被執行的。

要想使標籤@PreAuthorize生效,咱們須要設置一下OAuth的資源服務設置

/**
 * 資源服務配置
 */
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

   @Override
   public void configure(HttpSecurity http) throws Exception {
      http.csrf().disable().exceptionHandling()
            .authenticationEntryPoint(
                  (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
            .and().authorizeRequests().antMatchers(PermitAllUrl.permitAllUrl("/users-anon/**", "/sys/login","/wechat/**")).permitAll()
            .anyRequest().authenticated().and().httpBasic();
   }
   @Override
   public void configure(ResourceServerSecurityConfigurer resource) throws Exception {
      //這裏把自定義異常加進去
      resource.authenticationEntryPoint(new AuthExceptionEntryPoint())
            .accessDeniedHandler(new CustomAccessDeniedHandler());
   }


   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder() {
      return new BCryptPasswordEncoder();
   }

}

其中@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)就是打開@PreAuthorize進行攔截生效的標籤,固然必定要設置prePostEnabled = true。

既然"hasAuthority('back:permission:save')"是一段Spring EL表達式,那麼hasAuthority()就必定是一個能夠執行的方法,該方法位於SecurityExpressionRoot類下,該類位於org.springframework.security.access.expression包中。

Spring Security可用表達式對象的基類就是SecurityExpressionRoot,它支持不少的方法

表達式

描述

hasRole([role])

當前用戶是否擁有指定角色。

hasAnyRole([role1,role2])

多個角色是一個以逗號進行分隔的字符串。若是當前用戶擁有指定角色中的任意一個則返回true。

hasAuthority([auth])

等同於hasRole

hasAnyAuthority([auth1,auth2])

等同於hasAnyRole

Principle

表明當前用戶的principle對象

authentication

直接從SecurityContext獲取的當前Authentication對象

permitAll

老是返回true,表示容許全部的

denyAll

老是返回false,表示拒絕全部的

isAnonymous()

當前用戶是不是一個匿名用戶

isRememberMe()

表示當前用戶是不是經過Remember-Me自動登陸的

isAuthenticated()

表示當前用戶是否已經登陸認證成功了。

isFullyAuthenticated()

若是當前用戶既不是一個匿名用戶,同時又不是經過Remember-Me自動登陸的,則返回true。

咱們具體看一下hasAuthority這個方法的實現,只有當這個方法返回的結果爲true的時候,咱們才能進一步訪問咱們的接口代碼

這裏面傳入的authority爲"back:permission:save"

public final boolean hasAuthority(String authority) {
   return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
   return hasAnyAuthorityName(null, authorities);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
   //獲取全部的用戶角色權限
   Set<String> roleSet = getAuthoritySet();
   //因爲咱們這裏傳入的roles只有"back:permission:save"
   //因此role即爲"back:permission:save",prefix則爲null
   for (String role : roles) {
      //defaultedRole依然爲"back:permission:save"
      String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
      //在權限集合中是否包含"back:permission:save"的該權限
      //根據咱們以前登陸的返回信息,能夠看到"authority": "back:permission:save"的存在
      //因此此處是能夠經過權限驗證的。
      if (roleSet.contains(defaultedRole)) {
         return true;
      }
   }

   return false;
}
private Set<String> getAuthoritySet() {
   //Set<String> rolesSecurityExpressionRoot的屬性
   //咱們能夠看到它是從一系列用戶認證裏面獲取到的權限集合
   if (roles == null) {
      roles = new HashSet<>();
      //authenticationSecurityExpressionRoot極爲重要的一個屬性,它自己是一個接口
      //管理着用戶認證信息的各個方法
      Collection<? extends GrantedAuthority> userAuthorities = authentication
            .getAuthorities();

      if (roleHierarchy != null) {
         userAuthorities = roleHierarchy
               .getReachableGrantedAuthorities(userAuthorities);
      }
      
      roles = AuthorityUtils.authorityListToSet(userAuthorities);
   }

   return roles;
}
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
   if (role == null) {
      return role;
   }
   //因爲defaultRolePrefix爲null,因此此處返回的就是"back:permission:save"
   if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
      return role;
   }
   if (role.startsWith(defaultRolePrefix)) {
      return role;
   }
   return defaultRolePrefix + role;
}

咱們能夠看一下AuthorityUtils.authorityListToSet()方法

public static Set<String> authorityListToSet(
      Collection<? extends GrantedAuthority> userAuthorities) {
   Set<String> set = new HashSet<>(userAuthorities.size());
   //很明顯這裏是把認證用戶的全部權限給轉化爲Set集合
   for (GrantedAuthority authority : userAuthorities) {
      set.add(authority.getAuthority());
   }

   return set;
}
相關文章
相關標籤/搜索