在前面的文章中耻警,已經(jīng)介紹了:
《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)潔。