springboot3整合SpringSecurity實(shí)現(xiàn)登錄校驗(yàn)與權(quán)限認(rèn)證
目前市面上常用的安全框架有:
Spring Security、Shiro,還有一個(gè)國(guó)人開發(fā)的框架目前也備受好評(píng):SaToken
但是與spring boot項(xiàng)目融合度最高的還是Spring Security,所以目前我們講解一下基于spring boot項(xiàng)目來整合spring security來實(shí)現(xiàn)常用的登錄校驗(yàn)與權(quán)限認(rèn)證;
Spring Security(安全框架)
1、介紹
Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。
如果項(xiàng)目中需要進(jìn)行權(quán)限管理,具有多個(gè)角色和多種權(quán)限,我們可以使用Spring Security。
采用的是責(zé)任鏈的設(shè)計(jì)模式,是一堆過濾器鏈的組合,它有一條很長(zhǎng)的過濾器鏈。
2、功能
Authentication (認(rèn)證),就是用戶登錄
Authorization (授權(quán)),判斷用戶擁有什么權(quán)限,可以訪問什么資源
安全防護(hù),跨站腳本攻擊,session攻擊等
非常容易結(jié)合Springboot項(xiàng)目進(jìn)行使用,本次就著重與實(shí)現(xiàn)認(rèn)證和授權(quán)這兩個(gè)功能
版本spring boot3.1.16、spring security6.x
身份認(rèn)證:
1、創(chuàng)建一個(gè)spring boot項(xiàng)目,并導(dǎo)入一些初始依賴:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.21</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2、由于我們加入了 spring-boot-starter-security 的依賴,所以security就會(huì)自動(dòng)生效了。這時(shí)直接編寫一個(gè)controller控制器,并編寫一個(gè)接口進(jìn)行測(cè)試:
可以看到我們?cè)谠L問這個(gè)接口時(shí)出現(xiàn)了攔截,必須要我們進(jìn)行登錄之后才能訪問;
那么接下來我們就來實(shí)現(xiàn)第一個(gè)功能:用戶登錄認(rèn)證;
3、自定義用戶的登錄認(rèn)證:
Spring Security 6.x 的認(rèn)證實(shí)現(xiàn)流程如下:
用戶提交登錄請(qǐng)求
Spring Security 將請(qǐng)求交給 UsernamePasswordAuthenticationFilter 過濾器處理。
UsernamePasswordAuthenticationFilter 獲取請(qǐng)求中的用戶名和密碼,并生成一個(gè) AuthenticationToken 對(duì)象,將其交給 AuthenticationManager 進(jìn)行認(rèn)證。
AuthenticationManager 通過 UserDetailsService 獲取用戶信息,然后使用 PasswordEncoder 對(duì)用戶密碼進(jìn)行校驗(yàn)。
如果密碼正確,AuthenticationManager 會(huì)生成一個(gè)認(rèn)證通過的 Authentication 對(duì)象,并返回給 UsernamePasswordAuthenticationFilter 過濾器。如果密碼不正確,則 AuthenticationManager 拋出一個(gè) AuthenticationException 異常。
UsernamePasswordAuthenticationFilter 將 Authentication 對(duì)象交給 SecurityContextHolder 進(jìn)行管理,并調(diào)用 AuthenticationSuccessHandler 處理認(rèn)證成功的情況。
如果認(rèn)證失敗,UsernamePasswordAuthenticationFilter 會(huì)調(diào)用 AuthenticationFailureHandler 處理認(rèn)證失敗的情況。
看起來有點(diǎn)復(fù)雜,其實(shí)寫起來很簡(jiǎn)單的。spring security的底層就是一堆的過濾器來是實(shí)現(xiàn)的,而我們只需要編寫一些重要的過濾器即可,其他的就用spring security默認(rèn)的實(shí)現(xiàn),只要不影響我們正常的登錄功能即可。
(創(chuàng)建一個(gè)用戶表用來進(jìn)行登錄實(shí)現(xiàn),注意這個(gè)表中的用戶名不能重復(fù),我們將用戶名作為每一個(gè)用戶的唯一憑證,就如同人的手機(jī)號(hào)或者身份證號(hào)一樣)表的結(jié)構(gòu)非常簡(jiǎn)單,一些配置我這里就不在描述了(實(shí)體類、mapper、service、controller等)
認(rèn)證的實(shí)現(xiàn)流程:
1、創(chuàng)建一個(gè)MyUserDetailsService類來實(shí)現(xiàn)SpringSecurity的UserDetailsService接口
(這里進(jìn)行用戶登錄和授權(quán)的邏輯處理)
UserDetailsService:此接口中定義了登錄服務(wù)方法,用來實(shí)現(xiàn)登錄邏輯。方法的返回值是UserDetails,也是spring security框架定義中的一個(gè)接口,用來存儲(chǔ)用戶信息,我們可以自定義一個(gè)類用來實(shí)現(xiàn)這個(gè)接口,將來返回的時(shí)候就返回我們自定義的用戶實(shí)體類。
實(shí)現(xiàn)UserDetailsService接口
@Component public class MyUserDetailsService implements UserDetailsService { /* * UserDetailsService:提供查詢用戶功能,如根據(jù)用戶名查詢用戶,并返回UserDetails *UserDetails,SpringSecurity定義的類, 記錄用戶信息,如用戶名、密碼、權(quán)限等 * */ @Autowired private SysUserMapper sysUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據(jù)用戶名從數(shù)據(jù)庫中查詢用戶 SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>() .eq(username != null, SysUser::getUsername, username)); if (sysUser==null){ throw new UsernameNotFoundException("用戶不存在"); } MySysUserDetails mySysUserDetails=new MySysUserDetails(sysUser); return mySysUserDetails; } }
(在原有數(shù)據(jù)庫表的基礎(chǔ)上)實(shí)現(xiàn)UserDetails接口:
@Data @AllArgsConstructor @NoArgsConstructor public class MySysUserDetails implements UserDetails { private Integer id; private String username; private String password; // 用戶擁有的權(quán)限集合,我這里先設(shè)置為null,將來會(huì)再更改的 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } public MySysUserDetails(SysUser sysUser) { this.id = sysUser.getId(); this.username = sysUser.getUsername(); this.password = sysUser.getPassword(); } // 后面四個(gè)方法都是用戶是否可用、是否過期之類的。我都設(shè)置為true @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
2、通過配置類對(duì)AuthenticationManager與自定義的UserDetails和PasswordEncoder進(jìn)行關(guān)聯(lián)
Spring Security是通過AuthenticationManager實(shí)現(xiàn)的認(rèn)證,會(huì)借此來判斷用戶名和密碼的正確性
密碼解析器spring security框架定義的接口:PasswordEncoder
spring security框架強(qiáng)制要求,必須在spring容器中存在PasswordEncoder類型對(duì)象,且對(duì)象唯一
@Configuration @EnableWebSecurity //開啟webSecurity服務(wù) public class SecurityConfig { @Autowired private MyUserDetailsService myUserDetailsService; @Bean public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){ DaoAuthenticationProvider provider=new DaoAuthenticationProvider(); //將編寫的UserDetailsService注入進(jìn)來 provider.setUserDetailsService(myUserDetailsService); //將使用的密碼編譯器加入進(jìn)來 provider.setPasswordEncoder(passwordEncoder); //將provider放置到AuthenticationManager 中 ProviderManager providerManager=new ProviderManager(provider); return providerManager; } /* * 在security安全框架中,提供了若干密碼解析器實(shí)現(xiàn)類型。 * 其中BCryptPasswordEncoder 叫強(qiáng)散列加密??梢员WC相同的明文,多次加密后, * 密碼有相同的散列數(shù)據(jù),而不是相同的結(jié)果。 * 匹配時(shí),是基于相同的散列數(shù)據(jù)做的匹配。 * Spring Security 推薦使用 BCryptPasswordEncoder 作為密碼加密和解析器。 * */ @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
3、在登錄方法所在的類中注入AuthenticationManager,調(diào)用authenticate實(shí)現(xiàn)認(rèn)證邏輯,并且在認(rèn)證之后返回認(rèn)證過的用戶信息:
controller層:
// 用戶登錄 @PostMapping("/login") public String login(@RequestBody LoginDto loginDto){ String token= sysUserService.login(loginDto); return token; }
對(duì)應(yīng)的service層的方法:
在這之前,介紹一個(gè)非常重要的類:UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken是Spring Security中用于表示基于用戶名和密碼的身份驗(yàn)證令牌的類。它主要有以下兩個(gè)構(gòu)造方法:
UsernamePasswordAuthenticationToken(Object principal, Object credentials)
- principal參數(shù)表示認(rèn)證主體,通常是用戶名或用戶對(duì)象。在身份驗(yàn)證過程中,這通常是用來標(biāo)識(shí)用戶的信息,可以是用戶名、郵箱等。
- credentials參數(shù)表示憑據(jù),通常是用戶的密碼或其他憑證信息。在身份驗(yàn)證過程中,這用于驗(yàn)證用戶的身份。
UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
- 除了上述兩個(gè)參數(shù)外,這個(gè)構(gòu)造方法還接受一個(gè)授權(quán)權(quán)限集合(authorities參數(shù))。這個(gè)集合表示用戶所擁有的權(quán)限,通常是一個(gè)包含用戶權(quán)限信息的集合。
- GrantedAuthority接口代表了用戶的權(quán)限信息,可以通過該接口的實(shí)現(xiàn)類來表示用戶具體的權(quán)限。
這兩個(gè)構(gòu)造方法的作用是創(chuàng)建一個(gè)包含用戶身份信息、憑據(jù)信息和權(quán)限信息的身份驗(yàn)證令牌,以便在Spring Security中進(jìn)行身份驗(yàn)證和授權(quán)操作。通過這些構(gòu)造方法,可以將用戶的相關(guān)信息封裝成一個(gè)完整的身份驗(yàn)證對(duì)象,方便在安全框架中進(jìn)行處理和驗(yàn)證。
總之,UsernamePasswordAuthenticationToken是在Spring Security中用于表示用戶名密碼身份驗(yàn)證信息的重要類,通過不同的構(gòu)造方法可以滿足不同場(chǎng)景下的需求。
編寫具體的登錄方法,創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken對(duì)象,并傳入相應(yīng)的用戶名和密碼;注入一個(gè)AuthenticationManager的bean,這個(gè)bean是spring security封裝的用來進(jìn)行認(rèn)證的類,調(diào)用這個(gè)類的authenticate方法并傳入U(xiǎn)sernamePasswordAuthenticationToken對(duì)象;
@Autowired private AuthenticationManager authenticationManager; // 登錄接口的具體實(shí)現(xiàn) @Override public String login(LoginDto loginDto) { // 傳入用戶名和密碼 UsernamePasswordAuthenticationToken usernamePassword = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword()); //是實(shí)現(xiàn)登錄邏輯,此時(shí)就回去調(diào)用LoadUserByUsername方法 Authentication authenticate = authenticationManager.authenticate(usernamePassword); // 獲取返回的用戶信息 Object principal = authenticate.getPrincipal(); //強(qiáng)轉(zhuǎn)為MySysUserDetails類型 MySysUserDetails mySysUserDetails = (MySysUserDetails) principal; // 輸出用戶信息 System.err.println(mySysUserDetails); //返回token String token= UUID.randomUUID().toString(); return token; }
我在test類中設(shè)置一些用戶數(shù)據(jù),并進(jìn)行測(cè)試;
@Autowired private SysUserMapper sysUserMapper; @Autowired private PasswordEncoder passwordEncoder; @Test void contextLoads() { //導(dǎo)入了一個(gè)用戶 SysUser sysUser=new SysUser(); sysUser.setUsername("zhangsan"); sysUser.setPassword(passwordEncoder.encode("123456")); sysUserMapper.insert(sysUser); }
這里我們已經(jīng)寫好了自定義的登錄流程,將項(xiàng)目運(yùn)行起來(我同時(shí)還寫了一個(gè)普通的test方法,類型是get,用來一起測(cè)試)
訪問http://localhost:8080/test
這是我們寫的一個(gè)普通的get方法,我們明明訪問的是http://localhost:8080/test這個(gè)路徑,但是卻自動(dòng)跳轉(zhuǎn)到了Spring Security提供的默認(rèn)的登錄頁面;這是因?yàn)镾pring Security默認(rèn)所有的請(qǐng)求都要先登錄才行,我們?cè)谶@里登錄之后就可以繼續(xù)訪問test頁面了;
(由于我們已經(jīng)實(shí)現(xiàn)了UserDetailsService接口,并且在用戶表中導(dǎo)入了一條用戶數(shù)據(jù),那么,這里的用戶名和密碼就是我們?cè)跀?shù)據(jù)庫中存儲(chǔ)的用戶名和密碼)
登錄成功之后,我們就可以訪問到test的信息了:
既然這個(gè)test請(qǐng)求要先進(jìn)行攔截認(rèn)證才能訪問,那么,我們剛才編寫的登錄接口sys-user/login豈不是也要先進(jìn)行攔截認(rèn)證才能訪問,這就與我們編寫登錄接口的初衷違背了,我們這個(gè)接口就是用來登陸的,現(xiàn)在還要先登錄認(rèn)證,之后再訪問這個(gè)登錄接口。那么有沒有一種方法,不使用SpringSecurity默認(rèn)的登錄頁面呢,使我們編寫的登錄接口所有人都可以直接訪問呢?
4、使用(SecurityFilterChain)過濾器, 配置用戶登錄的接口可以暴露出來,被所有人都正常的訪問(還應(yīng)在暴露一個(gè)注冊(cè)接口,但我這里就先不寫了)
還是在第二步設(shè)置的SecurityConfig類中設(shè)置過濾器:
在spring security6.x版本之后,原先經(jīng)常用的and()方法被廢除了,現(xiàn)在spring官方推薦使用Lambda表達(dá)式的寫法。
(因?yàn)槲覀兘酉聛硪M(jìn)行測(cè)試,所以禁用CSRF保護(hù),CSRF(Cross-Site Request Forgery)是一種攻擊方式,攻擊者通過偽造用戶的請(qǐng)求來執(zhí)行惡意操作。)
/* * 配置權(quán)限相關(guān)的配置 * 安全框架本質(zhì)上是一堆的過濾器,稱之為過濾器鏈,每一個(gè)過濾器鏈的功能都不同 * 設(shè)置一些鏈接不要攔截 * */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 關(guān)閉csrf httpSecurity.csrf(it->it.disable()); httpSecurity.authorizeHttpRequests(it-> it.requestMatchers("/sys-user/login").permitAll() //設(shè)置登錄路徑所有人都可以訪問 .anyRequest().authenticated() //其他路徑都要進(jìn)行攔截 ); return httpSecurity.build(); }
5、將項(xiàng)目運(yùn)行起來(我同時(shí)還寫了一個(gè)普通的test方法,類型是get,沒有放行,用于測(cè)試能不能攔截到):
訪問test請(qǐng)求:遇到攔截,說明我們的配置生效了
訪問登錄頁面:能正常訪問,且密碼正確,返回了一個(gè)我們自己生成的一個(gè)token。
6、自定義一個(gè)登錄頁面:
SpringSecurity雖然默認(rèn)有一個(gè)登錄頁面,但是我們一般情況下還是用我們自己寫的登錄頁面,這樣可操作性就大了很多;
引入thymeleaf依賴,我們直接在idea項(xiàng)目中建立一個(gè)登錄頁面;
編寫一個(gè)登錄頁面,主要是完成用戶的登錄,同時(shí)我們也不再需要頻繁的使用postman進(jìn)行測(cè)試了:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" xmlns:th="http://www.thymeleaf.org"> <title>自定義的登錄頁面</title> </head> <body> <form action="/sys-user/login" method="post"> 用戶名: <input type="text" name="username" > <br> 密碼: <input type="password" name="password"> <br> <input type="submit" value="登錄"> </form> </body> </html>
這是一個(gè)簡(jiǎn)單的登錄頁面,就指定了用戶名和密碼。
并且指定from表單的提交路徑為我們自定義的登錄接口;將這個(gè)頁面放在resource/templates目錄下,方便我們將來的調(diào)用;
HTML中的form表單默認(rèn)情況下會(huì)將數(shù)據(jù)格式化為key-value形式,而不是JSON格式。
也就是說我們剛剛寫的自定義登錄接口時(shí)是用@RequestBody接受收json類型的數(shù)據(jù),這肯定是接受不到的,有兩種方法實(shí)現(xiàn):
1、直接用@RequestParam("username") ,@RequestParam("password")接收這兩個(gè)參數(shù)
2、@ModelAttribute
注解:@ModelAttribute("formData") User user //在@ModelAttribute注解內(nèi)寫表單的id,還能使用對(duì)象進(jìn)行接收
我們也可以在前端將from表單的數(shù)據(jù)轉(zhuǎn)化為json之后,在進(jìn)行發(fā)送,但那樣需要寫js,我就直接在后端改一下了。
還是使用使用(SecurityFilterChain)過濾器,指定我們自定義的登錄表單路徑,(解釋一下fromLogin方法):
formLogin 方法是 Spring Security 中用于配置基于表單的登錄認(rèn)證的一種方式。它通常用于傳統(tǒng)的 Web 應(yīng)用程序,其中前端頁面由后端動(dòng)態(tài)生成,并且用戶在頁面中輸入用戶名和密碼來進(jìn)行登錄。在這種情況下,Spring Security 負(fù)責(zé)處理登錄請(qǐng)求、驗(yàn)證用戶身份、生成會(huì)話等操作。
/* * 配置權(quán)限相關(guān)的配置 * 安全框架本質(zhì)上是一堆的過濾器,稱之為過濾器鏈,每一個(gè)過濾器鏈的功能都不同 * 設(shè)置一些鏈接不要攔截 * */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { // 關(guān)閉csrf httpSecurity.csrf(it->it.disable()); // 配置路徑相關(guān) httpSecurity.authorizeHttpRequests(it-> it.requestMatchers("/login","sys-user/login").permitAll() //設(shè)置登錄路徑所有人都可以訪問 .anyRequest().authenticated() //其他路徑都要進(jìn)行攔截 ); //表單 httpSecurity.formLogin(from-> from.loginPage("/login") //跳轉(zhuǎn)到自定義的登錄頁面 .loginProcessingUrl("/sys-user/login") //處理前端的請(qǐng)求,與from表單的action一致即可 .defaultSuccessUrl("/index") //默認(rèn)的請(qǐng)求成功之后的跳轉(zhuǎn)頁面,直接訪問登錄頁面 ); return httpSecurity.build(); }
注意,這里還需要將/login這個(gè)接口進(jìn)行放行。
我們知道,不能直接訪問login.html這個(gè)自定義的登錄頁面,但是我們可以使用路徑映射。先寫一個(gè)login的get請(qǐng)求,并將這個(gè)請(qǐng)求映射到login.html頁面。
.defaultSuccessUrl("/index"):這個(gè)方法是我們默認(rèn)的登錄成功之后跳轉(zhuǎn)的請(qǐng)求地址。
如果你之前有請(qǐng)求的地址,但是這個(gè)地址沒有放行或者你沒有登錄,那么會(huì)自動(dòng)跳轉(zhuǎn)到我們自定義的登錄頁面,完成登錄之后,會(huì)跳轉(zhuǎn)到你最先訪問的地址;如果你直接訪問的就是/login登錄地址,那么默認(rèn)的登錄成功之后跳轉(zhuǎn)到我們指定的地址:/index
@Controller public class Login { @GetMapping("/login") public String login(){ System.out.println("用戶進(jìn)入登錄頁面"); return "login"; //沒使用json返回,直接映射到自定義登錄的頁面 } @GetMapping("/index") @ResponseBody public String index(){ return "用戶登錄成功"; } }
現(xiàn)在我們已經(jīng)自定義了一個(gè)登錄頁面,將項(xiàng)目啟動(dòng)起來進(jìn)行測(cè)試:
我訪問/test地址,這個(gè)地址沒有放行,而且我們這是沒有登錄,那么會(huì)自動(dòng)跳轉(zhuǎn)到我們自定義的登錄頁面:
我們進(jìn)行登錄之后,會(huì)跳轉(zhuǎn)到/test請(qǐng)求地址:
可以看到我們的結(jié)果與我們?cè)O(shè)想的一樣:
現(xiàn)在我們直接訪問/login登錄頁面:可以看到返回了/index頁面的內(nèi)容(這個(gè)是我們?cè)O(shè)置的默認(rèn)登錄成功之后返回的頁面)
7、退出接口
需要注意的是在Spring Security中,沒有專門用于處理退出失敗的接口。退出(注銷)操作通常是由瀏覽器發(fā)起的,Spring Security會(huì)攔截注銷請(qǐng)求并執(zhí)行相應(yīng)的注銷邏輯。
退出操作通常是通過調(diào)用SecurityContextLogoutHandler
來完成的,它會(huì)清除用戶的安全上下文,包括認(rèn)證信息和會(huì)話信息。
在security框架中,默認(rèn)提供了退出登陸的功能。請(qǐng)求地址是 /lohout 此為默認(rèn)值,可以通過配置進(jìn)行修該。直接請(qǐng)求 /logout ,會(huì)實(shí)現(xiàn)自動(dòng)退出登錄邏輯(默認(rèn)的/logout接收get、和post請(qǐng)求)
退出登陸時(shí),會(huì)清楚內(nèi)存中的登錄用戶主體信息,銷毀會(huì)話對(duì)象等等。
自定義退出接口:
httpSecurity.logout(logout->{ logout.logoutUrl("/user/login") //自定義退出接口 .logoutSuccessHandler(logoutSuccess); //退出成功之后的邏輯 });
編寫退出成功之后的邏輯,我們可以在這里刪除掉redis中的數(shù)據(jù),清除登錄的上下文,設(shè)置返回的信息等等....(如果是前后端分離狀態(tài)下的spring security,這些工作都可以在自定義的退出接口中進(jìn)行實(shí)現(xiàn)。如果是前后端不分離的表單式登錄,還是使用傳統(tǒng)的Cookie和Session來進(jìn)行用戶信息的保存,我們自需要調(diào)用ogout.logoutUrl("/user/login") 方法來指定退出路徑即可,退出的邏輯不需要我們來實(shí)現(xiàn)。)
@Component public class LogoutSuccess implements LogoutSuccessHandler { @Resource private RedisTemplate<String,String> redisTemplate; /* * 登錄成功之后的邏輯 * */ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { String token = request.getHeader("token"); // 刪除redis中的數(shù)據(jù) redisTemplate.delete(token); Map<String,Object> map=new HashMap<>(); map.put("msg","退出成功"); map.put("code",200); response.getWriter().write(JSON.toJSONString(map)); response.setContentType("application/json;charset=utf-8"); } }
權(quán)限校驗(yàn):
我們費(fèi)了很多功夫完成了身份認(rèn)證,權(quán)限校驗(yàn)相對(duì)來說是比較簡(jiǎn)單的:
首先,我先解釋一下角色與權(quán)限在SpringSecurity中的作用:
角色(Role):角色是一組權(quán)限的集合,通常代表著用戶的身份或職責(zé)。在Spring Security中,可以通過配置將角色分配給用戶或者用戶組,以此來控制用戶對(duì)系統(tǒng)資源的訪問。例如,管理員擁有添加、刪除和修改用戶的權(quán)限,而普通用戶只能查看自己的信息。
權(quán)限(Permission):權(quán)限是指對(duì)某一特定資源的訪問控制,例如讀寫文件、訪問數(shù)據(jù)庫等。在Spring Security中,通常使用“資源-操作”命名方式來定義權(quán)限,例如“/admin/* - GET”表示允許訪問以/admin/開頭的所有URL的GET請(qǐng)求??梢詫?quán)限分配給角色,也可以將其分配給單獨(dú)的用戶。
角色與權(quán)限之間的關(guān)系是多對(duì)多的;
建立兩張簡(jiǎn)單的表;一張用來存放角色、一張用來存放權(quán)限
角色表:
權(quán)限表:
這里建立的兩張表只是用來進(jìn)行測(cè)試,正常的數(shù)據(jù)不可能這么少的。建立相應(yīng)的實(shí)體類;
SpringSecurity要求將身份認(rèn)證信息存到GrantedAuthority對(duì)象列表中。代表了當(dāng)前用戶的權(quán)限。 GrantedAuthority對(duì)象由AuthenticationManager插入到Authentication對(duì)象中,然后在做出授權(quán)決策 時(shí)由AccessDecisionManager實(shí)例讀取。 GrantedAuthority 接口只有一個(gè)方法
AuthorizationManager實(shí)例通過該方法來獲得GrantedAuthority。通過字符串的形式表示, GrantedAuthority可以很容易地被大多數(shù)AuthorizationManager實(shí)現(xiàn)讀取。如果GrantedAuthority不 能精確地表示為String,則GrantedAuthorization被認(rèn)為是復(fù)雜的,getAuthority()必須返回null
告知權(quán)限的流程:
直接在登錄時(shí)查詢用戶的權(quán)限,并放在我們自定義的實(shí)現(xiàn)了UserDetail的接口類中,用來表示登錄用戶的全部信息;
在MySysUserDetails類中加入兩個(gè)屬性,記錄從數(shù)據(jù)庫中查處的角色和權(quán)限信息
我這里就簡(jiǎn)單一點(diǎn),不在做多表關(guān)聯(lián)查詢了。直接把zhangsan用戶設(shè)置為超級(jí)管理員,擁有所有權(quán)限;lisi用戶設(shè)置為普通管理員,擁有基本權(quán)限。
在MyUserDetailsService中實(shí)現(xiàn)用戶權(quán)限的賦值:
@Component public class MyUserDetailsService implements UserDetailsService { /* * UserDetailsService:提供查詢用戶功能,如根據(jù)用戶名查詢用戶,并返回UserDetails *UserDetails,SpringSecurity定義的類, 記錄用戶信息,如用戶名、密碼、權(quán)限等 * */ @Autowired private SysUserMapper sysUserMapper; @Autowired private SysRoleMapper sysRoleMapper; @Autowired private SysPermissionsMapper sysPermissionsMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根據(jù)用戶名從數(shù)據(jù)庫中查詢用戶 SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>() .eq(username != null, SysUser::getUsername, username)); if (sysUser==null){ throw new UsernameNotFoundException("用戶不存在"); } MySysUserDetails mySysUserDetails=new MySysUserDetails(sysUser); if ("zhangsan".equals(username)){ //zhangsan用戶是超級(jí)管理員,擁有一切權(quán)限 SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleName, "超級(jí)管理員")); Set<SysRole> roles=new HashSet<>(); roles.add(sysRole); mySysUserDetails.setRoles(roles); SysPermissions sysPermissions = sysPermissionsMapper.selectById(1); Set<String> permissions=new HashSet<>(); permissions.add(sysPermissions.getPermissionsName()); mySysUserDetails.setPermissions(permissions); } if ("lisi".equals(username)){ //lisi用戶是普通管理員,擁有基本權(quán)限 SysRole sysRole = sysRoleMapper.selectOne(new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleName, "普通管理員")); Set<SysRole> roles=new HashSet<>(); roles.add(sysRole); mySysUserDetails.setRoles(roles); SysPermissions sysPermissions = sysPermissionsMapper.selectById(2); Set<String> permissions=new HashSet<>(); permissions.add(sysPermissions.getPermissionsName()); mySysUserDetails.setPermissions(permissions); } return mySysUserDetails; } }
在實(shí)現(xiàn)了UserDetailes接口的用戶信息類MySysUserDetails中完成角色和權(quán)限的賦值:
// 角色信息 private Set<SysRole> roles; // 權(quán)限信息 private Set<String> permissions; // 用戶擁有的權(quán)限集合,我這里先設(shè)置為null,將來會(huì)再更改的 @Override public Collection<? extends GrantedAuthority> getAuthorities() { System.err.println("進(jìn)入權(quán)限的獲取方法"); List<GrantedAuthority> authorities = new ArrayList<>(); // 授權(quán)信息列表 // 將角色名稱添加到授權(quán)信息列表中 roles.forEach(role-> authorities.add(new SimpleGrantedAuthority(role.getRoleName()))); // 將權(quán)限名稱添加到授權(quán)信息列表中 permissions.forEach(permission-> authorities.add(new SimpleGrantedAuthority(permission)) ); return authorities; // 返回授權(quán)信息列表 }
用戶認(rèn)證之后,會(huì)去存儲(chǔ)用戶對(duì)應(yīng)的權(quán)限,并且給資源設(shè)置對(duì)應(yīng)的權(quán)限,SpringSecurity支持兩種粒度 的權(quán)限:
1、基于請(qǐng)求的:在配置文件中配置路徑,可以使用**的通配符
2、基于方法的:在方法上使用注解實(shí)現(xiàn)
角色配置:在UserDetails接口中存在相關(guān)的權(quán)限和角色管理,只不過我們?cè)趯?shí)現(xiàn)這個(gè)接口的時(shí)候,將這些都設(shè)置為了null。現(xiàn)在我們只需要將這些信息實(shí)現(xiàn)即可:
1、基于請(qǐng)求:
還是在SecurityFilter過濾器中實(shí)現(xiàn)請(qǐng)求地址的權(quán)限校驗(yàn)
httpSecurity.authorizeHttpRequests(it-> //hello地址只有超級(jí)管理員角色才能訪問 it.requestMatchers("/hello").hasRole("超級(jí)管理員") //hello2地址只有"擁有所有權(quán)限"的權(quán)限才能訪問 .requestMatchers("hello2").hasAuthority("擁有所有權(quán)限") .requestMatchers("/login","sys-user/login").permitAll() //設(shè)置登錄路徑所有人都可以訪問 .anyRequest().authenticated() //其他路徑都要進(jìn)行攔截 );
使用sili進(jìn)行登錄時(shí),訪問hello2接口顯示權(quán)限不夠:
使用zhangsan進(jìn)行登錄時(shí),訪問hello2接口可以訪問到:
2、基于方法:
基于方法的權(quán)限認(rèn)證要在SecurityConfig類上加上@EnableMethodSecurity注解,表示開啟了方法權(quán)限的使用;
常用的有四個(gè)注解:
@PreAuthorize
@PostAuthorize
@PreFilter
@PostFilter
/*測(cè)試@PreAuthorize注解 * 作用:使用在類或方法上,擁有指定的權(quán)限才能訪問(在方法運(yùn)行前進(jìn)行校驗(yàn)) * String類型的參數(shù):語法是Spring的EL表達(dá)式 * 有權(quán)限:test3權(quán)限 * hasRole:會(huì)去匹配authorities,但是會(huì)在hasRole的參數(shù)前加上一個(gè)ROLE_前綴, * 所以在定義權(quán)限的時(shí)候需要加上ROLE_前綴 * role和authorities的關(guān)系是:role是一種復(fù)雜的寫法,有ROLE_前綴,authorities是role的簡(jiǎn)化寫法 * 如果使用 * hasAnyRole:則匹配的權(quán)限是在authorities加上前綴ROLE_ * 推薦使用 * hasAnyAuthority:匹配authorities,但是不用在authorities的參數(shù)前加上ROLE_前綴 * */ @PreAuthorize("hasAnyAuthority('擁有所有權(quán)限')") @ResponseBody @GetMapping("/test3") public String test3(){ System.out.println("一個(gè)請(qǐng)求"); return "一個(gè)test3請(qǐng)求"; }
/* @PostAuthorize:在方法返回時(shí)進(jìn)行校驗(yàn)。 可以還是校驗(yàn)權(quán)限、或者校驗(yàn)一些其他的東西(接下來我們校驗(yàn)返回值的長(zhǎng)度) *返回結(jié)果的長(zhǎng)度大于3、則認(rèn)為是合法的 returnObject:固定寫法,代指返回對(duì)象 * */ @ResponseBody @PostAuthorize("returnObject.length()>4") @GetMapping("/test4") public String test4(){ System.out.println("一個(gè)test4請(qǐng)求"); return "小張自傲張最終"; }
/* * @PreFilter:過濾符合條件的數(shù)據(jù)進(jìn)入到接口 * */ @PostFilter("filterObject.length()>3") @ResponseBody @GetMapping("/test5") public String test5(){ System.out.println("一個(gè)test4請(qǐng)求"); List<String> list = new ArrayList<>(); list.add("張三"); list.add("王麻子"); list.add("狗叫什么"); return "一個(gè)test5請(qǐng)求"; }
/* * @PreFilter:過濾符合條件的數(shù)據(jù)返回,數(shù)據(jù)必須是Collection、map、Array【數(shù)組】 * */ @PreFilter("filterObject.length()>5") @ResponseBody @PostMapping("/test6") public List<String> test6(@RequestBody List<String> list){ return list; }
這四個(gè)常用的權(quán)限校驗(yàn)方法我都寫出來了,運(yùn)行結(jié)果我就不在一一截圖了。
需要注意的是這些方法不僅僅局限在權(quán)限的校驗(yàn),還能對(duì)返回的結(jié)果做一定的操作;
最需要注意的就是@PreFilter注解,它要求前端傳遞的參數(shù)一定是數(shù)組或集合;
還有在SpringSecurity框架中:
role和authorities的關(guān)系是:role是一種復(fù)雜的寫法,有ROLE_前綴,authorities是role的簡(jiǎn)化寫法
基于方法鑒權(quán) 在SpringSecurity6版本中@EnableGlobalMethodSecurity被棄用,取而代之的是 @EnableMethodSecurity。默認(rèn)情況下,會(huì)激活pre-post注解,并在內(nèi)部使用 AuthorizationManager。
新老API區(qū)別 此@EnableMethodSecurity替代了@EnableGlobalMethodSecurity。提供了以下改進(jìn): 1. 使用簡(jiǎn)化的AuthorizationManager。 2. 支持直接基于bean的配置,而不需要擴(kuò)展GlobalMethodSecurityConfiguration 3. 使用Spring AOP構(gòu)建,刪除抽象并允許您使用Spring AOP構(gòu)建塊進(jìn)行自定義 4. 檢查是否存在沖突的注釋,以確保明確的安全配置 5. 符合JSR-250 6. 默認(rèn)情況下啟用@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter
主要的權(quán)衡似乎是您希望您的授權(quán)規(guī)則位于何處。重要的是要記住,當(dāng)您使用基于注釋的方法安全性 時(shí),未注釋的方法是不安全的。為了防止這種情況,請(qǐng)?jiān)贖ttpSecurity實(shí)例中聲明一個(gè)兜底授權(quán)規(guī)則。 如果方法上也定義了權(quán)限,則會(huì)覆蓋類上的權(quán)限
注意:使用注解的方式實(shí)現(xiàn),如果接口的權(quán)限發(fā)生變化,需要修改代碼了。
總結(jié):
登錄校驗(yàn)(Authentication):
- 用戶提交用戶名和密碼進(jìn)行登錄。
- Spring Security會(huì)攔截登錄請(qǐng)求,并將用戶名和密碼與存儲(chǔ)在系統(tǒng)中的憑據(jù)(如數(shù)據(jù)庫或LDAP)進(jìn)行比對(duì)。
- 如果用戶名和密碼匹配,則認(rèn)為用戶通過了身份驗(yàn)證,可以繼續(xù)訪問受限資源。
- 認(rèn)證成功后,Spring Security會(huì)創(chuàng)建一個(gè)包含用戶信息和權(quán)限的安全上下文(Security Context)。
權(quán)限認(rèn)證(Authorization):
- 一旦用戶通過了身份驗(yàn)證,Spring Security就會(huì)開始進(jìn)行權(quán)限認(rèn)證。
- 針對(duì)每個(gè)受限資源或操作,可以配置相應(yīng)的權(quán)限要求,例如需要哪些角色或權(quán)限才能訪問。
- Spring Security會(huì)根據(jù)配置的權(quán)限要求,檢查當(dāng)前用戶所擁有的角色和權(quán)限,判斷是否滿足訪問條件。
- 如果用戶擁有足夠的角色或權(quán)限,就被允許訪問資源;否則將被拒絕訪問,并可能重定向到登錄頁面或返回相應(yīng)的錯(cuò)誤信息。
Spring Security通過身份驗(yàn)證(Authentication)來確認(rèn)用戶的身份,并通過授權(quán)(Authorization)來控制用戶對(duì)受保護(hù)資源的訪問。這種分離的設(shè)計(jì)使得安全配置更加靈活,并且可以輕松地對(duì)不同的用戶和角色進(jìn)行管理和控制。
這都是一些基礎(chǔ)的理論,只是一個(gè)小demo,帶你認(rèn)識(shí)一下spring security的工作原理,下面我將實(shí)戰(zhàn)中演示使用security的案例:
前后端分離,使用vue3整合SpringSecurity加JWT實(shí)現(xiàn)登錄認(rèn)證
到此這篇關(guān)于springboot3整合SpringSecurity實(shí)現(xiàn)登錄校驗(yàn)與權(quán)限認(rèn)證的文章就介紹到這了,更多相關(guān)springboot 登錄校驗(yàn)與權(quán)限認(rèn)證 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
實(shí)現(xiàn)一個(gè)規(guī)則引擎的可視化具體方案
項(xiàng)目原因需要用到規(guī)則引擎,但是發(fā)現(xiàn)大部分不可以自由的進(jìn)行規(guī)則定義,通過不斷嘗試變換關(guān)鍵字在搜索引擎搜索,最終在stackoverflow找到了一個(gè)探討這個(gè)問題的帖子,特此將帖子中提到的方案分享一下,如果你跟我一樣在研究同樣的問題,也許對(duì)你有用2021-04-04java正則替換括號(hào)中的逗號(hào)實(shí)現(xiàn)示例
本文主要介紹了java正則替換括號(hào)中的逗號(hào)實(shí)現(xiàn)示例,主要介紹了兩種示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01Mybatis Generator逆向工程的使用詳細(xì)教程
這篇文章主要介紹了Mybatis Generator逆向工程的使用詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter
這篇文章主要介紹了dubbo將異常轉(zhuǎn)換成RuntimeException的原因分析?ExceptionFilter問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Java正則表達(dá)式學(xué)習(xí)之分組與替換
這篇文章主要給大家介紹了關(guān)于Java正則表達(dá)式學(xué)習(xí)之分組與替換的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09