天天看點

Springsecurity從資料庫中動态加載登陸、授權、資源鑒權規則(重要)六、動态加載資源鑒權規則(重要)

五、加載動态資料進行登入與授權(重要)

實際的業務系統中,使用者與權限的對應關系通常是存放在RBAC權限模型的資料庫表中的

  • RBAC的權限模型可以從使用者擷取為使用者配置設定的一個或多個角色,從使用者的角色又可以擷取該角色的多種權限。通過關聯查詢可以擷取某個使用者的角色資訊和權限資訊。
  • 如果我們不希望使用者、角色、權限資訊寫死在配置裡面。我們應該實作UserDetails與UserDetailsService接口,進而從資料庫或者其他的存儲上動态的加載這些資訊。

5.1UserDetails與UserDetailsService接口

UserDetailsService接口有一個方法叫做loadUserByUsername,我們實作動态加載使用者、角色、權限資訊就是通過實作該方法。函數見名知義:通過使用者名加載使用者。該方法的傳回值就是UserDetails(本質上是個實體類,Security會自動從裡面取值進行對比)。

UserDetails就是使用者資訊,即:使用者名、密碼、該使用者所具有的權限

源碼中的UserDetails接口都有哪些方法:

public interface UserDetails extends Serializable {
    //擷取使用者的權限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //擷取密碼
    String getPassword();

    //擷取使用者名
    String getUsername();

    //賬号是否沒過期
    boolean isAccountNonExpired();

    //賬号是否沒被鎖定
    boolean isAccountNonLocked();

    //密碼是否沒過期
    boolean isCredentialsNonExpired();

    //賬戶是否可用
    boolean isEnabled();
}
           

我們把這些資訊提供給Spring Security,Spring Security就知道怎麼做登入驗證了,

這也展現了Springboot的整體理念,配置大于編碼,根本不需要我們自己寫Controller實作登入驗證邏輯。

5.2、實作UserDetails 接口

一個适應于UserDetails的java POJO類,所謂的 UserDetails接口實作就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails資料(從資料庫查詢)。

package com.springSecurityDemo.basicserver.config.auth;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * 實作使用者資訊接口,相當于Security又套了一層的使用者實體類
 */
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetails implements UserDetails {

    //**********************************編寫UserDetails相關屬性
    public String password; //密碼
    public String username;//使用者名
    public boolean accountNonExpired; //目前賬戶是否過期
    public boolean accountNonLocked; //是否沒被鎖定
    public boolean credentialsNonExpired; //是否沒過期
    public boolean enabled; // 賬戶是否可用
    Collection<? extends GrantedAuthority> authorities; // 使用者權限集合

    //*****************通過下面的方法SpringSecuirty擷取使用者的的相關資料
    /**************這幾個參數一定要傳遞好,否則會導緻無法登陸,原本重寫過來是null,我們應該重寫成我們定義的屬性傳遞回去**/
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {

        return this.username;
    }


    //賬号是否沒過期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否沒被鎖定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //密碼是否沒過期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //賬号是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }

    //******************************自定義set方法對黑盒子進行指派讓springsecurity進行調用
    public void setPassword(String password)
    {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

}
           

5.3實作UserDetailsService接口

5.3.0Dao層需要實作三個接口給Security查詢出其想要的資料才能進行這部操作:

實作三個接口:一是通過userId(使用者名)查詢使用者資訊;二是根據使用者名查詢使用者角色清單;三是**通過角色清單查詢權限清單。**這裡使用的是Mybatis

public interface MyUserDetailsServiceMapper {

    //根據userID查詢使用者資訊
    @Select("SELECT username,password,enabled\n" +
            "FROM sys_user u\n" +
            "WHERE u.username = #{userId}")
    MyUserDetails findByUserName(@Param("userId") String userId);

    //根據userID查詢使用者角色
    @Select("SELECT role_code\n" +
            "FROM sys_role r\n" +
            "LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
            "LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
            "WHERE u.username = #{userId}")
    List<String> findRoleByUserName(@Param("userId") String userId);


    //根據使用者角色查詢使用者權限
    @Select({
      "<script>",
         "SELECT url " ,
         "FROM sys_menu m " ,
         "LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
         "LEFT JOIN sys_role r ON r.id = rm.role_id ",
         "WHERE r.role_code IN ",
         "<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
            "#{roleCode}",
         "</foreach>",
      "</script>"
    })
    List<String> findAuthorityByRoleCodes(@Param("roleCodes") List<String> roleCodes);

}
           
  • 通常資料庫表sys_user字段要和SysUser屬性一一對應,比如username、password、enabled。但是比如accountNonLocked字段用于登入多次錯誤鎖定,但我們一般不會在表裡存是否鎖定,而是存一個鎖定時間字段。通過鎖定時間是否大于目前時間判斷賬号是否鎖定,是以實作過程中可以靈活做判斷并用好set方法,不必拘泥于一一對應的形式。
  • 角色是一種特殊的權限,在Spring Security我們可以使用hasRole(角色辨別)表達式判斷使用者是否具有某個角色,決定他是否可以做某個操作;通過hasAuthority(權限辨別)表達式判斷是否具有某個操作權限。
package com.springSecurityDemo.basicserver.config.auth;

import org.springframework.security.core.authority.AuthorityUtils;
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.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 實作UserDetailsService,來通過使用者名擷取使用者資訊(也是Security的起始驗證)
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    //注入之前寫的dao接口
    @Resource
    private MyUserDetailsServiceMapper myUserDetailsServiceMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.加載基礎使用者資訊 MyUserDetails是實作了UserDetails的實體類
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(username);

        if(myUserDetails == null){
            throw new UsernameNotFoundException("使用者名不存在");
        }

        //2.加載使用者角色清單
        List<String> roleCodes = myUserDetailsServiceMapper.findRoleByUserName(username);
        //3.通過使用者角色清單加載使用者的資源權限清單
        List<String> authority = myUserDetailsServiceMapper.findAuthorityByRoleCodes(roleCodes);
        //3.1角色是一個特殊的權限,也要添加到查出來的權限清單中,Security中必須有ROLE_字首(規定辨別)
        roleCodes.stream()
                .map(rc->"ROLE_"+rc) //每個對象前加字首
                .collect(Collectors.toList()); //再轉換回List
        //4.添加修改好字首的角色字首的角色權限
        authority.addAll(roleCodes);

        //5.把權限類型的權限給UserDetails
        myUserDetails.setAuthorities(
                //逗号分隔的字元串轉換成權限權限類型清單
                AuthorityUtils.commaSeparatedStringToAuthorityList(
                        //List轉字元串,逗号分隔
                        String.join(",",authority)
                )
        );
        return myUserDetails; //全部交給springsecurity
    }
}
           

