SpringBoot整合Srping Security实现权限控制

文章目录

  • 核心概念介绍
    • [1. 认证](#1. 认证)
    • [2. 授权](#2. 授权)
  • 实现原理
  • 整合Springboot实现权限控制
    • [1. 引入依赖](#1. 引入依赖)
    • [2. 定义权限配置类](#2. 定义权限配置类)
    • [3. 配置处理器](#3. 配置处理器)
    • [4. 用户信息提供者](#4. 用户信息提供者)
    • [5. 在接口上设置权限](#5. 在接口上设置权限)

核心概念介绍

Spring Security 实现权限控制主要基于两大核心机制:认证(Authentication)授权(Authorization)

1. 认证

即输入账号密码登录,系统判断用户身份是否合法的过程。

2. 授权

授权是在认证后发生的,根据用户权限来控制访问不同资源。

2.1基于角色的访问控制

Role-Based Access Control,即按角色进行授权。

在业务代码中可以这样写:

java 复制代码
//伪代码
if(主体.hasRole("总经理角色id")){
  查询工资;
}

如果扩展,让部门经理也可以查工资,则需要改动代码:

java 复制代码
//伪代码
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
  查询工资;
}

这样看,当授权角色发生变化时,需要修改代码,可扩展性差。

2.2 基于资源的访问控制【推荐】

Resource-Based Access Control,即按资源进行授权

如果这个人有查询工资的权限,就可以查询工资。

伪代码可以是:

java 复制代码
if(主体.hasPermission("查询工资权限标识")){
  查询工资
}

实现原理

spring security的底层是由servlet过滤器实现的。

当一个 HTTP 请求到达时,会依次经过多个Filter,每个过滤器负责特定安全任务。(所有请求都会经过servlet,而在经过servlet之前会先通过过滤链)

整合Springboot实现权限控制

1. 引入依赖

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
  </parent>
  <groupId>com.whl.security</groupId>
  <artifactId>security-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>security-demo</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <!--spring web-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--spring security-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!--thymeleaf-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    </dependency>
    <!--test-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!--mysql-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
    </dependency>
    <!--mybatis plus-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.5.4.1</version>
      <exclusions>
        <exclusion>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>3.0.3</version>
    </dependency>

    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
    <!--swagger测试-->
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
      <version>4.1.0</version>
    </dependency>
    <!--fastjson-->
    <dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>2.0.9</version>
    </dependency>

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

2. 定义权限配置类

java 复制代码
@Configuration //配置类
@EnableMethodSecurity
public class WebSecurityConfig {
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    //开启授权保护
    http.authorizeHttpRequests(
          (authorize) -> authorize
//            .requestMatchers("/user/list").hasAuthority("USER_LIST")
//            .requestMatchers("/user/add").hasAuthority("USER_ADD")
            .requestMatchers("/user/**").hasRole("ADMIN")
          //对所有请求开启授权保护
          .anyRequest()
          //已认证的请求会被自动授权
          .authenticated()
    );
    //自动使用表单授权方式【注释后就没有html的登录页面了,就只有浏览器默认自带的】
    http.formLogin(form -> {
      form.loginPage("/login").permitAll()
        .usernameParameter("myusername")
        .passwordParameter("mypassword")
        .failureUrl("/login?error")//校验失败时跳转
        .successHandler(new MyAuthenticationSuccessHandler())//认证成功时的处理
        .failureHandler(new MyAuthenticationFailureHandler())//认证失败时的处理
      ;
    });

    http.logout(logout -> {
      logout.logoutSuccessHandler(new MyLogoutSuccessHandler()); //注销成功处理
    });

    http.exceptionHandling(exception -> {
      exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); //请求未认证处理
      exception.accessDeniedHandler(new MyAccessDeniedHandler());
    });

    //会话并发处理
    http.sessionManagement(session -> {
      session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy());
    });

    //跨域
    http.cors(withDefaults());

    //关闭CSRF攻击防御
    http.csrf(csrf -> csrf.disable());

    return http.build();
  }
}

3. 配置处理器

  • 认证成功时
java 复制代码
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                      Authentication authentication) throws IOException, ServletException {
	  //获取用户身份信息
	  Object principal = authentication.getPrincipal();
	  //    //获取用户凭证信息
	  //    Object credentials = authentication.getCredentials();
	  //    //获取用户权限信息
	  //    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

	  HashMap result = new HashMap();
	  result.put("code", 0);
	  result.put("msg", "登录成功");
	  result.put("data", principal);

 	  //将对象转为json字符串
	  String json = JSON.toJSONString(result);

	  //认证成功后返回json数据
	  response.setContentType("application/json;charset=UTF-8");
	  response.getWriter().println(json);

	}
}
  • 认证失败时
java 复制代码
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException,                     ServletException {

    String localizedMessage = exception.getLocalizedMessage();

    HashMap result = new HashMap();
    result.put("code", -1);
    result.put("msg", localizedMessage);

    //将对象转为json字符串
    String json = JSON.toJSONString(result);

    //认证成功后返回json数据
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().println(json);
  }
}
  • 注销成功处理
