首頁技術文章正文

Spring Security在前后端分離項目中的使用

更新時間:2022-11-17 來源:黑馬程序員 瀏覽量:

  1 文章導讀

  Spring Security 是 Spring 家族中的一個**安全管理框架,可以和Spring Boot項目很方便的集成。Spring Security框架的兩大核心功能:認證和授權。

  認證:驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶。簡單的理解就是登陸操作,如果可以登錄成功就說明您是本系統(tǒng)的用戶,如不能登錄就說明不是本系統(tǒng)的用戶!而且登錄成功以后需要記錄當前登錄用戶的信息!

1668652670836_1.jpg

  授權:經(jīng)過認證后判斷當前用戶是否有權限進行某個操作!

 

1668652683640_2.jpg

  如上圖所示就是展示了當前登錄用戶可以操作的權限:用戶管理、角色管理、菜單管理等,并且針對角色管理可以進行新增、修改、刪除、導出等權限。

  而現(xiàn)在前后端分離開發(fā)成為了主流的開發(fā)方式,那么在前后端分離開發(fā)方式下如何使用Spring Security就是本文章需要重點研究的內(nèi)容。

  2 Spring Security認證功能

  2.1 前端分離項目的認證流程

  要想了解如果使用Spring Security進行認證,那么就需要先了解一下前后端分離項目中的認證流程,如下所示:

1668652705141_3png.jpg

  2.2 Spring Security原理初探

  要想使用Spring Security框架來實現(xiàn)上述的認證操作,就必須先要了解一個Spring Security框架的工作流程。

  2.2.1 過濾器鏈

  Spring Security的原理其實就是一個過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器。

1668652722459_4.jpg

  圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示。

  UsernamePasswordAuthenticationFilter: 負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。

  ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。

  FilterSecurityInterceptor:負責權限校驗的過濾器。

  2.2.2 認證流程

  Spring Security的認證流程大致如下所示:

1668652746683_5.jpg

  概念速查:

  Authentication接口: 它的實現(xiàn)類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關信息。

  AuthenticationManager接口:定義了認證Authentication的方法。

  UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。

  UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。

  概念速查:

  Authentication接口: 它的實現(xiàn)類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關信息。

  AuthenticationManager接口:定義了認證Authentication的方法。

  UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(jù)用戶名查詢用戶信息的方法。

  UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。

  2.3 認證實現(xiàn)

  在前后端分離項目中,前端請求的是我們自己定義的認證接口。因為在認證成功以后就需要針對當前用戶生成token,Spring Security中提供的原始認證就無法實現(xiàn)了。在我們自定義的認證接口中,需要調(diào)用Spring Security的API借助于Spring Security實現(xiàn)認證。

  2.3.1 思路分析

  認證:

  1、自定義認證接口

   ① 調(diào)用ProviderManager的方法進行認證 如果認證通過生成jwt

   ② 把用戶信息存入redis中

  2、自定義UserDetailsService

   ① 在這個實現(xiàn)類中去查詢數(shù)據(jù)庫

  校驗:

  1、定義Jwt認證過濾器

   ① 獲取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的相關配置

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ù)庫驅動 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- lombok依賴包 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

  創(chuàng)建數(shù)據(jù)庫表

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 '賬號狀態(tài)(0正常 1停用)',
  `email` VARCHAR(64) DEFAULT NULL COMMENT '郵箱',
  `phone_number` VARCHAR(32) DEFAULT NULL COMMENT '手機號',
  `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)建時間',
  `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新時間',
  `del_flag` INT(11) DEFAULT '0' COMMENT '刪除標志(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ù)庫相關配置

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實體類

@Data
@TableName(value = "sys_user")
public class User {