5.4注冊UserDetailsService

重寫WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法

@Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
//        //靜态配置使用者
//        auth.inMemoryAuthentication()
//                .withUser("user")
//                .password(passwordEncoder().encode("123456"))
//                .roles("user")
//                    .and()
//                .withUser("admin")
//                .password(passwordEncoder().encode("123456"))
//                .authorities("sys:log","sys:user") //賦予資源id,放行其通路資源
//                //.roles("admin")
//                    .and()
//                .passwordEncoder(passwordEncoder());//配置BCrypt加密

        //從資料庫中動态加載使用者資訊與權限
        //把做好的一系列myUserDetailsService資訊交給security,并且設定加密方式
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());

    }
           

使用BCryptPasswordEncoder,表示存儲中(資料庫)取出的密碼必須是經過BCrypt加密算法加密的。

六、動态加載資源鑒權規則(重要)

簡單說“資源鑒權規則”就是:你有哪些權限?這些權限能夠通路哪些資源?即:權限與資源的比對關系。

6.1SecurityConfiger中的配置:

//權限校驗規則
             .authorizeRequests()
                //login頁面和login的url誰都可以通路
                .antMatchers("/login.html","/login").permitAll()
//                        //權限表達式的使用:通路該url需要admin角色或ROLE_admin權限
//                .antMatchers("/system/*").access("hasAnyRole('admin') or hasAnyAuthority('ROLE_admin')")
                .antMatchers("/index").authenticated() //首頁是隻要登入了就可以通路
                //使用權限表達式規則 将自定義權限規則傳入,所有url必須走我們寫的權限規則方法,才能通路
                .anyRequest().access("@rbcaService.hasPermission(request,authentication)")
           
  • 首先将靜态規則去掉(注釋掉的部分内容),這部分内容我們将替換為動态從資料庫加載
  • 登入頁面“login.html”和登入認證處理路徑“/login”需完全對外開發,不需任何鑒權就可以通路
  • 首頁**"/index"必須authenticated,即:登陸之後才能通路**。不做其他額外鑒權規則控制。
  • 最後,其他的資源的通路我們通過權限規則表達式實作,表達式規則中使用了rbacService,這個類我們自定義實作。該類服務hasPermission從記憶體(或資料庫)動态加載資源比對規則,進行資源通路鑒權。

6.2動态資源鑒權規則

  • 首先通過登入使用者名加載使用者的urls(即資源通路路徑、資源唯一辨別)。
  • 如果urls清單中任何一個元素,能夠和request.getRequestURI()請求資源路徑相比對,則表示該使用者具有通路該資源的權限。
  • urls.stream().anyMatch是java8的文法,可以周遊數組,傳回一個boolean類型。
  • hasPermission有兩個參數,第一個參數是HttpServletRequest ,第二個參數是Authentication認證主體
  • 使用者每一次通路系統資源的時候,都會執行這個方法,判斷該使用者是否具有通路該資源的權限。
package com.springSecurityDemo.basicserver.config.auth;


import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

@Component("rbcaService") //給這個bean取名
public class MyRBACService {


    @Resource
    private MyRBACServiceMapper rbacServiceMapper;

    //security提供的工具類
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 判斷某使用者是否有該請求資源的通路權限
     *
     * @param request
     * @param authentication
     * @return
     */
    public boolean hasPermission(HttpServletRequest request,
                                 Authentication authentication) {
        //從security中拿出使用者主體,實際上是我們之前封裝的UserDetials,
        //但是又被封了一層
        Object principal = authentication.getPrincipal();


        //如果取出的principal是我們放進去的UserDetails類,并且已經登入
        if (principal instanceof UserDetails) {
            //1.強轉擷取name
            String username = ((UserDetails) principal).getUsername();
			
            
            //2.從記憶體中擷取權限(因為已經登入),放入security容器中,如果有的話傳回true
    List<GrantedAuthority> authorityList =
          AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
           
            return userDetails.getAuthorities().contains(authorityList.get(0));
            
            
            //2.通過使用者名擷取使用者資源(使用者找角色,角色找資源)(這裡拿url做的辨別,是以是url)
 //           List<String> urlByUserName = rbacServiceMapper.findUrlByUserName(username);

            //3.周遊urls,然後通過antPathMatcher判斷是否比對,比對的上傳回true
       //     return urlByUserName.stream().anyMatch(
      //              url -> antPathMatcher.match(url, request.getRequestURI())
       //     );

        }
        return false;
    }

}
           

鑒權加載規則與方法級别的權限驗證與參數驗證略,可以自己找資料如果需要

繼續閱讀