更新時(shí)間:2022-11-17 來(lái)源:黑馬程序員 瀏覽量:
1 文章導(dǎo)讀
Spring Security 是 Spring 家族中的一個(gè)**安全管理框架,可以和Spring Boot項(xiàng)目很方便的集成。Spring Security框架的兩大核心功能:認(rèn)證和授權(quán)。
認(rèn)證:驗(yàn)證當(dāng)前訪問(wèn)系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶。簡(jiǎn)單的理解就是登陸操作,如果可以登錄成功就說(shuō)明您是本系統(tǒng)的用戶,如不能登錄就說(shuō)明不是本系統(tǒng)的用戶!而且登錄成功以后需要記錄當(dāng)前登錄用戶的信息!
授權(quán):經(jīng)過(guò)認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作!
如上圖所示就是展示了當(dāng)前登錄用戶可以操作的權(quán)限:用戶管理、角色管理、菜單管理等,并且針對(duì)角色管理可以進(jìn)行新增、修改、刪除、導(dǎo)出等權(quán)限。
而現(xiàn)在前后端分離開(kāi)發(fā)成為了主流的開(kāi)發(fā)方式,那么在前后端分離開(kāi)發(fā)方式下如何使用Spring Security就是本文章需要重點(diǎn)研究的內(nèi)容。
2 Spring Security認(rèn)證功能
2.1 前端分離項(xiàng)目的認(rèn)證流程
要想了解如果使用Spring Security進(jìn)行認(rèn)證,那么就需要先了解一下前后端分離項(xiàng)目中的認(rèn)證流程,如下所示:
2.2 Spring Security原理初探
要想使用Spring Security框架來(lái)實(shí)現(xiàn)上述的認(rèn)證操作,就必須先要了解一個(gè)Spring Security框架的工作流程。
2.2.1 過(guò)濾器鏈
Spring Security的原理其實(shí)就是一個(gè)過(guò)濾器鏈,內(nèi)部包含了提供各種功能的過(guò)濾器。這里我們可以看看入門案例中的過(guò)濾器。
圖中只展示了核心過(guò)濾器,其它的非核心過(guò)濾器并沒(méi)有在圖中展示。
UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理我們?cè)诘顷戫?yè)面填寫(xiě)了用戶名密碼后的登陸請(qǐng)求。
ExceptionTranslationFilter:處理過(guò)濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器。
2.2.2 認(rèn)證流程
Spring Security的認(rèn)證流程大致如下所示:
概念速查:
Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問(wèn)系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法。
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
UserDetails接口:提供核心用戶信息。通過(guò)UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
概念速查:
Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問(wèn)系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
AuthenticationManager接口:定義了認(rèn)證Authentication的方法。
UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
UserDetails接口:提供核心用戶信息。通過(guò)UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
2.3 認(rèn)證實(shí)現(xiàn)
在前后端分離項(xiàng)目中,前端請(qǐng)求的是我們自己定義的認(rèn)證接口。因?yàn)樵谡J(rèn)證成功以后就需要針對(duì)當(dāng)前用戶生成token,Spring Security中提供的原始認(rèn)證就無(wú)法實(shí)現(xiàn)了。在我們自定義的認(rèn)證接口中,需要調(diào)用Spring Security的API借助于Spring Security實(shí)現(xiàn)認(rèn)證。
2.3.1 思路分析
認(rèn)證:
1、自定義認(rèn)證接口
① 調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過(guò)生成jwt
② 把用戶信息存入redis中
2、自定義UserDetailsService
① 在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫(kù)
校驗(yàn):
1、定義Jwt認(rèn)證過(guò)濾器
① 獲取token
② 解析token獲取其中的userid
③ 從redis中獲取用戶信息
④ 存入SecurityContextHolder
2.3.2 集成Redis
添加依賴
<!--redis依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
添加redis配置
在application.yml文件中添加Redis的相關(guān)配置
spring: redis: host: 127.0.0.1 port: 6379
2.3.3 集成Mybatis Plus
添加依賴
<!-- 引入mybatis plus的依賴 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <!-- 數(shù)據(jù)庫(kù)驅(qū)動(dòng) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- lombok依賴包 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
創(chuàng)建數(shù)據(jù)庫(kù)表
CREATE TABLE `sys_user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名', `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱', `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼', `status` CHAR(1) DEFAULT '0' COMMENT '賬號(hào)狀態(tài)(0正常 1停用)', `email` VARCHAR(64) DEFAULT NULL COMMENT '郵箱', `phone_number` VARCHAR(32) DEFAULT NULL COMMENT '手機(jī)號(hào)', `sex` CHAR(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)', `avatar` VARCHAR(128) DEFAULT NULL COMMENT '頭像', `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)', `create_by` BIGINT(20) DEFAULT NULL COMMENT '創(chuàng)建人的用戶id', `create_time` DATETIME DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人', `update_time` DATETIME DEFAULT NULL COMMENT '更新時(shí)間', `del_flag` INT(11) DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表' -- 插入數(shù)據(jù) insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '張三', '1234', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);
數(shù)據(jù)庫(kù)相關(guān)配置
spring: datasource: url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=UTC username: root password: 1234 driver-class-name: com.mysql.cj.jdbc.Driver # mybatis plus的配置 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id
User實(shí)體類
@Data @TableName(value = "sys_user") public class User { @TableId private Long id ; // 唯一標(biāo)識(shí) private String userName ; // 用戶名 private String nickName ; // 昵稱 private String password ; // 密碼 private String status ; // 狀態(tài) 賬號(hào)狀態(tài)(0正常 1停用) private String email ; // 郵箱 private String phoneNumber ; // 電話號(hào)碼 private String sex ; // 性別 用戶性別(0男,1女,2未知) private String avatar ; // 用戶頭像 private String userType ; // 用戶類型 (0管理員,1普通用戶) private Long createBy ; // 創(chuàng)建人 private Date createTime ; // 創(chuàng)建時(shí)間 private Long updateBy ; // 更新人 private Date updateTime ; // 更新時(shí)間 private Integer delFlag ; // 是否刪除 (0代表未刪除,1代表已刪除) }
UserMapper接口
public interface UserMapper extends BaseMapper<User> { }
啟動(dòng)類
@SpringBootApplication @MapperScan(basePackages = "com.itheima.security.mapper") public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class , args) ; } }
2.3.4 集成Junit
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
編寫(xiě)測(cè)試類
@SpringBootTest(classes = SecurityApplication.class) public class SecurityApplicationTest { @Autowired private UserMapper userMapper ; @Test public void findAll() { List<User> selectList = userMapper.selectList(new LambdaQueryWrapper<User>()); selectList.forEach( s -> System.out.println(s) ); } }
2.3.5 UserDetailsService
在Spring Security的整個(gè)認(rèn)證流程中會(huì)調(diào)用會(huì)調(diào)用UserDetailsService中的loadUserByUsername方法根據(jù)用戶名稱查詢用戶數(shù)據(jù)。默認(rèn)情況下調(diào)用的是InMemoryUserDetailsManager中的方法,該UserDetailsService是從內(nèi)存中獲取用戶的數(shù)據(jù)?,F(xiàn)在我們需要從數(shù)據(jù)庫(kù)中獲取用戶的數(shù)據(jù),那么此時(shí)就需要自定義一個(gè)UserDetailsService來(lái)覆蓋默認(rèn)的配置。
UserDetailsServiceImpl
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper ; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根據(jù)用戶名查詢用戶數(shù)據(jù) LambdaQueryWrapper<User> lambdaQueryWrapper = Wrappers.<User>lambdaQuery().eq(User::getUserName ,username) ; User user = userMapper.selectOne(lambdaQueryWrapper); // 如果查詢不到數(shù)據(jù),說(shuō)明用戶名或者密碼錯(cuò)誤,直接拋出異常 if(user == null) { throw new RuntimeException("用戶名或者密碼錯(cuò)誤") ; } // 將查詢到的對(duì)象轉(zhuǎn)換成Spring Security所需要的UserDetails對(duì)象 return new LoginUser(user); } }
LoginUser
package com.itheima.security.domain; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; // 用來(lái)封裝數(shù)據(jù)庫(kù)查詢出來(lái)的用戶數(shù)據(jù) @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() { // 賬號(hào)是否沒(méi)有過(guò)期 return true; } @Override public boolean isAccountNonLocked() { // 賬號(hào)是否沒(méi)有被鎖定 return true; } @Override public boolean isCredentialsNonExpired() { // 賬號(hào)的憑證是否沒(méi)有過(guò)期 return true; } @Override public boolean isEnabled() { // 賬號(hào)是否可用 return true; } }
測(cè)試認(rèn)證
先通過(guò)Spring Security提供的默認(rèn)登錄接口進(jìn)行認(rèn)證的測(cè)試,需要啟動(dòng)Redis。此時(shí)控制臺(tái)會(huì)輸出如下錯(cuò)誤:
報(bào)錯(cuò)的原因:默認(rèn)情況下Spring Security在獲取到UserDetailsService返回的用戶信息以后,會(huì)調(diào)用PasswordEncoder中的matches方法進(jìn)行校驗(yàn),但是此時(shí)在Spring容器中并不存在任何的PasswordEncoder的對(duì)象,因此無(wú)法完成校驗(yàn)操作。
解決方案:
① 使用明文認(rèn)證
要使用明文進(jìn)行認(rèn)證,就需要在密碼字段值的前面添加{noop}字樣!
?、?配置加密算法
2.3.6 配置加密算法
一般情況下關(guān)于密碼在數(shù)據(jù)庫(kù)中都是密文存儲(chǔ)的,在進(jìn)行認(rèn)證的時(shí)候都是基于密文進(jìn)行校驗(yàn)。具體的實(shí)現(xiàn)步驟:
1、使用指定的加密算法【**BCrypt**】對(duì)密碼進(jìn)行加密處理,將加密以后的密文存儲(chǔ)到數(shù)據(jù)庫(kù)中
2、在Spring容器中注入一個(gè)PasswordEncoder對(duì)象,一般情況下注入的就是:BCryptPasswordEncoder
我們可以定義一個(gè)Spring Security的配置類,Spring Security要求這個(gè)配置類要繼承WebSecurityConfigurerAdapter。
@Configuration public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder() ; } }
測(cè)試:將數(shù)據(jù)庫(kù)的用戶密碼更改為使用BCryptPasswordEncoder加密以后的密文
@SpringBootTest(classes = SecurityApplication.class) public class SecurityApplicationTest { @Autowired private PasswordEncoder passwordEncoder ; @Test public void testBcrypt() { // 加密測(cè)試 String encode = passwordEncoder.encode("1234"); System.out.println(encode); // 校驗(yàn)測(cè)試 boolean matches = passwordEncoder.matches("1234", "$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm"); System.out.println(matches); } }
2.3.7 登錄接口
整體實(shí)現(xiàn)思路:
?、?接下我們需要自定義登陸接口,然后讓Spring Security對(duì)這個(gè)接口放行,讓用戶訪問(wèn)這個(gè)接口的時(shí)候不用登錄也能訪問(wèn)。
?、?在接口中我們通過(guò)**AuthenticationManager**的authenticate方法來(lái)進(jìn)行用戶認(rèn)證,所以需要在Security Config中配置把AuthenticationManager注入容器。
?、?認(rèn)證成功的話要生成一個(gè)jwt,將jwt令牌進(jìn)行返回。并且為了讓用戶下回請(qǐng)求時(shí)能通過(guò)jwt識(shí)別出具體的是哪個(gè)用戶,在返回之前,我們需要把用戶信息存入redis,可以把用戶id作為key。
攔截規(guī)則配置
在SpringSecurityConfigurer中重寫(xiě)configure(HttpSecurity http)方法:
// 配置Spring Security的攔截規(guī)則 @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 關(guān)閉csrf .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 指定session的創(chuàng)建策略,不使用session .and() // 再次獲取到HttpSecurity對(duì)象 .authorizeRequests() // 進(jìn)行認(rèn)證請(qǐng)求的配置 .antMatchers("/user/login").anonymous() // 對(duì)于登錄接口,允許匿名訪問(wèn) .anyRequest().authenticated(); // 除了上面的請(qǐng)求以外所有的請(qǐng)求全部需要認(rèn)證 }
Spring容器注冊(cè)AuthenticationManager
在SpringSecurityConfigurer中重寫(xiě)authenticationManagerBean方法:
登錄接口定義
UserController
@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private UserService userService ; @PostMapping(value = "/login") public ResponseResult<Map> login(@RequestBody User user) { return userService.login(user) ; } }
ResponseResult
@Data @NoArgsConstructor @AllArgsConstructor public class ResponseResult<T> { private Integer code ; private String msg ; private T data ; }
UserService
@Service public class UserServiceImpl implements UserService { @Autowired private AuthenticationManager authenticationManager ; @Autowired private RedisTemplate<String , String> redisTemplate ; @Override public ResponseResult<Map> login(User user) { // 創(chuàng)建Authentication對(duì)象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName() , user.getPassword()) ; // 調(diào)用AuthenticationManager的authenticate方法進(jìn)行認(rèn)證 Authentication authentication = authenticationManager.authenticate(authenticationToken); if(authentication == null) { throw new RuntimeException("用戶名或密碼錯(cuò)誤"); } // 將用戶的數(shù)據(jù)存儲(chǔ)到Redis中 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); String userId = loginUser.getUser().getId().toString(); redisTemplate.boundValueOps("login_user:" + userId).set(JSON.toJSONString(loginUser)); // 生成JWT令牌并進(jìn)行返回 Map<String , String> params = new HashMap<>() ; params.put("userId" , userId) ; String token = JwtUtils.getToken(params); // 構(gòu)建返回?cái)?shù)據(jù) Map<String , String> result = new HashMap<>(); result.put("token" , token) ; return new ResponseResult<Map>(200 , "操作成功" , result); } }
2.3.8 認(rèn)證過(guò)濾器
當(dāng)用戶在訪問(wèn)我們受保護(hù)的資源的時(shí)候,就需要校驗(yàn)用戶是否已經(jīng)登錄。我們需要自定義一個(gè)過(guò)濾器進(jìn)行實(shí)現(xiàn)。
過(guò)濾器內(nèi)部的邏輯:
1、獲取請(qǐng)求頭中的token,對(duì)token進(jìn)行解析。
2、取出其中的userid。
3、使用userid去redis中獲取對(duì)應(yīng)的LoginUser對(duì)象。
4、然后封裝Authentication對(duì)象存入SecurityContextHolder。
5、放行。
注意:這個(gè)過(guò)濾器需要將其加入到Spring Security的過(guò)濾器鏈中
認(rèn)證過(guò)濾器:
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String , String> redisTemplate ; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1、從請(qǐng)求頭中獲取token,如果請(qǐng)求頭中不存在token,直接放行即可!由Spring Security的過(guò)濾器進(jìn)行校驗(yàn)! String token = request.getHeader("token"); if(token == null || "".equals(token)) { filterChain.doFilter(request , response); return ; } // 2、對(duì)token進(jìn)行解析,取出其中的userId String userId = null ; try { Claims claims = JwtUtils.getClaims(token); userId= claims.get("userId").toString(); }catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法") ; } // 3、使用userId從redis中查詢對(duì)應(yīng)的LoginUser對(duì)象 String loginUserJson = redisTemplate.boundValueOps("login_user:" + userId).get(); LoginUser loginUser = JSON.parseObject(loginUserJson, LoginUser.class); if(loginUser != null) { // 4、然后將查詢到的LoginUser對(duì)象的相關(guān)信息封裝到UsernamePasswordAuthenticationToken對(duì)象中,然后將該對(duì)象存儲(chǔ)到Security的上下文對(duì)象中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null , null) ; SecurityContextHolder.getContext().setAuthentication(authenticationToken); } // 5、放行 filterChain.doFilter(request , response); } }
配置過(guò)濾器:
2.3.9 退出登錄
我們只需要定義一個(gè)退出接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對(duì)應(yīng)的數(shù)據(jù)即可。
UserService添加退出登錄接口
@Override public ResponseResult logout() { // 獲取登錄的用戶信息 LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long userId = loginUser.getUser().getId(); // 刪除Redis中的用戶數(shù)據(jù) redisTemplate.delete("login_user:" + userId) ; // 返回 return new ResponseResult(200 , "退出成功" , null) ; }
3 Spring Security授權(quán)功能
3.1 權(quán)限系統(tǒng)的作用
權(quán)限系統(tǒng)作用:保證系統(tǒng)的安全性
舉例:例如一個(gè)學(xué)校圖書(shū)館的管理系統(tǒng),如果是普通學(xué)生登錄以后使用借書(shū)和還書(shū)的功能,不可能讓他具有添加書(shū)籍信息,刪除書(shū)籍信息等功能。但是如果是一個(gè)圖書(shū)館管理員的賬號(hào)登錄了,應(yīng)該就能看到并使用添加書(shū)籍信息,刪除書(shū)籍信息等功能。總結(jié)起來(lái)就是不同的用戶可以使用不同的功能,這就是權(quán)限系統(tǒng)要去實(shí)現(xiàn)的效果。
權(quán)限功能的實(shí)現(xiàn)我們不能只依賴前端去根據(jù)用戶的權(quán)限來(lái)選擇顯示哪些菜單、哪些按鈕。因?yàn)槿绻腥酥懒藢?duì)應(yīng)功能的接口地址就可以不通過(guò)前端,直接去發(fā)送請(qǐng)求來(lái)實(shí)現(xiàn)相關(guān)功能操作。所以我們還需要在后臺(tái)進(jìn)行用戶權(quán)限的判斷,判斷當(dāng)前用戶是否有相應(yīng)的權(quán)限,必須具有所需權(quán)限才能進(jìn)行相應(yīng)的操作。
3.2 授權(quán)基本流程
在Spring Security中,會(huì)使用默認(rèn)的**FilterSecurityInterceptor**來(lái)進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會(huì)從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問(wèn)當(dāng)前資源所需的權(quán)限。所以我們?cè)陧?xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。然后設(shè)置我們的資源所需要的權(quán)限即可。
3.3 入門案例
3.3.1 資源添加所需權(quán)限
Spring Security為我們提供了**基于注解的權(quán)限控制**方案,這也是我們項(xiàng)目中主要采用的方式。我們可以使用注解去指定訪問(wèn)對(duì)應(yīng)的資源所需的權(quán)限。但是要使用它我們需要先開(kāi)啟相關(guān)配置。
開(kāi)啟權(quán)限配置功能
在啟動(dòng)類上添加@EnableGlobalMethodSecurity(prePostEnabled = true)方法添加所需權(quán)限。
不給用戶添加任何權(quán)限信息進(jìn)行測(cè)試,返回信息為:
{ "timestamp": "2022-07-04T06:31:47.821+00:00", "status": 403, "error": "Forbidden", "path": "/hello" }
3.3.2 用戶添加所擁有的權(quán)限
UserDetailsServiceImpl
在UserDetailsServiceImpl中構(gòu)建測(cè)試的權(quán)限數(shù)據(jù),并將其設(shè)置給LoginUser對(duì)象:
LoginUser
LoginUser接收權(quán)限數(shù)據(jù),并且對(duì)getAuthorities方法進(jìn)行改造,返回Spring Security所需要的權(quán)限對(duì)象:
JwtAuthenticationTokenFilter
在JWT過(guò)濾器中需要從Redis中獲取LoginUser對(duì)象,在構(gòu)建UsernamePasswordAuthenticationToken對(duì)象的時(shí)候,為其設(shè)置權(quán)限數(shù)據(jù):
3.4 從數(shù)據(jù)庫(kù)查詢權(quán)限信息
3.4.1 RBAC權(quán)限模型
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開(kāi)發(fā)者使用也是相對(duì)易用、通用權(quán)限模型。
3.4.2 環(huán)境準(zhǔn)備
數(shù)據(jù)庫(kù)環(huán)境準(zhǔn)備
權(quán)限表(菜單表):
CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜單名', `path` varchar(200) DEFAULT NULL COMMENT '路由地址', `component` varchar(255) DEFAULT NULL COMMENT '組件路徑', `visible` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0顯示 1隱藏)', `status` char(1) DEFAULT '0' COMMENT '菜單狀態(tài)(0正常 1停用)', `perms` varchar(100) DEFAULT NULL COMMENT '權(quán)限標(biāo)識(shí)', `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標(biāo)', `create_by` bigint(20) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(20) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `del_flag` int(11) DEFAULT '0' COMMENT '是否刪除(0未刪除 1已刪除)', `remark` varchar(500) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜單表'; # 插入基礎(chǔ)數(shù)據(jù) insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543917775762886657, '添加用戶', '/user/addUser', 'addUser', '0', '0', 'system:user:add', 'icon-add', 1, '2022-07-04 11:20:57', 1, '2022-07-04 11:20:57', 0, '添加用戶按鈕'); insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543918065589379073, '查看用戶列表', '/user/userList', 'userList', '0', '0', 'system:user:list', 'icon-list', 1, '2022-07-04 11:22:06', 1, '2022-07-04 11:22:06', 0, '查看用戶列表用戶按鈕');
角色表:
CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(128) DEFAULT NULL, `role_key` varchar(100) DEFAULT NULL COMMENT '角色權(quán)限字符串', `status` char(1) DEFAULT '0' COMMENT '角色狀態(tài)(0正常 1停用)', `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag', `create_by` bigint(200) DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_by` bigint(200) DEFAULT NULL, `update_time` datetime DEFAULT NULL, `remark` varchar(500) DEFAULT NULL COMMENT '備注', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; # 插入測(cè)試數(shù)據(jù) insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (1, '系統(tǒng)管理員', 'admin', '0', 0, 1, '2022-07-04 19:25:06', 1, '2022-07-04 19:25:19', '系統(tǒng)管理員'); insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (2, '普通用戶', 'user', '0', 0, 1, '2022-07-04 19:25:48', 1, '2022-07-04 19:25:52', '普通用戶角色');
角色菜單中間表:
CREATE TABLE `sys_role_menu` ( `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色I(xiàn)D', `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜單id', PRIMARY KEY (`role_id`,`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; # 插入基礎(chǔ)測(cè)試數(shù)據(jù) insert into security.sys_role_menu (role_id, menu_id) values (1, 1543917775762886657); insert into security.sys_role_menu (role_id, menu_id) values (1, 1543918065589379073); insert into security.sys_role_menu (role_id, menu_id) values (2, 1543918065589379073);
用戶表:
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名', `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱', `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼', `status` char(1) DEFAULT '0' COMMENT '賬號(hào)狀態(tài)(0正常 1停用)', `email` varchar(64) DEFAULT NULL COMMENT '郵箱', `phone_number` varchar(32) DEFAULT NULL COMMENT '手機(jī)號(hào)', `sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)', `avatar` varchar(128) DEFAULT NULL COMMENT '頭像', `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)', `create_by` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建人的用戶id', `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_by` bigint(20) DEFAULT NULL COMMENT '更新人', `update_time` datetime DEFAULT NULL COMMENT '更新時(shí)間', `del_flag` int(11) DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表'; # 插入測(cè)試數(shù)據(jù) insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '張三', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0); insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578310, 'admin', '系統(tǒng)管理員', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);
用戶角色中間表:
CREATE TABLE `sys_user_role` ( `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用戶id', `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; # 插入基礎(chǔ)數(shù)據(jù) insert into security.sys_user_role (user_id, role_id) values (1501123580308578309, 2); insert into security.sys_user_role (user_id, role_id) values (1501123580308578310, 1);
SQL測(cè)試查詢某一個(gè)用戶所具有的權(quán)限:
SELECT distinct m.perms FROM sys_user u left join sys_user_role ur on ur.user_id = u.id left join sys_role_menu rm on rm.role_id = ur.role_id left join sys_menu m on m.id = rm.menu_id WHERE u.id = 1501123580308578310 ;
Menu實(shí)體類
// 菜單表(Menu)實(shí)體類 @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor public class Menu { @TableId private Long id; private String menuName; // 菜單名 private String path; // 路由地址 private String component; // 組件路徑 private String visible; // 菜單狀態(tài)(0顯示 1隱藏) private String status; // 菜單狀態(tài)(0正常 1停用) private String perms; // 權(quán)限標(biāo)識(shí) private String icon; // 菜單圖標(biāo) private Long createBy; // 創(chuàng)建人 private Date createTime; // 創(chuàng)建時(shí)間 private Long updateBy; // 更新人 private Date updateTime; // 更新時(shí)間 private Integer delFlag; // 是否刪除(0未刪除 1已刪除) private String remark; // 備注 }
MenuMapper接口
// 操作菜單表的Mapper接口 public interface MenuMapper extends BaseMapper<Menu> { // 查詢某一個(gè)用戶的權(quán)限信息 public abstract List<String> findUserMenuById(Long userId) ; }
application.yml修改
MenuMapper.xml映射文件
<?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.itheima.security.mapper.MenuMapper"> <select id="findUserMenuById" resultType="java.lang.String"> SELECT distinct m.perms FROM sys_user u left join sys_user_role ur on ur.user_id = u.id left join sys_role_menu rm on rm.role_id = ur.role_id left join sys_menu m on m.id = rm.menu_id WHERE u.id = #{userId} ; </select> </mapper>
3.4.3 UserDetailsService修改
從數(shù)據(jù)庫(kù)中查詢?cè)撚脩舻恼鎸?shí)權(quán)限信息:
4 自定義失敗處理
4.1 實(shí)現(xiàn)思路
我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對(duì)響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
在SpringSecurity中,如果我們?cè)谡J(rèn)證或者授權(quán)的過(guò)程中出現(xiàn)了異常會(huì)被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會(huì)去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
?、?如果是認(rèn)證過(guò)程中出現(xiàn)的異常會(huì)被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對(duì)象的方法去進(jìn)行異常處理。
② 如果是授權(quán)過(guò)程中出現(xiàn)的異常會(huì)被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對(duì)象的方法去進(jìn)行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給Spring Security即可。
4.5 代碼實(shí)現(xiàn)
4.5.1 認(rèn)證失敗處理器
@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(), "認(rèn)證失敗請(qǐng)重新登錄", null); String json = JSON.toJSONString(result) ; WebUtils.renderString(response,json); } }
4.5.2 授權(quán)失敗處理器
@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)限不足" , null); String json = JSON.toJSONString(result); WebUtils.renderString(response,json); } }
4.5.3 Spring Security配置處理器
實(shí)現(xiàn)步驟:
1、先注入對(duì)應(yīng)的處理器
2、使用HttpSecurity對(duì)象的方法去配置
5 跨域處理
5.1 跨域說(shuō)明
瀏覽器出于安全的考慮,使用 XMLHttpRequest對(duì)象發(fā)起 HTTP請(qǐng)求時(shí)必須遵守同源策略,否則就是跨域的HTTP請(qǐng)求,默認(rèn)情況下是被禁止的。
同源策略要求源相同才能正常進(jìn)行通信,所謂的源相同指定是:協(xié)議、域名、端口號(hào)都完全一致。
前后端分離項(xiàng)目,前端項(xiàng)目和后端項(xiàng)目一般都不是同源的,所以肯定會(huì)存在跨域請(qǐng)求的問(wèn)題。
所以我們就要處理一下,讓前端能進(jìn)行跨域請(qǐng)求。
5.2 解決方案
5.2.1 Spring Boot項(xiàng)目添加跨域請(qǐng)求配置
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 設(shè)置允許跨域的路徑 registry.addMapping("/**") // 設(shè)置允許跨域請(qǐng)求的域名 .allowedOriginPatterns("*") // 是否允許cookie .allowCredentials(true) // 設(shè)置允許的請(qǐng)求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 設(shè)置允許的header屬性 .allowedHeaders("*") // 跨域允許時(shí)間 .maxAge(3600); } }
5.2.2 Spring Security開(kāi)啟跨域訪問(wèn)支持
由于我們的資源都會(huì)收到Spring Security的保護(hù),所以想要跨域訪問(wèn)還要讓Spring Security運(yùn)行跨域訪問(wèn)。
//SpringSecurityConfigurer#configure 允許跨域 http.cors();
6 其他問(wèn)題說(shuō)明
6.1 其他權(quán)限校驗(yàn)方式
我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進(jìn)行校驗(yàn)。Spring Security還為我們提供了其它方法例如:hasAnyAuthority,hasRole,
hasAnyRole等。
6.1.2 hasAnyAuthority
hasAnyAuthority方法可以傳入多個(gè)權(quán)限,只有用戶有其中任意一個(gè)權(quán)限都可以訪問(wèn)對(duì)應(yīng)資源。
6.1.3 hasRole
hasRole要求有對(duì)應(yīng)的角色才可以訪問(wèn),但是它內(nèi)部會(huì)把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對(duì)應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
6.1.4 hasAnyRole
hasAnyRole 有任意的角色就可以訪問(wèn)。它內(nèi)部也會(huì)把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對(duì)應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
6.2 基于配置的權(quán)限控制
我們也可以在配置類中使用使用配置的方式對(duì)資源進(jìn)行權(quán)限控制。
注意: 如果此時(shí)在方法上使用了@PreAuthorize(value = "hasAuthority('system:user:add')")指定了權(quán)限信息,那么就需要用于同時(shí)擁有兩個(gè)權(quán)限才可以進(jìn)行訪問(wèn)。
6.3 CSRF
CSRF是指跨站請(qǐng)求偽造(Cross-site request forgery),是web常見(jiàn)的攻擊之一。https://blog.csdn.net/freeking101/article/details/86537087
Spring Security去防止CSRF攻擊的方式就是通過(guò)csrf_token。后端會(huì)生成一個(gè)csrf_token,前端發(fā)起請(qǐng)求的時(shí)候需要攜帶這個(gè)csrf_token,后端會(huì)有過(guò)濾器進(jìn)行校驗(yàn),如果沒(méi)有攜帶或者是偽造的就不允許訪問(wèn)。
我們可以發(fā)現(xiàn)CSRF攻擊依靠的是cookie中所攜帶的認(rèn)證信息。但是在前后端分離的項(xiàng)目中我們的認(rèn)證信息其實(shí)是token,而token并不是存儲(chǔ)在cookie中,并且需要前端代碼去把token設(shè)置到請(qǐng)求頭中才可以,所以CSRF攻擊也就不用擔(dān)心了。
7 .總結(jié)
本文章給大家介紹了一下在前后端分離項(xiàng)目中如何使用Spring Security完成認(rèn)證和授權(quán)的相關(guān)操作,并且介紹一下如何自定義認(rèn)證和授權(quán)失敗的處理器,以及如何解決跨域的相關(guān)問(wèn)題。大家可以參考本文章實(shí)際操作一下,相信大家很快就可以掌握Spring Security在前后端分離項(xiàng)目中的使用。