????歡迎來(lái)到我的CSDN主頁(yè)!????
??我是Java方文山,一個(gè)在CSDN分享筆記的博主。????
??推薦給大家我的專欄《Spring Security》。????
??點(diǎn)擊這里,就可以查看我的主頁(yè)啦!????
Java方文山的個(gè)人主頁(yè)
??如果感覺(jué)還不錯(cuò)的話請(qǐng)給我點(diǎn)贊吧!????
??期待你的加入,一起學(xué)習(xí),一起進(jìn)步!????
前言
我們都知道Spring Security是做認(rèn)證鑒權(quán)的框架,為了一些不必要的麻煩,登錄功能都是他們自己做的,那我們就需要將登錄的功能交給Spring Security管理,但是他們做的東西肯定也是不滿足于我們的需求,所以我們要在根據(jù)他們提供的代碼上自定義,可以將自定義的用戶信息獲取邏輯集成到Spring Security中,從而實(shí)現(xiàn)基于數(shù)據(jù)庫(kù)的用戶身份認(rèn)證。
再上一篇中我是通過(guò)實(shí)現(xiàn)UserDetailsService接口并重寫(xiě)loadUserByUsername方法完成的,但是后面我想了想不應(yīng)該把代碼放在UserServiceImpl層,所以我們需要?jiǎng)?chuàng)建一個(gè)類來(lái)繼承UserServiceImpl將自定義用戶身份認(rèn)證的代碼寫(xiě)入。
MyUserDetailsService?
package com.csdn.security.config;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.csdn.security.pojo.User;
import com.csdn.security.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Objects;
/**
* @author Java方文山
* @compay csdn_Java方文山
* @create 2023-12-22-12:10
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserServiceImpl userService;
/**
* 實(shí)現(xiàn)Spring Security內(nèi)置的UserDetailService接口,重寫(xiě)loadUserByUsername方法實(shí)現(xiàn)數(shù)據(jù)庫(kù)的身份校驗(yàn)
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根據(jù)用戶名查詢數(shù)據(jù)庫(kù)中用戶信息
User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
//判斷用戶是否存在
if(Objects.isNull(user))
throw new UsernameNotFoundException("用戶不存在");
//權(quán)限校驗(yàn)TODO,后續(xù)講解
return user;
}
}
記得我們也要把我們做身份驗(yàn)證這里也改掉,替換成MyUserDetailsService?
?一、前期準(zhǔn)備
我們將數(shù)據(jù)相應(yīng)到前端,肯定需要特定的格式,方便我們前端做數(shù)據(jù)顯示,這里會(huì)用到兩個(gè)類
JsonResponseStatus?
package com.csdn.security.resp;
import lombok.Getter;
@Getter
public enum JsonResponseStatus {
OK(200, "OK"),
UN_KNOWN(500, "未知錯(cuò)誤"),
RESULT_EMPTY(1000, "查詢結(jié)果為空!"),
;
private final Integer code;
private final String msg;
JsonResponseStatus(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
這個(gè)類是一個(gè)枚舉類,用于定義接口返回的 JSON 響應(yīng)狀態(tài)。它包含了一些常見(jiàn)的響應(yīng)狀態(tài),如 OK(200, "OK") 表示請(qǐng)求成功,UN_KNOWN(500, "未知錯(cuò)誤") 表示未知錯(cuò)誤,RESULT_EMPTY(1000, "查詢結(jié)果為空!") 表示查詢結(jié)果為空。每個(gè)響應(yīng)狀態(tài)都有一個(gè)對(duì)應(yīng)的狀態(tài)碼和消息。在接口的返回值中,可以使用這個(gè)枚舉類來(lái)表示具體的響應(yīng)狀態(tài)。?
JsonResponseBody
package com.csdn.security.resp;
import lombok.Data;
@Data
public class JsonResponseBody<T> {
private Integer code;
private String msg;
private T data;
private Long total;
private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data) {
this.code = jsonResponseStatus.getCode();
this.msg = jsonResponseStatus.getMsg();
this.data = data;
}
private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data, Long total) {
this.code = jsonResponseStatus.getCode();
this.msg = jsonResponseStatus.getMsg();
this.data = data;
this.total = total;
}
public static <T> JsonResponseBody<T> success() {
return new JsonResponseBody<T>(JsonResponseStatus.OK, null);
}
public static <T> JsonResponseBody<T> success(T data) {
return new JsonResponseBody<T>(JsonResponseStatus.OK, data);
}
public static <T> JsonResponseBody<T> success(T data, Long total) {
return new JsonResponseBody<T>(JsonResponseStatus.OK, data, total);
}
public static <T> JsonResponseBody<T> unknown() {
return new JsonResponseBody<T>(JsonResponseStatus.UN_KNOWN, null);
}
public static <T> JsonResponseBody<T> other(JsonResponseStatus jsonResponseStatus) {
return new JsonResponseBody<T>(jsonResponseStatus, null);
}
}
?這個(gè)類是一個(gè)通用的 JSON 響應(yīng)體類
- code:表示響應(yīng)狀態(tài)碼。
- msg:表示響應(yīng)消息。
- data:表示響應(yīng)數(shù)據(jù)的泛型對(duì)象。
- total:表示響應(yīng)數(shù)據(jù)的總數(shù)。
修改我們登陸成功后的操作,加入一個(gè)JSON字符串轉(zhuǎn)換類并將獲取到的對(duì)象輸出到前端。
//JSON格式轉(zhuǎn)換
@Autowired
private ObjectMapper objectMapper;
//設(shè)置登錄成功后重定向到那個(gè)頁(yè)面
.successHandler((req, resp, auth) -> {
objectMapper.writeValue(resp.getOutputStream(),auth.getPrincipal());
})
?重啟服務(wù)器登錄進(jìn)行測(cè)試
?可以看到我們的數(shù)據(jù)都響應(yīng)到前端了,前端人員就可以拿著數(shù)據(jù)進(jìn)行操作了
二、分布式鑒權(quán)
1.授權(quán)介紹
Spring Security 中的授權(quán)分為兩種類型:
-
基于角色的授權(quán):以用戶所屬角色為基礎(chǔ)進(jìn)行授權(quán),如管理員、普通用戶等,通過(guò)為用戶分配角色來(lái)控制其對(duì)資源的訪問(wèn)權(quán)限。
-
基于資源的授權(quán):以資源為基礎(chǔ)進(jìn)行授權(quán),如 URL、方法等,通過(guò)定義資源所需的權(quán)限,來(lái)控制對(duì)該資源的訪問(wèn)權(quán)限。
Spring Security 提供了多種實(shí)現(xiàn)授權(quán)的機(jī)制,最常用的是使用基于注解的方式,建立起訪問(wèn)資源和權(quán)限之間的映射關(guān)系。
其中最常用的兩個(gè)注解是 @Secured
和 @PreAuthorize
。@Secured
注解是更早的注解,基于角色的授權(quán)比較適用,@PreAuthorize
基于 SpEL
表達(dá)式的方式,可靈活定義所需的權(quán)限,通常用于基于資源的授權(quán)。
2.表設(shè)計(jì)
我們會(huì)用到以下五張表sys_user、sys_user_role、sys_role、sys_role_module、sys_module
sys_user(用戶表)
sys_user_role(用戶角色表)
sys_role(角色表)
?sys_role_module(角色權(quán)限表)
?sys_module(模塊表)
3.獲取用戶權(quán)限
我們?cè)跀?shù)據(jù)表都在一個(gè)服務(wù)器的情況下,可以直接進(jìn)行聯(lián)表查詢
select * from sys_user a,
sys_user_role b,
sys_role_module c,
sys_module d
where a.id=b.user_id and
b.role_id=c.role_id and
c.module_id=d.id
如果我們不是源于一個(gè)數(shù)據(jù)庫(kù)或者一個(gè)服務(wù)器,那么就要將數(shù)據(jù)在后端做處理,先在我們自定義身份驗(yàn)證類也就是MyUserDetailsService類,隨后將五個(gè)表的service實(shí)現(xiàn)類引入
?認(rèn)證身份的同時(shí)把權(quán)限查詢出來(lái)。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根據(jù)用戶名查詢數(shù)據(jù)庫(kù)中用戶信息
User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
//判斷用戶是否存在
if(Objects.isNull(user))
throw new UsernameNotFoundException("用戶不存在");
//權(quán)限校驗(yàn)TODO
//.stream().map()遍歷所有對(duì)象將新的數(shù)據(jù)放入流中
// .collect將流中的數(shù)據(jù)變成一個(gè)集合
//1.先查詢出用戶擁有的角色
List<Integer> userId = userRoleService.
list(new QueryWrapper<UserRole>()
.eq("user_id", user.getId()))
.stream().map(UserRole::getRoleId)
.collect(Collectors.toList());
//2.根據(jù)角色查詢角色名稱
List<String> roleId = roleService
.list(new QueryWrapper<Role>().in("role_id", userId))
.stream().map(Role::getRoleName)
.collect(Collectors.toList());
//3.根據(jù)角色查詢角色所擁有模塊ID
List<Integer> ModuleId = roleModuleService
.list(new QueryWrapper<RoleModule>().in("role_id", userId))
.stream().map(RoleModule::getModuleId)
.collect(Collectors.toList());
//4.根據(jù)模塊ID查詢模塊URL
List<String> urls = moduleService
.list(new QueryWrapper<Module>().in("id", ModuleId))
.stream().map(Module::getUrl)
.collect(Collectors.toList());
//將用戶角色名稱與模塊url放入一個(gè)集合
roleId.addAll(urls);
//認(rèn)證集合對(duì)象
List<SimpleGrantedAuthority> collect = roleId.stream().map(e -> {
return new SimpleGrantedAuthority(e);
}).collect(Collectors.toList());
//將認(rèn)證集合對(duì)象賦值給用戶實(shí)體
user.setAuthorities(collect);
return user;
}
?這里還有一個(gè)問(wèn)題需要我們解決,我們寫(xiě)完以上代碼進(jìn)行測(cè)試后發(fā)現(xiàn)有錯(cuò)誤
該錯(cuò)誤很簡(jiǎn)單出現(xiàn)在對(duì)模塊URL遍歷這里,因?yàn)槲覀兊臄?shù)據(jù)庫(kù)表中有兩個(gè)值是空的
第一個(gè)方法是加上url的值
第二個(gè)方法就是過(guò)濾
4.修改SpringSecurity配置類
當(dāng)我們想要開(kāi)啟spring
方法級(jí)安全時(shí),只需要在任何 @Configuration
實(shí)例上使用@EnableGlobalMethodSecurity
注解就能達(dá)到此目的。同時(shí)這個(gè)注解為我們提供了prePostEnabled
、securedEnabled
和 jsr250Enabled
三種不同的機(jī)制來(lái)實(shí)現(xiàn)同一種功能。
修改WebSecurityConfig
配置類,開(kāi)啟基于方法的安全認(rèn)證機(jī)制,也就是說(shuō)在web層的controller啟用注解機(jī)制的安全確認(rèn)。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
...
}
@EnableGlobalMethodSecurity
是Spring Security提供的一個(gè)注解,用于啟用方法級(jí)別的安全性。它可以在任何@Configuration類上使用,以啟用Spring Security的方法級(jí)別的安全性功能。它接受一個(gè)或多個(gè)參數(shù),用于指定要使用的安全注解類型和其他選項(xiàng)。以下是一些常用的參數(shù):
-
prePostEnabled
:如果設(shè)置為true
,則啟用@PreAuthorize
和@PostAuthorize
注解。默認(rèn)值為false
。 -
securedEnabled
:如果設(shè)置為true
,則啟用@Secured
注解。默認(rèn)值為false
。 -
jsr250Enabled
:如果設(shè)置為true
,則啟用@RolesAllowed
注解。默認(rèn)值為false
。 -
proxyTargetClass
:如果設(shè)置為true
,則使用CGLIB代理而不是標(biāo)準(zhǔn)的JDK動(dòng)態(tài)代理。默認(rèn)值為false
。
使用@EnableGlobalMethodSecurity
注解后,可以在應(yīng)用程序中使用Spring Security提供的各種注解來(lái)保護(hù)方法,例如@Secured
、@PreAuthorize
、@PostAuthorize
和@RolesAllowed
。這些注解允許您在方法級(jí)別上定義安全規(guī)則,以控制哪些用戶可以訪問(wèn)哪些方法。
注解介紹:
注解 | 說(shuō)明 |
---|---|
@PreAuthorize |
用于在方法執(zhí)行之前對(duì)訪問(wèn)進(jìn)行權(quán)限驗(yàn)證 |
@PostAuthorize |
用于在方法執(zhí)行之后對(duì)返回結(jié)果進(jìn)行權(quán)限驗(yàn)證 |
@Secured |
用于在方法執(zhí)行之前對(duì)訪問(wèn)進(jìn)行權(quán)限驗(yàn)證 |
@RolesAllowed |
是Java標(biāo)準(zhǔn)的注解之一,用于在方法執(zhí)行之前對(duì)訪問(wèn)進(jìn)行權(quán)限驗(yàn)證 |
5.控制Controller層接口權(quán)限
@PreAuthorize("hasAuthority('order:manager:list')")
@RequestMapping("/queryRoles")
public String queryRoles(){
return "管理員權(quán)限";
}
@PreAuthorize("hasAnyAuthority('book:manager:list','order:manager:list')")
@RequestMapping("/queryModules")
public String queryModules(){
return "管理員權(quán)限/用戶權(quán)限";
}
至此我們的鑒權(quán)就全部完成了
?
到這里我的分享就結(jié)束了,歡迎到評(píng)論區(qū)探討交流!!
??如果覺(jué)得有用的話還請(qǐng)點(diǎn)個(gè)贊吧 ??文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-786803.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-786803.html
到了這里,關(guān)于【Spring Security】分布式鑒權(quán)的使用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!