Spring Security基于資源的認(rèn)證和授權(quán)

在前面的文章中耻警,已經(jīng)介紹了:

《Spring Security入門案例》

《Spring Security使用數(shù)據(jù)庫(kù)進(jìn)行認(rèn)證和授權(quán)》

但都是基于角色(Role Based Access Control)的案例甸怕,本文主要演示下基于資源(Resoure Based Access Control)的認(rèn)證與授權(quán)案例。(本文的內(nèi)容是基于以上兩篇文章進(jìn)行的延續(xù)梢杭,建議提前閱讀前面兩篇文章的內(nèi)容)

一武契、基于內(nèi)存的案例

首先新建一個(gè)Controller募判,里面只有新增和刪除用戶兩個(gè)接口,其中root用戶可以操作新增和刪除内颗,zhang用戶只能刪除敦腔。

@RestController
public class UserController {

    @GetMapping("/addUser")
    public String addUser(){
        return "add user success!";
    }

    @GetMapping("/deleteUser")
    public String deleteUser(){
        return "delete user success!";
    }
}

然后是Security的配置類:

@EnableWebSecurity
public class AnotherSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("root").password(passwordEncoder().encode("root999")).authorities("user:add", "user:delete")
                .and()
                .withUser("zhang").password(passwordEncoder().encode("mm111")).authorities("user:delete");
    }

    /**
     * 對(duì)請(qǐng)求進(jìn)行鑒權(quán)的配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 需要user:add權(quán)限才可以訪問(wèn)
                .antMatchers("/addUser").hasAuthority("user:add")
                // 需要user:delete權(quán)限才可以訪問(wèn)
                .antMatchers("/deleteUser").hasAuthority("user:delete")
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }

    /**
     * 默認(rèn)開啟密碼加密符衔,前端傳入的密碼Security會(huì)在加密后和數(shù)據(jù)庫(kù)中的密文進(jìn)行比對(duì),一致的話就登錄成功
     * 所以必須提供一個(gè)加密對(duì)象躺盛,供security加密前端明文密碼使用
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

注意到形帮,接口的資源名稱是在配置類里面配置的,與基于角色的訪問(wèn)控制相比辩撑,基于資源的控制粒度更細(xì)界斜,能夠更加靈活地控制合冀。

二、基于數(shù)據(jù)庫(kù)的案例

我們需要基于前面的案例君躺,再增加如下的資源表棕叫、用戶資源對(duì)應(yīng)關(guān)系表:

CREATE TABLE `auth_resource` (
  `resource_id` int DEFAULT NULL,
  `resource_name` varchar(100) DEFAULT NULL,
  `resource_code` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

INSERT INTO auth.auth_resource (resource_id, resource_name, resource_code) VALUES(1, '添加用戶', 'user:add');
INSERT INTO auth.auth_resource (resource_id, resource_name, resource_code) VALUES(2, '刪除用戶', 'user:delete');
CREATE TABLE `auth_user_resource` (
  `id` int DEFAULT NULL,
  `user_id` int DEFAULT NULL,
  `resource_code` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(1, 1, 'user:add');
INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(2, 1, 'user:delete');
INSERT INTO auth.auth_user_resource (id, user_id, resource_code) VALUES(3, 2, 'user:delete');

然后創(chuàng)建Dao層相關(guān)的內(nèi)容:

<select id="getAnotherUserByUserName" parameterType="string" resultType="com.example.securitydemo.po.AnotherUser">
    select
    au.user_id userId,
    au.user_name userName,
    au.password,
    au.expired,
    au.locked
    from
    auth_user au
    where
    au.user_name = #{userName}
</select>

<select id="getUserResourceByUserId" parameterType="integer" resultType="com.example.securitydemo.po.Resource">
    select
    ar.resource_id resourceId,
    ar.resource_code resourceCode,
    ar.resource_name resourceName
    from
    auth_user_resource aur
    left join auth_resource ar on
    aur.resource_code = ar.resource_code
    where
    aur.user_id = #{userId}
</select>
@Mapper
public interface UserMapper {

    AnotherUser getAnotherUserByUserName(String userName);

    List<Resource> getUserResourceByUserId(Integer userId);

}

創(chuàng)建用戶和資源的實(shí)體類:

@Data
public class AnotherUser {

    private Integer userId;

    private String userName;

    private String password;

    private List<Resource> resourceList;

}
@Data
public class Resource {

    private Integer resourceId;
    private String resourceName;
    private String resourceCode;

}

然后谍珊,我們就可以開始編寫Service層的代碼了,實(shí)現(xiàn)將數(shù)據(jù)庫(kù)中用戶的賬密和所屬資源加載進(jìn)來(lái)的操作:

@Slf4j
@Service
public class AnotherUserService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 根據(jù)用戶名去數(shù)據(jù)庫(kù)獲取用戶信息侮邀,SpringSecutity會(huì)自動(dòng)進(jìn)行密碼的比對(duì)
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 用戶名必須是唯一的贝润,不允許重復(fù)
        AnotherUser user = userMapper.getAnotherUserByUserName(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("根據(jù)用戶名找不到該用戶的信息!");
        }
        List<com.example.securitydemo.po.Resource> resourceList = userMapper.getUserResourceByUserId(user.getUserId());
        if (ObjectUtils.isEmpty(resourceList)) {
            log.warn("該用戶沒(méi)有任何權(quán)限华畏!");
            return null;
        }
        int num = resourceList.size();
        // 定義一個(gè)數(shù)組用來(lái)存放當(dāng)前用戶的所有資源權(quán)限
        String[] resourceCodeArray = new String[num];
        for (int i = 0; i < num; i++) {
            resourceCodeArray[i] = resourceList.get(i).getResourceCode();
        }
        return User.withUsername(user.getUserName())
                .password(user.getPassword())
                .authorities(resourceCodeArray).build();
    }
}

最后,還有我們的配置類(其它配置類需要先注釋掉):

@EnableWebSecurity
public class AnotherDBSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AnotherUserService userService;

    /**
     * 對(duì)請(qǐng)求進(jìn)行鑒權(quán)的配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            // 需要user:add權(quán)限才可以訪問(wèn)
            .antMatchers("/addUser").hasAuthority("user:add")
            // 需要user:delete權(quán)限才可以訪問(wèn)
            .antMatchers("/deleteUser").hasAuthority("user:delete")
            .and()
            .formLogin()
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 默認(rèn)開啟密碼加密侣夷,前端傳入的密碼Security會(huì)在加密后和數(shù)據(jù)庫(kù)中的密文進(jìn)行比對(duì)百拓,一致的話就登錄成功
     * 所以必須提供一個(gè)加密對(duì)象
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

如此晰甚,本案例的所有代碼就都寫好了,重新啟動(dòng)項(xiàng)目后厕九,可以實(shí)現(xiàn)基于資源的認(rèn)證和授權(quán)功能。

補(bǔ)充:應(yīng)該有注意到俊鱼,本案例中使用數(shù)據(jù)庫(kù)的實(shí)體類和UserService跟上一篇中的用法不太一樣畅买,其實(shí)這是兩種寫法,本案例也可以改造為和上一篇中一樣的寫法,而且較為推薦這種寫法纹冤。

需要改造的代碼如下:

@Data
public class AnotherUser2 implements UserDetails{

    private Integer userId;

    private String userName;

    private String password;

    private Integer expired;

    private Integer locked;

    private List<Resource> resourceList;

    /**
     * 獲取用戶的所有角色信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for(Resource resource : resourceList){
            authorities.add(new SimpleGrantedAuthority(resource.getResourceCode()));
        }
        return authorities;
    }

    /**
     * 指定哪一個(gè)是用戶的密碼字段
     * @return
     */
    @Override
    public String getPassword() {
        return password;
    }

    /**
     * 指定哪一個(gè)是用戶的賬戶字段
     * @return
     */
    @Override
    public String getUsername() {
        return userName;
    }


    /**
     * 判斷賬戶是否過(guò)期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return (expired == 0);
    }

    /**
     * 判斷賬戶是否鎖定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return (locked == 0);
    }

    /**
     * 判斷密碼是否過(guò)期
     * 可以根據(jù)業(yè)務(wù)邏輯或者數(shù)據(jù)庫(kù)字段來(lái)決定
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判斷賬戶是否可用
     * 可以根據(jù)業(yè)務(wù)邏輯或者數(shù)據(jù)庫(kù)字段來(lái)決定
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

}
@Slf4j
@Service
public class AnotherUserService2 implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 根據(jù)用戶名去數(shù)據(jù)庫(kù)獲取用戶信息萌京,SpringSecutity會(huì)自動(dòng)進(jìn)行密碼的比對(duì)
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 用戶名必須是唯一的,不允許重復(fù)
        AnotherUser2 user = userMapper.getAnotherUser2ByUserName(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("根據(jù)用戶名找不到該用戶的信息靠瞎!");
        }
        List<com.example.securitydemo.po.Resource> resourceList = userMapper.getUserResourceByUserId(user.getUserId());
        user.setResourceList(resourceList);
        return user;
    }
}

即將用戶賬密和權(quán)限的填充從service挪到Bean中進(jìn)行求妹,如此service會(huì)顯得更加簡(jiǎn)潔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末父能,一起剝皮案震驚了整個(gè)濱河市净神,隨后出現(xiàn)的幾起案子溉委,更是在濱河造成了極大的恐慌爱榕,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藻三,死亡現(xiàn)場(chǎng)離奇詭異絮爷,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)岖寞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門柜蜈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人隶垮,你說(shuō)我怎么就攤上這事秘噪。” “怎么了指煎?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵至壤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我像街,道長(zhǎng)镰绎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任俭厚,我火速辦了婚禮驶臊,結(jié)果婚禮上叼丑,老公的妹妹穿的比我還像新娘扛门。我一直安慰自己鸠信,他們只是感情好论寨,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布葬凳。 她就那樣靜靜地躺著,像睡著了一般劲装。 火紅的嫁衣襯著肌膚如雪昌简。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天谦疾,我揣著相機(jī)與錄音犬金,去河邊找鬼念恍。 笑死晚顷,一個(gè)胖子當(dāng)著我的面吹牛音同,可吹牛的內(nèi)容都是我干的秃嗜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼叽赊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼必搞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起恕洲,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤梅割,失蹤者是張志新(化名)和其女友劉穎葛家,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體底燎,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弹砚,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年桌吃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片为流。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡让簿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出尔当,到底是詐尸還是另有隱情,我是刑警寧澤锐帜,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布畜号,位于F島的核電站,受9級(jí)特大地震影響蛮拔,放射性物質(zhì)發(fā)生泄漏痹升。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一肛跌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧衍慎,春花似錦、人聲如沸西饵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驯嘱。三九已至,卻和暖如春茂蚓,著一層夾襖步出監(jiān)牢的瞬間剃幌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工牍白, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人茂腥。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓切省,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親般渡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芙盘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

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