java 复制代码
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
	@Override
	public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                              Authentication authentication) throws IOException, ServletException {

		HashMap result = new HashMap();
		result.put("code", 0);//成功
		result.put("msg", "注销成功");

		//将对象转为json字符串
		String json = JSON.toJSONString(result);

		//认证成功后返回json数据
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().println(json);
	}
}
  • 请求未认证处理
java 复制代码
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
                       AuthenticationException authException) throws IOException, ServletException {
  	String localizedMessage = "需要登录"; //authException.getLocalizedMessage();

  	HashMap result = new HashMap();
  	result.put("code", -1); //失败
  	result.put("msg", localizedMessage);

  	//将对象转为json字符串
  	String json = JSON.toJSONString(result);

  	//认证成功后返回json数据
  	response.setContentType("application/json;charset=UTF-8");
  	response.getWriter().println(json);
  }
}
  • 请求未授权时
java 复制代码
public class MyAccessDeniedHandler implements AccessDeniedHandler {
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
                      AccessDeniedException accessDeniedException) throws IOException, ServletException {

		HashMap result = new HashMap();
		result.put("code", -1); //失败
		result.put("msg", "没有权限");

		//将对象转为json字符串
		String json = JSON.toJSONString(result);

		//认证成功后返回json数据
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().println(json);
    }
}
  • 会话失效时处理
java 复制代码
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {

 /**
  * 当会话失效时的处理.
  *
  * @param event
  * @throws IOException
  * @throws ServletException
  */
  @Override
  public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException,
      		ServletException {

    	HashMap result = new HashMap();
    result.put("code", -1); //失败
    result.put("msg", "该账号已从其他设备登录");

    //将对象转为json字符串
    String json = JSON.toJSONString(result);

    //返回响应
    HttpServletResponse response = event.getResponse();
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().println(json);

  }
}

4. 用户信息提供者

java 复制代码
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {

  //引入持久层
  @Resource
  private UserMapper userMapper;

  @Override
  public UserDetails updatePassword(UserDetails user, String newPassword) {
    return null;
  }

  /**
   * 向数据库中插入新的用户信息.
   *
   * @param userDetails 用户信息
   */
  @Override
  public void createUser(UserDetails userDetails) {
    //实现数据插入
    User user = new User();
    user.setUsername(userDetails.getUsername());
    user.setPassword(userDetails.getPassword());
    user.setEnabled(true);

    userMapper.insert(user);

  }

  @Override
  public void updateUser(UserDetails user) {

  }

  @Override
  public void deleteUser(String username) {

  }

  @Override
  public void changePassword(String oldPassword, String newPassword) {

  }

  @Override
  public boolean userExists(String username) {
    return false;
  }

  /**
   * 通过用户名从数据库中获取用户信息.
   *
   * @param username 用户名
   * @return UserDetails对象
   * @throws UsernameNotFoundException
   */
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    if (user == null) {
      throw new UsernameNotFoundException(username);
    } else {
      /** Collection<GrantedAuthority> authorities = new ArrayList<>();
//      authorities.add(() -> "USER_LIST");
      authorities.add(() -> "USER_ADD");

      //组装security中的user对象
      return new org.springframework.security.core.userdetails.User(
          user.getUsername(),
          user.getPassword(),
          user.getEnabled(),
          true, //用户账号是否过期
          true, //用户凭证是否过期
          true, //用户是否未被锁定
          authorities //权限列表【暂时先创建空的】
      );*/

      return org.springframework.security.core.userdetails.User
        .withUsername(user.getUsername())
        .password(user.getPassword())
        .disabled(!user.getEnabled())
        .credentialsExpired(false)
        .accountLocked(false)
        .roles("ADMIN")
        .authorities("USER_ADD")
        .build();

    }

  }
}

这里loadUserByUsername方法中,authorities()中的内容是写死的,实际使用中可以通过查询数据库方式实现:

