SpringSecurity角色權限控制(SpringBoot+SpringSecurity+JWT)
一、項目介紹
通過springboot整合jwt和security,以用戶名/密碼的方式進行認證和授權。認證通過jwt+數據庫的,授權這里使用了兩種方式,分別是SpringSecurity自帶的hasRole方法+SecurityConfig 和 我們自定義的permission+@PreAuthorize注解。
二、SpringSecurity簡介
SpringSecurity中的幾個重要組件:
1.SecurityContextHolder(class)
用來存儲和獲取當前線程關聯(lián)的 SecurityContext 對象的類。
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ku2An95n-1683728698707)(E:/Blog/lansg/source/img/1683186758072-ccd6a67a-c2c5-4ec7-aca3-b42404ce8277.png)]](http://img.jbzj.com/file_images/article/202405/2024050711265639.png)
其中有兩種 SecurityContext 模式:
- MODE_THREADLOCAL:將 SecurityContext 對象存儲到當前線程中,只在當前線程中可見。多線程時,每個線程的 SecurityContext 對象都是獨立的。
- MODE_INHERITABLETHREADLOCAL:將 SecurityContext 對象存儲到當前線程中,對當前線程和子線程都可見。也就是說,在當前線程中存儲的 SecurityContext 對象可以被傳遞給子線程使用。
表示用戶已通過身份驗證的最簡單方法就是設置 SecurityContextHolder!
2.SecurityContext(Interface)
用來存儲當前已經被認證的用戶,包含了當前執(zhí)行操作的線程上下文信息以及用戶認證和授權信息。包含Authentication。
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KFfG9auG-1683728698708)(E:/Blog/lansg/source/img/1683186616655-7d1408b0-f5aa-4a06-ae7e-417d63c3fe94.png)]](http://img.jbzj.com/file_images/article/202405/2024050711265640.png)
3.Authentication(Interface)
存儲了當前正在執(zhí)行操作的用戶的身份驗證信息,包括用戶名、密碼、權限,可以作為AuthenticationManager的輸入,它包含principal、credentials、authorities。
- principal:用戶的標識。通常情況下是UserDetails接口的一個實例。
- credentials:用戶的密碼。多數情況下,為確保密碼不被泄露,會在用戶身份驗證之后被清除。
- authorities:用戶擁有的權限。

Authentication 接口的常用實現類有以下幾種:
- AnonymousAuthenticationToken:匿名用戶的身份驗證信息。
- UsernamePasswordAuthenticationToken:用戶名/密碼的身份驗證信息。
- RememberMeAuthenticationToken:用于“記住我”功能的身份驗證信息。
- PreAuthenticatedAuthenticationToken:基于預先認證信息的身份驗證。
通常情況下,用戶在進行登錄時需要通過身份驗證,當身份驗證成功時,就會通過 Authentication 接口封裝用戶的身份信息。在后續(xù)的操作中,認證后的用戶可以通過 SecurityContextHolder 獲取 Authentication 對象,并根據其中的信息獲得用戶的身份信息及相應的權限等。
對比SecurityContext和Authentication:
Authentication 是一個封裝了用戶身份認證信息的對象,表示用戶已經通過了驗證。
SecurityContext 則是一個上下文類對象,用于保存和獲取當前線程關聯(lián)的上下文信息,包括了 Authentication 對象。

4.AuthenticationManager(Interface)
定義了用戶身份驗證的api接口(例如將用戶名和密碼和數據庫進行比對)??梢越邮找粋€Authentication對象作為入參,驗證成功后會返回已被驗證的Authentication對象。

5.GrantedAuthority(Interface)
授權信息以GrantedAuthority的形式存儲在Authentication對象中,GrantedAuthority接口表示一個授權(權限)對象,包含一個字符串類型的授權名字(authority name)。授權名字通常是一個表示權限的字符串,例如"ROLE_ADMIN"、"ROLE_USER"等
二、整理思路
- 搭建springboot項目,導入相關依賴
- 在數據庫導入sql創(chuàng)建用戶表
- 創(chuàng)建幾個關于User的對象便于數據傳輸
- dao層開發(fā)(對用戶信息增刪改查)
- 實現UserDetails接口和UserDetailService接口
- 自定義實現校驗token的攔截器JwtAuthenticationTokenFilter
- 自定義實現用戶登錄校驗的攔截器JWTAuthenticationFilter
- service層開發(fā)(包括PermissionService和UserService)
- 創(chuàng)建測試Controller
- 實現SpringSecurity的配置類SecurityConfig
- 通過postman進行測試
三、具體實現步驟
1.項目主要相關依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
2.用戶表
創(chuàng)建一個名為security_jwt_demo的數據庫,導入項目根目錄下/db/db.sql文件即可。
3.項目中用到的幾個user相關對象
(1)User實體
對應數據庫中的user表
@Data
public class User {
private Long id;
private String username;
private String password;
private String permission;
private String role;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", permission='" + permission + '\'' +
", role='" + role + '\'' +
'}';
}
}
(2)LoginUser
UserDetails的實現類,使用用戶名/密碼驗證時需要用到其作為返回值。這個類中包含了用戶名、密碼和當前登錄用戶所具備的權限。
@Data
public class LoginUser implements UserDetails {
private Long id;
private String username;
private String password;
//通過自定義方式進行授權
private Set<String> permissions = new HashSet<String>();
//通過springSecurity進行授權
private Collection<? extends GrantedAuthority> authorities;
public LoginUser(){}
public LoginUser(User user,Collection<? extends GrantedAuthority> authorities) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
permissions.add(user.getPermission());
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
}
(3)UserVO
主要用來登錄時從輸入流中獲取登錄用戶的信息,對應了前端傳遞的參數,包括用戶名、密碼、記住我等屬性。
@Data
public class UserVO {
private String username;
private String password;
private Integer rememberMe;
}
4.dao層開發(fā)
創(chuàng)建mapper接口并配置對應的mapper.xml
@Mapper
public interface UserMapper {
//根據用戶名獲取用戶
User getByName(String username);
//根據用戶id獲取用戶權限
List<String> getPermissionById(Long id);
//新增一個用戶
int insertUser(User user);
}
5.實現UserDetail接口和UserDetailService接口
首先說一下為什么要實現這兩個接口。
SpringSecurity提供了多種身份驗證的方式,在這里我們使用的是用戶名/密碼的方式進行驗證,而如果要使用這種方式進行驗證的話,我們需要實現UserDetailService接口中的loadUserByUsername方法,這個方法用來從數據庫中進行查詢用戶,然后和傳入的用戶密碼進行比對。這個方法會返回一個UserDetails對象。
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getByName(username);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
return new LoginUser(user,authorities);
}
}
對于UserDetailService接口,SpringSecurity提供了基于內存和JDBC的兩種驗證方式,默認是JDBC的方式。我們也可以通過自定義實現UserDetailService接口,來達到自定義身份驗證的結果。這里我們使用的是自定義身份驗證的方式。
對于UserDetails,在Spring Security中,UserDetails接口是表示用戶信息的規(guī)范。該接口表示應用程序中的用戶,并提供有關用戶的基本信息,如用戶名、密碼、角色、權限等,因此我們需要有一個類似用戶的對象來實現該接口(LoginUser)。
6.JwtAuthenticationTokenFilter
檢驗用戶token的過濾器。對于客戶端發(fā)出的請求,首先對用戶的token進行校驗,如果token不合法表示當前用戶未登錄,繼續(xù)執(zhí)行其他過濾器的邏輯;如果token合法則設置SecurityContextHolder表示用戶已被認證。
/**
* token過濾器 驗證token有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
if (StringUtils.isBlank(token) || !token.startsWith(JwtTokenUtil.TOKEN_PREFIX)){
chain.doFilter(request,response);
return;
}
try {
//如果能獲取到token則Authentication進行設置,表示已認證
SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
} catch (Exception e) {
e.printStackTrace();
}
//繼續(xù)執(zhí)行其他過濾器的邏輯
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws Exception {
String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX,"");
//判斷token是否過期
boolean expiration = JwtTokenUtil.isExpiration(token);
if (expiration){
throw new Exception("過期了");
}else{
String username = JwtTokenUtil.getUsername(token);
User user = userMapper.getByName(username);
List<String> permissions = userMapper.getPermissionById(user.getId());
LoginUser loginUser = new LoginUser(user, Collections.singleton(new SimpleGrantedAuthority(user.getRole())));
loginUser.setPermissions(new HashSet<>(permissions));
//新建一個UsernamePasswordAuthenticationToken用來設置Authentication
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
return authenticationToken;
}
}
}
7.JWTAuthenticationFilter
用戶登錄時對用戶名密碼進行校驗的過濾器,在token校驗過濾器之后執(zhí)行。在該過濾器中會對用戶名密碼進行比對,校驗成功后返回一個token給客戶端,下次客戶端訪問時在請求頭帶上此token代表該用戶已經被認證。
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 從輸入流中獲取到登錄的信息
try {
UserVO vo = new ObjectMapper().readValue(request.getInputStream(), UserVO.class);
rememberMe.set(vo.getRememberMe() == null ? 0 : vo.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(vo.getUsername(), vo.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 成功驗證后調用的方法
// 如果驗證成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authResult.getPrincipal();
System.out.println("loginUser:" + loginUser.toString());
boolean isRemember = rememberMe.get() == 1;
String role = "";
String token = JwtTokenUtil.createToken(loginUser.getUsername(), role, isRemember);
/* 返回創(chuàng)建成功的token
但是這里創(chuàng)建的token只是單純的token
按照jwt的規(guī)定,最后請求的時候應該是 `Bearer token`*/
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
8.service層開發(fā)
由于業(yè)務邏輯比較簡單,我們在service層中主要實現自定義授權的邏輯,用戶相關的Service不做實現。
自定義授權的實現如下,首先在數據庫的user表中有個string類型的permission字段,代表用戶所擁有的權限。在進行授權時檢查用戶權限屬性是否包含該權限,如果包含則表示當前用戶具有訪問權限。
/**
* 自定義權限實現,ss取自SpringSecurity首字母
*/
@Service("ss")
public class PermissionService {
public boolean hasPer(String permission) throws Exception {
if (StringUtils.isBlank(permission)){
return false;
}
LoginUser loginUser = SecurityUtil.getLoginUser();
if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return loginUser.getPermissions().contains(StringUtils.trim(permission));
}
}
9.Controller層開發(fā)
我們用到的Controller主要有兩個,一個是用戶相關的,一個是進行測試的接口。
對于我們進行授權測試的接口,在使用自定義授權邏輯時(PermissionService),要配合@PreAuthorize注解實現(createJob方法),對應的JobController如下:
@RestController
@RequestMapping("/jobs")
public class JobController {
@GetMapping("/list")
public String listJobs(){
System.out.println("接收到請求...");
return "展示所有任務";
}
//通過PermissionService自定義授權實現
@PostMapping("/create")
@PreAuthorize("@ss.hasPer('job:add')")
public String createJob(){
return "創(chuàng)建一個新任務";
}
//通過SpringSecurity配合用戶角色(role字段)實現權限管理
@DeleteMapping("/delete")
public String deleteJob(){
return "刪除一個任務";
}
}
對于用戶相關的Controller,我們只需要寫一個注冊方法就行了,如下所示:
@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserMapper userMapper;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/register")
public String register(@RequestBody Map<String,String> registerUser){
User user = new User();
user.setUsername(registerUser.get("username"));
//對密碼進行一下加密
user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password")));
user.setPermission(registerUser.get("permission"));
user.setRole(registerUser.get("role"));
userMapper.insertUser(user);
return user.toString();
}
}
為什么不需要注冊接口呢?是因為UsernamePasswordAuthenticationFilter已經幫我們實現了,默認是"/login"
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
這里我們也可以對這個路徑進行修改,如下所示:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
10.通過SecurityConfig類對SpringSecurity進行配置
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
@Qualifier("userDetailServiceImpl")
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
// 測試用資源,需要驗證了的用戶才能訪問
.antMatchers("/jobs/create").authenticated()
//只有角色為admin的用戶才能進行刪除
.antMatchers(HttpMethod.DELETE,"/jobs/delete").hasRole("ADMIN")
// 其他請求都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//將驗證token的過濾器添加在驗證用戶名/密碼的過濾器之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource(){
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
到這里就全部完成了!讓我們測試一下是否生效。
11.postman測試
這是數據庫表中原有的角色和權限:

