首頁技術(shù)文章正文

Spring Security在前后端分離項(xiàng)目中的使用

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

  1 文章導(dǎo)讀

  Spring Security 是 Spring 家族中的一個(gè)**安全管理框架,可以和Spring Boot項(xiàng)目很方便的集成。Spring Security框架的兩大核心功能:認(rèn)證和授權(quán)。

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

1668652670836_1.jpg

  授權(quán):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作!

 

1668652683640_2.jpg

  如上圖所示就是展示了當(dāng)前登錄用戶可以操作的權(quán)限:用戶管理、角色管理、菜單管理等,并且針對(duì)角色管理可以進(jìn)行新增、修改、刪除、導(dǎo)出等權(quán)限。

  而現(xiàn)在前后端分離開發(fā)成為了主流的開發(fā)方式,那么在前后端分離開發(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)證流程,如下所示:

1668652705141_3png.jpg

  2.2 Spring Security原理初探

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

  2.2.1 過濾器鏈

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

1668652722459_4.jpg

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

  UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理我們?cè)诘顷戫撁嫣顚懥擞脩裘艽a后的登陸請(qǐng)求。

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

  FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過濾器。

  2.2.2 認(rèn)證流程

  Spring Security的認(rèn)證流程大致如下所示:

1668652746683_5.jpg

  概念速查:

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

  AuthenticationManager接口:定義了認(rèn)證Authentication的方法。

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

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

  概念速查:

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

  AuthenticationManager接口:定義了認(rèn)證Authentication的方法。

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

  UserDetails接口:提供核心用戶信息。通過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)證就無法實(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)證通過生成jwt

   ② 把用戶信息存入redis中

  2、自定義UserDetailsService

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

  校驗(yàn):

  1、定義Jwt認(rèn)證過濾器

   ① 獲取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ù)庫驅(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ù)庫表

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ù)庫相關(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>

  編寫測(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ù)庫中獲取用戶的數(shù)據(jù),那么此時(shí)就需要自定義一個(gè)UserDetailsService來覆蓋默認(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ù),說明用戶名或者密碼錯(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;

// 用來封裝數(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() {          // 賬號(hào)是否沒有過期
        return true;
    }

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

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

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

  測(cè)試認(rèn)證

  先通過Spring Security提供的默認(rèn)登錄接口進(jìn)行認(rèn)證的測(cè)試,需要啟動(dòng)Redis。此時(shí)控制臺(tái)會(huì)輸出如下錯(cuò)誤:

1668653160546_6.jpg

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

  解決方案:

  ① 使用明文認(rèn)證

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

1668653199614_7.jpg

  ② 配置加密算法

  2.3.6 配置加密算法

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

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

  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ù)庫的用戶密碼更改為使用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è)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。

  ② 在接口中我們通過**AuthenticationManager**的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在Security Config中配置把AuthenticationManager注入容器。

  ③ 認(rèn)證成功的話要生成一個(gè)jwt,將jwt令牌進(jìn)行返回。并且為了讓用戶下回請(qǐng)求時(shí)能通過jwt識(shí)別出具體的是哪個(gè)用戶,在返回之前,我們需要把用戶信息存入redis,可以把用戶id作為key。

  攔截規(guī)則配置

  在SpringSecurityConfigurer中重寫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ì)于登錄接口,允許匿名訪問
            .anyRequest().authenticated();                                                  // 除了上面的請(qǐng)求以外所有的請(qǐng)求全部需要認(rèn)證
}

  Spring容器注冊(cè)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對(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)證過濾器

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

  過濾器內(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è)過濾器需要將其加入到Spring Security的過濾器鏈中

  認(rèn)證過濾器:

@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的過濾器進(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);
    }

}

  配置過濾器:

1668653570365_9.jpg

  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é)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄以后使用借書和還書的功能,不可能讓他具有添加書籍信息,刪除書籍信息等功能。但是如果是一個(gè)圖書館管理員的賬號(hào)登錄了,應(yīng)該就能看到并使用添加書籍信息,刪除書籍信息等功能??偨Y(jié)起來就是不同的用戶可以使用不同的功能,這就是權(quán)限系統(tǒng)要去實(shí)現(xiàn)的效果。

  權(quán)限功能的實(shí)現(xiàn)我們不能只依賴前端去根據(jù)用戶的權(quán)限來選擇顯示哪些菜單、哪些按鈕。因?yàn)槿绻腥酥懒藢?duì)應(yīng)功能的接口地址就可以不通過前端,直接去發(fā)送請(qǐng)求來實(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**來進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會(huì)從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。當(dāng)前用戶是否擁有訪問當(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)目中主要采用的方式。我們可以使用注解去指定訪問對(duì)應(yīng)的資源所需的權(quán)限。但是要使用它我們需要先開啟相關(guān)配置。

  開啟權(quán)限配置功能

  在啟動(dòng)類上添加@EnableGlobalMethodSecurity(prePostEnabled = true)方法添加所需權(quán)限。

1668653685820_10.jpg

  不給用戶添加任何權(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ì)象:

1668653734894_11.jpg

  LoginUser

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

1668653752919_12.jpg

  JwtAuthenticationTokenFilter

  在JWT過濾器中需要從Redis中獲取LoginUser對(duì)象,在構(gòu)建UsernamePasswordAuthenticationToken對(duì)象的時(shí)候,為其設(shè)置權(quán)限數(shù)據(jù):

1668653768603_13.jpg

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

  3.4.1 RBAC權(quán)限模型

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

1668653790403_14.jpg

  3.4.2 環(huán)境準(zhǔn)備

  數(shù)據(jù)庫環(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修改

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ù)庫中查詢?cè)撚脩舻恼鎸?shí)權(quán)限信息:

1668654176350_16.jpg

  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)的過程中出現(xiàn)了異常會(huì)被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會(huì)去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。

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

 ?、?如果是授權(quán)過程中出現(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ì)象的方法去配置

1668654280395_17.jpg

  5 跨域處理

  5.1 跨域說明

  瀏覽器出于安全的考慮,使用 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)求的問題。

  所以我們就要處理一下,讓前端能進(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開啟跨域訪問支持

  由于我們的資源都會(huì)收到Spring Security的保護(hù),所以想要跨域訪問還要讓Spring Security運(yùn)行跨域訪問。

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

  6 其他問題說明

  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)限都可以訪問對(duì)應(yīng)資源。

1668654376310_18.jpg

  6.1.3 hasRole

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

1668654517587_19.jpg

  6.1.4 hasAnyRole

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

1668654554408_20.jpg

  6.2 基于配置的權(quán)限控制

  我們也可以在配置類中使用使用配置的方式對(duì)資源進(jìn)行權(quán)限控制。

1668654573285_21.jpg

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

  6.3 CSRF

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

  Spring Security去防止CSRF攻擊的方式就是通過csrf_token。后端會(huì)生成一個(gè)csrf_token,前端發(fā)起請(qǐng)求的時(shí)候需要攜帶這個(gè)csrf_token,后端會(huì)有過濾器進(jìn)行校驗(yà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)問題。大家可以參考本文章實(shí)際操作一下,相信大家很快就可以掌握Spring Security在前后端分離項(xiàng)目中的使用。

分享到:
在線咨詢 我要報(bào)名
和我們?cè)诰€交談!