java 复制代码
@Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 1. 根据用户名从数据库查询用户信息
    AuthUsers user = authUsersService.lambdaQuery()
        .eq(AuthUsers::getUsername, username)
        .one();

    // 2. 校验用户是否存在
    if (user == null) {
      log.warn("认证失败:用户 '{}' 不存在。", username);
      throw new UsernameNotFoundException("用户不存在: " + username);
    }

    // 3. 查用户账户是否被禁用
    if (user.getIsDelete()) {
      log.warn("认证失败:用户 '{}' 已被禁用。", username);
      // 在Spring Security中,更标准的做法是抛出 `DisabledException`,但为了保持与原逻辑一致,这里仍使用UsernameNotFoundException
      throw new UsernameNotFoundException("用户已禁用: " + username);
    }

    // 4. 获取用户的权限列表
    List<GrantedAuthority> authorities = getUserAuthorities(user.getId());

    // 5. 构建Spring Security的UserDetails对象
    //    这个对象封装了用户名、密码、权限以及账户状态。
    return User.withUsername(user.getUsername())
        .password(user.getPasswordHash()) // 数据库中存储的已加密密码
        .authorities(authorities)          // 用户的权限集合
        .accountExpired(false)             // 账户是否过期
        .accountLocked(false)              // 账户是否被锁定
        .credentialsExpired(false)         // 凭证(密码)是否过期
        .disabled(false)                   // 账户是否被禁用
        .build();
  }

  /**
   * 根据用户ID获取其对应的所有权限(角色)。
   *
   * @param userId 用户ID
   * @return 用户的权限列表
   */
  private List<GrantedAuthority> getUserAuthorities(Long userId) {
    // 1. 根据用户ID查询其拥有的所有角色ID
    List<Long> roleIds = authUserRolesService.lambdaQuery()
        .eq(AuthUserRoles::getUserId, userId)
        .select(AuthUserRoles::getRoleId)
        .list()
        .stream()
        .map(AuthUserRoles::getRoleId)
        .collect(Collectors.toList());

    // 2. 如果用户未分配任何角色,记录日志并返回一个空的权限列表
    if (CollectionUtils.isEmpty(roleIds)) {
      log.info("用户ID '{}' 未分配任何角色。", userId);
      return List.of(); // 返回一个不可变的空列表
    }

    // 3. 根据角色ID列表,查询所有对应的角色信息
    List<AuthRoles> roles = authRolesService.lambdaQuery()
        .in(AuthRoles::getId, roleIds)
        .list();

    // 4. 将角色名称转换为Spring Security的GrantedAuthority对象
    return roles.stream()
        .map(role -> new SimpleGrantedAuthority(role.getName()))
        .collect(Collectors.toList());
  }

5. 在接口上设置权限

使用注解@PreAuthorize("hasRole('')")设置访问接口所需要的角色

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
  @Resource
  public UserService userService;

  /**
   * 获取用户列表.
   *
   * @return
   */
  @PreAuthorize("hasRole('ADMIN')")
  @GetMapping("/list")
  public List<User> getList() {
    return userService.list();
  }

  /**
   * 添加用户.
   *
   * @param user 用户信息
   * @return
   */
  @PreAuthorize("hasRole('USER')")
  @PostMapping("/add")
  public void add(@RequestBody User user) {
    userService.saveUserDetails(user);
  }

}

也可以使用注解@PreAuthorize("hasAuthority('')")设置访问接口需要的权限

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
  @Resource
  public UserService userService;

  /**
   * 获取用户列表.
   *
   * @return
   */
  @PreAuthorize("hasAuthority('USER_LIST')")
  @GetMapping("/list")
  public List<User> getList() {
    return userService.list();
  }

  /**
   * 添加用户.
   *
   * @param user 用户信息
   * @return
   */
  @PreAuthorize("hasAuthority('USER_ADD')")
  @PostMapping("/add")
  public void add(@RequestBody User user) {
    userService.saveUserDetails(user);
  }

}

更多详情可查看:https://github.com/TimberWill/security-demo

以上为个人学习分享,如有问题,欢迎指出:)

相关推荐
Renhao-Wan2 小时前
Java 算法实践(四):链表核心题型
java·数据结构·算法·链表
_codemonster3 小时前
JavaWeb开发系列(六)JSP基础
java·开发语言
万邦科技Lafite4 小时前
淘宝店铺所有商品API接口实战指南
java·数据库·mysql
jjjxxxhhh1234 小时前
【加密】-AES与对称加密
java·服务器·网络
临水逸4 小时前
飞牛fnos 2025 漏洞Java跨域URL浏览器
java·开发语言·安全·web安全
yaoxin5211234 小时前
324. Java Stream API - 实现 Collector 接口:自定义你的流式收集器
java·windows·python
H Corey4 小时前
数据结构与算法:高效编程的核心
java·开发语言·数据结构·算法
米羊1214 小时前
Struts 2 漏洞(上)
java·后端·struts
galaxyffang5 小时前
Java堆内存诊断:从工具使用到实战分析
java·jvm