(1)測試注冊接口

可以看到是可以注冊成功的,但是我們這里使用原有user1和user2進行權限測試(偷個懶)
(2)測試登錄接口
由于我們之前將原有的登錄接口從"/login"改為了"/auth/login",這里需要注意一下。

登錄成功后的ResponseBody是空的,響應頭中有token代表已經登錄成功了。我們需要從header中獲取該token,后續(xù)請求需要用到。這里展示了user2,user1也是一樣的。

(3)測試創(chuàng)建任務接口
由于創(chuàng)建任務需要"job:add"權限,查看數據庫user2是有該權限的,user1沒有。
將剛才登錄獲得的token添加到參數中:

然后發(fā)現就可以創(chuàng)建成功了

(4)測試刪除任務接口
刪除任務需要用戶角色是admin,故user2是無法進行刪除的。

(5)我們按照剛才的流程對user1進行測試,來達到對比的效果。
登錄:

創(chuàng)建任務:

刪除任務:

對于查詢接口不需要進行認證和授權,也就是說不需要登錄就能訪問:

到此這篇關于SpringSecurity角色權限控制(SpringBoot+SpringSecurity+JWT)的文章就介紹到這了,更多相關SpringSecurity角色權限內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺談java中類名.class, class.forName(), getClass()的區(qū)別
下面小編就為大家?guī)硪黄獪\談java中類名.class, class.forName(), getClass()的區(qū)別。小編覺得挺不錯的,現在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05
java并發(fā)編程專題(十)----(JUC原子類)基本類型詳解
這篇文章主要介紹了java JUC原子類基本類型詳解的相關資料,文中示例代碼非常詳細,幫助大家更好的理解和學習,感興趣的朋友可以了解下2020-07-07
Spring Boot 基于注解的 Redis 緩存使用詳解
本篇文章主要介紹了Spring Boot 基于注解的 Redis 緩存使用詳解,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05
IntelliJ IDEA AI Assistant 攜帶OpenCode保姆級
本文介紹了JetBrains官方AI插件AIAssistant的安裝、使用及接入本地客戶端的過程,通過安裝、測試、接入本地客戶端等步驟,詳細描述了使用過程中的注意事項和操作方法,感興趣的朋友跟隨小編一起看看吧2026-04-04
Java創(chuàng)建可執(zhí)行JAR文件的多種方式
本文主要介紹了Java創(chuàng)建可執(zhí)行JAR文件的多種方式,使用JDK的jar工具、IDE、Maven和Gradle來創(chuàng)建和配置可執(zhí)行JAR文件,具有一定的參考價值,感興趣的可以了解一下2024-07-07

