SpringSecurity進行認證與授權(quán)的示例代碼
一、SpringSecurity簡介
Spring Security 是 Spring 家族中的一個安全管理框架。相比與另外一個安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。
一般來說中大型的項目都是使用SpringSecurity 來做安全框架。小項目有Shiro的比較多,因為相比與SpringSecurity,Shiro的上手更加的簡單。
一般Web應用的需要進行認證和授權(quán)。
- 認證:驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶
- 授權(quán):經(jīng)過認證后判斷當前用戶是否有權(quán)限進行某個操作
而認證和授權(quán)也是SpringSecurity作為安全框架的核心功能。
1.1 入門Demo
依賴如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
引入依賴后我們在嘗試去訪問之前的接口就會自動跳轉(zhuǎn)到一個SpringSecurity的默認登陸頁面,默認用戶名是user,密碼會輸出在控制臺。
必須登陸之后才能對接口進行訪問。
訪問 localhost:8080/logout 這個鏈接可以 對其進行退出操作。
Ps:
以上過程了解即可,因為我們實際Web項目中,一般采用我們自定義的登錄驗證授權(quán)方案,不會采取SpringSecurity框架提供的默認方案。
二、認證
登錄校驗流程:
為了實現(xiàn)以上這種過程,我們需要先對SpringSecurity默認的流程進行了解,才可以對其進行修改,實現(xiàn)我們自定義的方案。
2.1 SpringSecurity完整流程
SpringSecurity的原理其實就是一個過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器:
圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示:
- UsernamePasswordAuthenticationFilter:負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。入門案例的認證工作主要有它負責。
- ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor:負責權(quán)限校驗的過濾器。我們可以通過Debug查看當前系統(tǒng)中SpringSecurity過濾器鏈中有哪些過濾器及它們的順序。
如果想查看所有的過濾器,可以通過獲取Spring容器,Debug方式來查看:
2.2 認證流程詳解
箭頭代表該方法屬于這個實現(xiàn)類的。
概念速查:
- Authentication接口: 它的實現(xiàn)類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
- AuthenticationManager接口:定義了認證Authentication的方法
- UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。
- UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
2.3 自定義認證實現(xiàn)
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進行認證 如果認證通過生成jwt 把用戶信息存入redis中
②自定義UserDetailsService
在這個實現(xiàn)類中去查詢數(shù)據(jù)庫
校驗
①定義Jwt 認證過濾器
獲取token 解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
這里為什么要存入 SecurityContextHolder中呢?
我們自定義的JWT過濾器的時候,肯定是需要將這個JWT過濾器放在UsernamePasswordAuthenticationFilter前的,這時我們將從redis獲取的用戶信息存入SecurityContextHolder才行,否則后續(xù)過濾器在進行校驗的時候,可能會因為SecurityContextHolder中沒有對應的值而判斷當前訪問用戶驗證不通過。
2.3.1 數(shù)據(jù)庫校驗用戶
定義Mapper接口
package com.example.springsecurity_demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.springsecurity_demo.domain.User; public interface UserMapper extends BaseMapper<User> { }
定義User實體類
package com.example.springsecurity_demo.domain; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_user") public class User implements Serializable { private static final long serialVersionUID = -40356785423868312L; /** * 主鍵 */ @TableId private Long id; /** * 用戶名 */ private String userName; /** * 昵稱 */ private String nickName; /** * 密碼 */ private String password; /** * 賬號狀態(tài)(0正常 1停用) */ private String status; /** * 郵箱 */ private String email; /** * 手機號 */ private String phonenumber; /** * 用戶性別(0男,1女,2未知) */ private String sex; /** * 頭像 */ private String avatar; /** * 用戶類型(0管理員,1普通用戶) */ private String userType; /** * 創(chuàng)建人的用戶id */ private Long createBy; /** * 創(chuàng)建時間 */ private Date createTime; /** * 更新人 */ private Long updateBy; /** * 更新時間 */ private Date updateTime; /** * 刪除標志(0代表未刪除,1代表已刪除) */ private Integer delFlag; }
配置Mapper掃描
package com.example.springsecurity_demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication @MapperScan("com.example.springsecurity_demo.mapper") public class SpringSecurityDemoApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args); System.out.println(1); } }
核心代碼實現(xiàn)
package com.example.springsecurity_demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.springsecurity_demo.domain.LoginUser; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.mapper.UserMapper; 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.Service; import java.util.Objects; @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查詢用戶信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果沒有查詢到用戶就拋出異常 if (Objects.isNull(user)) { throw new RuntimeException("用戶名或者密碼錯誤"); } //TODO 查詢對應的權(quán)限信息 return new LoginUser(user); } }
因為UserDetailsService方法的返回值是UserDetails(接口):
所以需要定義一個類,實現(xiàn)該接口,把用戶信息封裝在其中。
package com.example.springsecurity_demo.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; @Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
注意:如果要測試,需要往用戶表中寫入用戶數(shù)據(jù),并且如果你想讓用戶的密碼是明文存儲,需要在密碼前加{noop}。例如:
這樣登陸的時候就可以用fox作為用戶名,123作為密碼來登陸了。
2.3.2 密碼加密存儲
實際項目中我們不會把密碼明文存儲在數(shù)據(jù)庫中。
默認使用的PasswordEncoder要求數(shù)據(jù)庫中的密碼格式為:{id}password 。它會根據(jù)id去判斷密碼的加密方式。但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該
PasswordEncoder來進行密碼校驗。
我們可以定義一個SpringSecurity的配置類:
低版本配置如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
以下是高版本的SpringSecurity(SpringBoot 3 用以下配置):
package com.example.springsecurity_demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
2.3.3 登錄接口實現(xiàn)
接下來我們需要自定義登錄接口,這里我們需要讓SpringSecurity對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。(畢竟登錄接口如果還需要權(quán)限訪問,那么就很奇怪了)
在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。
認證成功的話要生成一個JWT,放入響應中返回,并且為了讓用戶下回請求時需要通過JWT識別出具體的是哪個用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。
Contorller類如下:
package com.example.springsecurity_demo.controller; import com.example.springsecurity_demo.domain.ResponseResult; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.service.LoginService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowired private LoginService loginService; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ return loginService.login(user); } }
Ps:
雖然字段聲明的類型是 LoginService,但實際上注入的是 LoginServiceImpl。這是因為 LoginServiceImpl 實現(xiàn)了 LoginService 接口,因此它被視為 LoginService 的一種類型。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認證 .anyRequest().authenticated(); // } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
實現(xiàn)類如下:
import com.sangeng.domain.LoginUser; import com.sangeng.domain.ResponseResult; import com.sangeng.domain.User; import com.sangeng.service.LoginServcie; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { //AuthenticationManager authenticate進行用戶認證 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //如果認證沒通過,給出對應的提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("登錄失敗"); } //如果認證通過了,使用userid生成一個jwt jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userid = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userid); Map<String,String> map = new HashMap<>(); map.put("token",jwt); //把完整的用戶信息存入redis userid作為key redisCache.setCacheObject("login:"+userid,loginUser); return new ResponseResult(200,"登錄成功",map); } }
分析:
這里創(chuàng)建UsernamePasswordAuthenticationToken對象是因為調(diào)用authenticationManager.authenticate方法 需要傳入Authentication,但是Authentication又是一個接口,所以需要傳入其實現(xiàn)類。
2.3.4 認證過濾器
我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進行解析取出其中的
userid。
使用userid去redis中獲取對應的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder。
import com.sangeng.domain.LoginUser; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //從redis中獲取用戶信息 String redisKey = "login:" + userid; LoginUser loginUser = redisCache.getCacheObject(redisKey); if(Objects.isNull(loginUser)){ throw new RuntimeException("用戶未登錄"); } //存入SecurityContextHolder //TODO 獲取權(quán)限信息封裝到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
分析:
這里先設置放行最后再使用return是因為,如果該請求無含token,那么就對其進行放行,讓請求進入下一個攔截器,后續(xù)Security框架還有很多攔截器可以對其進行驗證,而使用return是因為后續(xù)在進行參數(shù)返回的時候,不需要再執(zhí)行以下代碼。
簡單來說:圖中代碼紅線以上部分是參數(shù)請求時候所執(zhí)行的部分,紅線以下是返回響應體時候所執(zhí)行的部分。
還有個細節(jié)是:這里必須調(diào)用三個參數(shù)的構(gòu)造方法,而不是兩個,這是因為只有調(diào)用三個構(gòu)造方法的時候,才能保證該主體是認證過的,否則框架檢查時候還是會報錯:
SecurityConfig如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
2.3.5 退出登錄
我們只需要定義一個登陸接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數(shù)據(jù)即可。
import com.sangeng.domain.LoginUser; import com.sangeng.domain.ResponseResult; import com.sangeng.domain.User; import com.sangeng.service.LoginServcie; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult logout() { //獲取SecurityContextHolder中的用戶id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); //刪除redis中的值 redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"注銷成功"); } }
分析:
這里并不需要刪除SecurityContextHolder中的信息,只需要刪除redis中所存儲的即可,因為在進行認證的時候,需要先在SecurityContextHolder中拿到信息后,再從redis中獲取對應信息。
每個請求都對應一個SecurityContextHolder,所以刪除SecurityContextHolder中的信息是無效的,需要刪除redis中所存儲的信息。
三、授權(quán)
3.1 權(quán)限系統(tǒng)作用
例如一個學校圖書館的管理系統(tǒng),如果是普通學生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應該就能看到并使用添加書籍信息,刪除書籍信息等功能。
總結(jié)起來就是不同的用戶可以使用不同的功能。這就是權(quán)限系統(tǒng)要去實現(xiàn)的效果。
我們不能只依賴前端去判斷用戶的權(quán)限來選擇顯示哪些菜單哪些按鈕。因為如果只是這樣,如果有人知道了對應功能的接口地址就可以不通過前端,直接去發(fā)送請求來實現(xiàn)相關(guān)功能操作。
所以我們還需要在后臺進行用戶權(quán)限的判斷,判斷當前用戶是否有相應的權(quán)限,必須具有所需權(quán)限才能進行相應的操作。
3.2 授權(quán)基本流程
在SpringSecurity中,會使用默認的FilterSecurityInterceptor來進行權(quán)限校驗。在
FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的
權(quán)限信息。當前用戶是否擁有訪問當前資源所需的權(quán)限。
所以我們在項目中只需要把當前登錄用戶的權(quán)限信息也存入Authentication。
然后設置我們的資源所需要的權(quán)限即可。
如何將權(quán)限信息存入Authentication呢?
回顧之前的代碼,我們需要在自定義的認證過濾器中,將Authentication存入SecurityContextHolder中,那么Authentication的信息又是從哪里來呢?
:是從redis中獲取到的。
也就是我們當初應該往redis中存入權(quán)限信息,而redis中存儲的是loginUser,
loginUser是當初我們UserDetail的自定義實現(xiàn)類中所查詢到的用戶信息(也就是:從數(shù)據(jù)庫中查詢的)。
而最終這個loginUser對象會返回給這個authenticate對象,那么authenticate這個對象就會存入redis中。
以上就是整個流程,所以我們最終只需要完成兩個步驟即可:
- 在查詢數(shù)據(jù)庫的時候獲取對應的權(quán)限信息。
- 在實現(xiàn)認證過濾器的時候,需要獲取當前用戶的權(quán)限信息,并存入到SecurityContextHolder中。
也就是需要完善上述圖片中兩個 TODO 標識的代碼片段。
3.3 授權(quán)實現(xiàn)
3.3.1 限制訪問資源所需權(quán)限
SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權(quán)限。
但是要使用它我們需要先開啟相關(guān)配置,即在關(guān)于SpringSecurity的配置類中添加以下代碼:
@EnableGlobalMethodSecurity(prePostEnabled = true)
設置完之后,就可以使用對應的注解:@PreAuthorize。
@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello(){ return "hello"; } }
分析:
這里的test只供測試,test使用單引號是因為外層已經(jīng)有了雙引號,所以使用單引號來標識這是個字符串,實際上我們通過idea的提示(ctrl+p),也可以知道,這個hasAuthority所需要的參數(shù)也是String類型:
3.3.2 封裝權(quán)限信息
我們前面在寫UserDetailsServiceImpl的時候說過,在查詢出用戶后還要獲取對應的權(quán)限信息,封裝到UserDetails中返回。
我們先直接把權(quán)限信息寫死封裝到UserDetails中進行測試。
我們之前定義了UserDetails的實現(xiàn)類LoginUser,想要讓其能封裝權(quán)限信息就要對其進行修改。
import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities!=null){ return authorities; } //把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對象 authorities = new ArrayList<>(); for (String permission : permissions) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission); authorities.add(authority); } // authorities = permissions.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
分析:
這里我們重寫了getAuthorities方法, permission這個屬性存儲的是權(quán)限信息。
在getAuthorities方法中,由于返回值是一個Collection類型,所以我們這里選擇List集合。
又因為泛型要求是GrantedAuthority的子類,但是其是一個接口,所以我們通過查找其實現(xiàn)類(ctrl+alt+鼠標左鍵):
選擇了SimpleGrantedAuthority,因為看它名字有個simple,再觀察其構(gòu)造方法,發(fā)現(xiàn)只需要傳入一個字符串即可,于是我們就需要將我們類屬性的permission全部都通過構(gòu)造方法存入SimpleGrantedAuthority屬性中,然后將其一個個遍歷,放入list集合中。
還有一個小細節(jié)是這里加了@JSONField(serialize = false) 注解防止redis存儲loginUser時候序列化出錯(報異常),因為SimpleGrantedAuthority是Spring中提供的類。
雖然我們這里不讓這個List集合序列化,但是并不影響后續(xù)操作,因為在取出來反序列化的時候,我們自定義的permission屬性是可以被正常序列化的,那個時候通過它就可以讓程序正常運行。
3.3.3 從數(shù)據(jù)庫查詢權(quán)限信息
3.3.3.1 RBAC權(quán)限模型
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權(quán)限模型。
3.3.3.2 代碼實現(xiàn)
Menu類如下:
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; /** * 菜單表(Menu)實體類 * * @author makejava * @since 2021-11-24 15:30:08 */ @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L; @TableId private Long id; /** * 菜單名 */ private String menuName; /** * 路由地址 */ private String path; /** * 組件路徑 */ private String component; /** * 菜單狀態(tài)(0顯示 1隱藏) */ private String visible; /** * 菜單狀態(tài)(0正常 1停用) */ private String status; /** * 權(quán)限標識 */ private String perms; /** * 菜單圖標 */ private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; /** * 是否刪除(0未刪除 1已刪除) */ private Integer delFlag; /** * 備注 */ private String remark; }
mapper如下:
我們只需要根據(jù)用戶id去查詢到其所對應的權(quán)限信息即可。
所以我們可以先定義個mapper,其中提供一個方法可以根據(jù)userid查詢權(quán)限信息。
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.sangeng.domain.Menu; import java.util.List; public interface MenuMapper extends BaseMapper<Menu> { List<String> selectPermsByUserId(Long userid); }
尤其是自定義方法,所以需要創(chuàng)建對應的mapper文件,定義對應的sql語句
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.fox.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select> </mapper>
然后我們可以在UserDetailsServiceImpl中去調(diào)用該mapper的方法查詢權(quán)限信息封裝到LoginUser對象中即可:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sangeng.domain.LoginUser; import com.sangeng.domain.User; import com.sangeng.mapper.MenuMapper; import com.sangeng.mapper.UserMapper; 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.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查詢用戶信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果沒有查詢到用戶就拋出異常 if(Objects.isNull(user)){ throw new RuntimeException("用戶名或者密碼錯誤"); } // List<String> list = new ArrayList<>(Arrays.asList("test","admin")); List<String> list = menuMapper.selectPermsByUserId(user.getId()); //把數(shù)據(jù)封裝成UserDetails返回 return new LoginUser(user,list); } }
四、自定義失敗處理
我們還希望在認證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應進行統(tǒng)一的處理。要實現(xiàn)這個功能我們需要知道SpringSecurity的異常處理機制。
在SpringSecurity中,如果我們在認證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權(quán)失敗出現(xiàn)的異常。
如果是認證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用
AuthenticationEntryPoint對象的方法去進行異常處理。
如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和
AccessDeniedHandler然后配置給SpringSecurity即可。
4.1 創(chuàng)建自定義實現(xiàn)類
也就是說,我們只需要創(chuàng)建一個自定義的實現(xiàn)類然后分別去實現(xiàn)AccessDeniedHandler接口和AccessDeniedHandler接口即可,代碼如下。
認證失敗自定義實現(xiàn)類如下:
import com.alibaba.fastjson.JSON; import com.sangeng.domain.ResponseResult; import com.sangeng.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用戶認證失敗請查詢登錄"); String json = JSON.toJSONString(result); //處理異常 WebUtils.renderString(response,json); } }
授權(quán)失敗自定義實現(xiàn)類如下:
import com.alibaba.fastjson.JSON; import com.sangeng.domain.ResponseResult; import com.sangeng.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的權(quán)限不足"); String json = JSON.toJSONString(result); //處理異常 WebUtils.renderString(response,json); } }
涉及到的工具類如下:
由于response對象是較為原生的,所以我們需要進行書寫狀態(tài)碼,ContentType等。所以我們需要使用工具類對其進行修改。
import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WebUtils { /** * 將字符串渲染到客戶端 * * @param response 渲染對象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
4.2 將實現(xiàn)類配置給SpringSecurity
注入對應處理器:
然后我們可以使用HttpSecurity對象的方法去配置:
配合類代碼如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
五、跨域問題解決方案
瀏覽器出于安全的考慮,使用 XMLHttpRequest對象發(fā)起 HTTP請求時必須遵守同源策略,否則就是跨域的HTTP請求,默認情況下是被禁止的。 同源策略要求源相同才能正常進行通信,即協(xié)議、域名、端口號都完全一致。
前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。
所以我們就要處理一下,讓前端能進行跨域請求。
①先對SpringBoot配置,運行跨域請求
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 設置允許跨域的路徑 registry.addMapping("/**") // 設置允許跨域請求的域名 .allowedOriginPatterns("*") // 是否允許cookie .allowCredentials(true) // 設置允許的請求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 設置允許的header屬性 .allowedHeaders("*") // 跨域允許時間 .maxAge(3600); } }
②開啟SpringSecurity的跨域訪問
由于我們的資源都會收到SpringSecurity的保護,所以想要跨域訪問還要讓SpringSecurity運行跨域訪問。
六、其他權(quán)限校驗方法
我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。
SpringSecurity還為我們提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。
這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學習其他方法你就更容易理解,而不是死記硬背區(qū)別。并且我們也可以選擇定義校驗方法,實現(xiàn)我們自己的校驗邏輯。
hasAuthority方法實際是執(zhí)行到了SecurityExpressionRoot的hasAuthority,大家只要斷點調(diào)試既可知道它內(nèi)部的校驗原理。
它內(nèi)部其實是調(diào)用authentication的getAuthorities方法獲取用戶的權(quán)限列表。然后判斷我們存入的方法參數(shù)數(shù)據(jù)在權(quán)限列表中。
hasAnyAuthority方法可以傳入多個權(quán)限,只有用戶有其中任意一個權(quán)限都可以訪問對應資源。
@RequestMapping("/hello") @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')") public String hello(){ return "hello"; }
hasRole要求有對應的角色才可以訪問,但是它內(nèi)部會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權(quán)限也要有 ROLE_ 這個前綴才可以。
@RequestMapping("/hello") @PreAuthorize("hasRole('system:dept:list')") public String hello(){ return "hello"; }
hasAnyRole 有任意的角色就可以訪問。它內(nèi)部也會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權(quán)限也要有 ROLE_ 這個前綴才可以。
@RequestMapping("/hello") @PreAuthorize("hasAnyRole('admin','system:dept:list')") public String hello(){ return "hello"; }
七、自定義權(quán)限校驗方法
我們也可以定義自己的權(quán)限校驗方法,在@PreAuthorize注解中使用我們的方法。
import com.fox.domain.LoginUser; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.List; @Component("ex") public class FoxExpressionRoot { public boolean hasAuthority(String authority){ //獲取當前用戶的權(quán)限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判斷用戶權(quán)限集合中是否存在authority return permissions.contains(authority); } }
在SPEL表達式中使用 @ex相當于獲取容器中bean的名字未ex的對象。然后再調(diào)用這個對象的
hasAuthority方法:
@RequestMapping("/hello") @PreAuthorize("@ex.hasAuthority('system:dept:list')") public String hello(){ return "hello"; }
八、基于配置的權(quán)限控制
我們也可以在配置類中使用使用配置的方式對資源進行權(quán)限控制。
到此這篇關(guān)于SpringSecurity進行認證與授權(quán)的示例代碼的文章就介紹到這了,更多相關(guān)SpringSecurity 認證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity實現(xiàn)權(quán)限認證與授權(quán)的使用示例
- springSecurity用戶認證和授權(quán)的實現(xiàn)
- SpringBoot整合SpringSecurity認證與授權(quán)
- 深入淺析springsecurity入門登錄授權(quán)
- SpringSecurityOAuth2實現(xiàn)微信授權(quán)登錄
- SpringBoot+SpringSecurity實現(xiàn)基于真實數(shù)據(jù)的授權(quán)認證
- springsecurity第三方授權(quán)認證的項目實踐
- SpringSecurity數(shù)據(jù)庫進行認證和授權(quán)的使用
- SpringSecurity授權(quán)機制的實現(xiàn)(AccessDecisionManager與投票決策)
相關(guān)文章
spring?kafka?@KafkaListener詳解與使用過程
這篇文章主要介紹了spring-kafka?@KafkaListener詳解與使用,本文結(jié)合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02maven多profile 打包下 -P參和-D參數(shù)的實現(xiàn)
這篇文章主要介紹了maven多profile 打包下 -P參和-D參數(shù)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11@FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決
這篇文章主要介紹了@FeignClient?path屬性路徑前綴帶路徑變量時報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07Springbootadmin與security沖突問題及解決
這篇文章主要介紹了Springbootadmin與security沖突問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08自定義的Troop<T>泛型類( c++, java和c#)的實現(xiàn)代碼
這篇文章主要介紹了自定義的Troop<T>泛型類( c++, java和c#)的實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05