    @TableId
    private Long id ;                         // 唯一標識
    private String userName ;                // 用戶名
    private String nickName ;                // 昵稱
    private String password ;                // 密碼
    private String status ;                  // 狀態(tài) 賬號狀態(tài)(0正常 1停用)
    private String email ;                   // 郵箱
    private String phoneNumber ;            // 電話號碼
    private String sex ;                     // 性別  用戶性別(0男,1女,2未知)
    private String avatar ;                  // 用戶頭像
    private String userType ;                // 用戶類型 (0管理員,1普通用戶)
    private Long createBy ;                  // 創(chuàng)建人
    private Date createTime ;                // 創(chuàng)建時間
    private Long updateBy ;                  // 更新人
    private Date updateTime ;                // 更新時間
    private Integer delFlag ;                // 是否刪除  (0代表未刪除,1代表已刪除)
   
}

  UserMapper接口

public interface UserMapper extends BaseMapper<User> { }

  啟動類

@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>

  編寫測試類

@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的整個認證流程中會調(diào)用會調(diào)用UserDetailsService中的loadUserByUsername方法根據(jù)用戶名稱查詢用戶數(shù)據(jù)。默認情況下調(diào)用的是InMemoryUserDetailsManager中的方法,該UserDetailsService是從內(nèi)存中獲取用戶的數(shù)據(jù)?,F(xiàn)在我們需要從數(shù)據(jù)庫中獲取用戶的數(shù)據(jù),那么此時就需要自定義一個UserDetailsService來覆蓋默認的配置。

  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ù),說明用戶名或者密碼錯誤,直接拋出異常
        if(user == null) {
            throw new RuntimeException("用戶名或者密碼錯誤") ;
        }

        // 將查詢到的對象轉換成Spring Security所需要的UserDetails對象
        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;

// 用來封裝數(shù)據(jù)庫查詢出來的用戶數(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() {          // 賬號是否沒有過期
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {           // 賬號是否沒有被鎖定
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {      // 賬號的憑證是否沒有過期
        return true;
    }

    @Override
    public boolean isEnabled() {                    // 賬號是否可用
        return true;
    }
}

  測試認證

  先通過Spring Security提供的默認登錄接口進行認證的測試,需要啟動Redis。此時控制臺會輸出如下錯誤:

1668653160546_6.jpg

  報錯的原因:默認情況下Spring Security在獲取到UserDetailsService返回的用戶信息以后,會調(diào)用PasswordEncoder中的matches方法進行校驗,但是此時在Spring容器中并不存在任何的PasswordEncoder的對象,因此無法完成校驗操作。

  解決方案:

 ?、?使用明文認證

  要使用明文進行認證,就需要在密碼字段值的前面添加{noop}字樣!

1668653199614_7.jpg

 ?、?配置加密算法

  2.3.6 配置加密算法

  一般情況下關于密碼在數(shù)據(jù)庫中都是密文存儲的,在進行認證的時候都是基于密文進行校驗。具體的實現(xiàn)步驟:

  1、使用指定的加密算法【**BCrypt**】對密碼進行加密處理,將加密以后的密文存儲到數(shù)據(jù)庫中

  2、在Spring容器中注入一個PasswordEncoder對象,一般情況下注入的就是:BCryptPasswordEncoder

  我們可以定義一個Spring Security的配置類,Spring Security要求這個配置類要繼承WebSecurityConfigurerAdapter。

@Configuration
public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder() ;
    }

}

  測試:將數(shù)據(jù)庫的用戶密碼更改為使用BCryptPasswordEncoder加密以后的密文

@SpringBootTest(classes = SecurityApplication.class)
public class SecurityApplicationTest {

    @Autowired
    private PasswordEncoder passwordEncoder ;

    @Test
    public void testBcrypt() {
        // 加密測試
        String encode = passwordEncoder.encode("1234");
        System.out.println(encode);

        // 校驗測試
        boolean matches = passwordEncoder.matches("1234", "$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm");
        System.out.println(matches);
    }
}

  2.3.7 登錄接口

  整體實現(xiàn)思路:

  ① 接下我們需要自定義登陸接口,然后讓Spring Security對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。

 ?、?在接口中我們通過**AuthenticationManager**的authenticate方法來進行用戶認證,所以需要在Security Config中配置把AuthenticationManager注入容器。

 ?、?認證成功的話要生成一個jwt,將jwt令牌進行返回。并且為了讓用戶下回請求時能通過jwt識別出具體的是哪個用戶,在返回之前,我們需要把用戶信息存入redis,可以把用戶id作為key。

  攔截規(guī)則配置

  在SpringSecurityConfigurer中重寫configure(HttpSecurity http)方法:

// 配置Spring Security的攔截規(guī)則
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .csrf().disable()                                                               // 關閉csrf
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     // 指定session的創(chuàng)建策略,不使用session
            .and()                                                                          // 再次獲取到HttpSecurity對象
            .authorizeRequests()                                                            // 進行認證請求的配置
            .antMatchers("/user/login").anonymous()                                         // 對于登錄接口,允許匿名訪問
            .anyRequest().authenticated();                                                  // 除了上面的請求以外所有的請求全部需要認證
}

  Spring容器注冊AuthenticationManager

  在SpringSecurityConfigurer中重寫authenticationManagerBean方法:

1668653355037_8.jpg

  登錄接口定義

  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對象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName() , user.getPassword()) ;

        // 調(diào)用AuthenticationManager的authenticate方法進行認證
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        if(authentication == null) {
            throw new RuntimeException("用戶名或密碼錯誤");
        }

        // 將用戶的數(shù)據(jù)存儲到Redis中
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        redisTemplate.boundValueOps("login_user:" + userId).set(JSON.toJSONString(loginUser));

        // 生成JWT令牌并進行返回
        Map<String , String> params = new HashMap<>() ;
        params.put("userId" , userId) ;
        String token = JwtUtils.getToken(params);

        // 構建返回數(shù)據(jù)
        Map<String , String> result = new HashMap<>();
        result.put("token" , token) ;
        return new ResponseResult<Map>(200 , "操作成功" , result);

    }

}

  2.3.8 認證過濾器

  當用戶在訪問我們受保護的資源的時候,就需要校驗用戶是否已經(jīng)登錄。我們需要自定義一個過濾器進行實現(xiàn)。

  過濾器內(nèi)部的邏輯:

  1、獲取請求頭中的token,對token進行解析。

  2、取出其中的userid。

  3、使用userid去redis中獲取對應的LoginUser對象。

  4、然后封裝Authentication對象存入SecurityContextHolder。

  5、放行。

  注意:這個過濾器需要將其加入到Spring Security的過濾器鏈中

  認證過濾器:

@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、從請求頭中獲取token,如果請求頭中不存在token,直接放行即可!由Spring Security的過濾器進行校驗!
        String token = request.getHeader("token");
        if(token == null || "".equals(token)) {
            filterChain.doFilter(request , response);
            return ;
        }

        // 2、對token進行解析,取出其中的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中查詢對應的LoginUser對象
        String loginUserJson = redisTemplate.boundValueOps("login_user:" + userId).get();
        LoginUser loginUser = JSON.parseObject(loginUserJson, LoginUser.class);
        if(loginUser != null) {
            // 4、然后將查詢到的LoginUser對象的相關信息封裝到UsernamePasswordAuthenticationToken對象中,然后將該對象存儲到Security的上下文對象中
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null , null) ;
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
       
        // 5、放行
        filterChain.doFilter(request , response);
    }

}

  配置過濾器:

1668653570365_9.jpg

  2.3.9 退出登錄

  我們只需要定義一個退出接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數(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授權功能

  3.1 權限系統(tǒng)的作用

  權限系統(tǒng)作用:保證系統(tǒng)的安全性

  舉例:例如一個學校圖書館的管理系統(tǒng),如果是普通學生登錄以后使用借書和還書的功能,不可能讓他具有添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應該就能看到并使用添加書籍信息,刪除書籍信息等功能??偨Y起來就是不同的用戶可以使用不同的功能,這就是權限系統(tǒng)要去實現(xiàn)的效果。

  權限功能的實現(xiàn)我們不能只依賴前端去根據(jù)用戶的權限來選擇顯示哪些菜單、哪些按鈕。因為如果有人知道了對應功能的接口地址就可以不通過前端,直接去發(fā)送請求來實現(xiàn)相關功能操作。所以我們還需要在后臺進行用戶權限的判斷,判斷當前用戶是否有相應的權限,必須具有所需權限才能進行相應的操作。

  3.2 授權基本流程

  在Spring Security中,會使用默認的**FilterSecurityInterceptor**來進行權限校驗。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權限信息。當前用戶是否擁有訪問當前資源所需的權限。所以我們在項目中只需要把當前登錄用戶的權限信息也存入Authentication。然后設置我們的資源所需要的權限即可。

  3.3 入門案例

  3.3.1 資源添加所需權限

  Spring Security為我們提供了**基于注解的權限控制**方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權限。但是要使用它我們需要先開啟相關配置。

  開啟權限配置功能

  在啟動類上添加@EnableGlobalMethodSecurity(prePostEnabled = true)方法添加所需權限。

1668653685820_10.jpg

  不給用戶添加任何權限信息進行測試,返回信息為:

{
    "timestamp": "2022-07-04T06:31:47.821+00:00",
    "status": 403,
    "error": "Forbidden",
    "path": "/hello"
}

  3.3.2 用戶添加所擁有的權限

  UserDetailsServiceImpl

  在UserDetailsServiceImpl中構建測試的權限數(shù)據(jù),并將其設置給LoginUser對象:

1668653734894_11.jpg

  LoginUser

  LoginUser接收權限數(shù)據(jù),并且對getAuthorities方法進行改造,返回Spring Security所需要的權限對象:

1668653752919_12.jpg

  JwtAuthenticationTokenFilter

  在JWT過濾器中需要從Redis中獲取LoginUser對象,在構建UsernamePasswordAuthenticationToken對象的時候,為其設置權限數(shù)據(jù):

1668653768603_13.jpg

  3.4 從數(shù)據(jù)庫查詢權限信息

  3.4.1 RBAC權限模型

  RBAC權限模型(Role-Based Access Control)即:基于角色的權限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權限模型。

1668653790403_14.jpg

  3.4.2 環(huán)境準備

  數(shù)據(jù)庫環(huá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 '權限標識',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標',
  `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='菜單表';

# 插入基礎數(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 '角色權限字符串',
  `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='角色表';

# 插入測試數(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 '角色ID',
  `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜單id',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

# 插入基礎測試數(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 '賬號狀態(tài)(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '郵箱',
  `phone_number` varchar(32) DEFAULT NULL COMMENT '手機號',
  `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)建時間',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新時間',
  `del_flag` int(11) DEFAULT '0' COMMENT '刪除標志(0代表未刪除,1代表已刪除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 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', '張三', '$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;

# 插入基礎數(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測試查詢某一個用戶所具有的權限:

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實體類

// 菜單表(Menu)實體類
@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;           // 權限標識
    private String icon;            // 菜單圖標
    private Long createBy;          // 創(chuàng)建人
    private Date createTime;        // 創(chuàng)建時間
    private Long updateBy;          // 更新人
    private Date updateTime;        // 更新時間
    private Integer delFlag;        // 是否刪除(0未刪除 1已刪除)
    private String remark;          // 備注
   
}

  MenuMapper接口

// 操作菜單表的Mapper接口
public interface MenuMapper extends BaseMapper<Menu> {

    // 查詢某一個用戶的權限信息
    public abstract List<String> findUserMenuById(Long userId) ;

}

  application.yml修改

1668654127424_15.jpg

  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ù)庫中查詢該用戶的真實權限信息:

1668654176350_16.jpg

  4 自定義失敗處理

  4.1 實現(xiàn)思路

  我們還希望在認證失敗或者是授權失敗的情況下也能和我們的接口一樣返回相同結構的json,這樣可以讓前端能對響應進行統(tǒng)一的處理。要實現(xiàn)這個功能我們需要知道SpringSecurity的異常處理機制。

  在SpringSecurity中,如果我們在認證或者授權的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權失敗出現(xiàn)的異常。

 ?、?如果是認證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對象的方法去進行異常處理。

 ?、?如果是授權過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進行異常處理。

  所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給Spring Security即可。

  4.5 代碼實現(xiàn)

  4.5.1 認證失敗處理器

@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(), "認證失敗請重新登錄", null);
        String json = JSON.toJSONString(result) ;
        WebUtils.renderString(response,json);
    }

}

  4.5.2 授權失敗處理器

@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(), "權限不足" , null);
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);

    }

}

  4.5.3 Spring Security配置處理器

  實現(xiàn)步驟:

  1、先注入對應的處理器

  2、使用HttpSecurity對象的方法去配置

1668654280395_17.jpg

  5 跨域處理

  5.1 跨域說明

  瀏覽器出于安全的考慮,使用 XMLHttpRequest對象發(fā)起 HTTP請求時必須遵守同源策略,否則就是跨域的HTTP請求,默認情況下是被禁止的。

  同源策略要求源相同才能正常進行通信,所謂的源相同指定是:協(xié)議、域名、端口號都完全一致。

  前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。

  所以我們就要處理一下,讓前端能進行跨域請求。

  5.2 解決方案

  5.2.1 Spring Boot項目添加跨域請求配置

@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);
    }
}

  5.2.2 Spring Security開啟跨域訪問支持

  由于我們的資源都會收到Spring Security的保護,所以想要跨域訪問還要讓Spring Security運行跨域訪問。

//SpringSecurityConfigurer#configure 允許跨域
http.cors();

  6 其他問題說明

  6.1 其他權限校驗方式

  我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。Spring Security還為我們提供了其它方法例如:hasAnyAuthority,hasRole,

  hasAnyRole等。

  6.1.2 hasAnyAuthority

  hasAnyAuthority方法可以傳入多個權限,只有用戶有其中任意一個權限都可以訪問對應資源。

1668654376310_18.jpg

  6.1.3 hasRole

  hasRole要求有對應的角色才可以訪問,但是它內(nèi)部會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權限也要有 ROLE_ 這個前綴才可以。

1668654517587_19.jpg

  6.1.4 hasAnyRole

  hasAnyRole 有任意的角色就可以訪問。它內(nèi)部也會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權限也要有 ROLE_ 這個前綴才可以。

1668654554408_20.jpg

  6.2 基于配置的權限控制

  我們也可以在配置類中使用使用配置的方式對資源進行權限控制。

1668654573285_21.jpg

  注意: 如果此時在方法上使用了@PreAuthorize(value = "hasAuthority('system:user:add')")指定了權限信息,那么就需要用于同時擁有兩個權限才可以進行訪問。

  6.3 CSRF

  CSRF是指跨站請求偽造(Cross-site request forgery),是web常見的攻擊之一。https://blog.csdn.net/freeking101/article/details/86537087

  Spring Security去防止CSRF攻擊的方式就是通過csrf_token。后端會生成一個csrf_token,前端發(fā)起請求的時候需要攜帶這個csrf_token,后端會有過濾器進行校驗,如果沒有攜帶或者是偽造的就不允許訪問。

  我們可以發(fā)現(xiàn)CSRF攻擊依靠的是cookie中所攜帶的認證信息。但是在前后端分離的項目中我們的認證信息其實是token,而token并不是存儲在cookie中,并且需要前端代碼去把token設置到請求頭中才可以,所以CSRF攻擊也就不用擔心了。

  7 .總結

  本文章給大家介紹了一下在前后端分離項目中如何使用Spring Security完成認證和授權的相關操作,并且介紹一下如何自定義認證和授權失敗的處理器,以及如何解決跨域的相關問題。大家可以參考本文章實際操作一下,相信大家很快就可以掌握Spring Security在前后端分離項目中的使用。

分享到:
在線咨詢 我要報名
和我們在線交談!