【Spring Security】打造安全无忧的Web应用--使用篇

🥳🥳Welcome Huihui's Code World ! !🥳🥳

接下来看看由辉辉所写的关于Spring Security的相关操作吧 

目录

🥳🥳Welcome Huihui's Code World ! !🥳🥳

一.Spring Security中的授权是什么

二. 基于单体项目的授权

1.修改User配置角色和权限

2.修改SpringSecurity配置类

3.测试

三.基于前后端分离项目的授权 

JsonResponseBody

JsonResponseStatus

修改SpringSecurity配置类


在正式讲解今天的内容之前,我们还需要了解一个东西--RBAC数据模型,RBAC(Role-Based Access Control,基于角色的访问控制)数据模型是一种访问控制模型,它使用角色作为访问控制的基本单元。在这个模型中,用户被分配到不同的角色,每个角色代表着一组操作或任务。系统管理员将访问权限授予角色,而不是直接授予给用户。当用户需要执行某个操作时,系统可以检查该用户是否拥有执行此操作所需的角色,并根据角色的权限来授权或拒绝访问。RBAC 数据模型可以有效地管理复杂的访问控制策略,提高系统安全性和管理效率。

遇到复杂的权限管理,都是配置在数据库中的,一般由五张表组成,即RBAC。

  1)RBAC数据模型的五张表

          用户表,存储用户信息,由业务人员维护;

          角色表,存储角色信息,由业务人员维护 ;

          资源表,存储资源信息(菜单、按钮及其URL),由开发人员维护;

          用户-角色关系表,存储用户和角色的对应关系,多对多,由业务人员维护;

          角色-资源关系表,存储角色和资源的对应关系 ,多对多,由业务人员维护;

一.Spring Security中的授权是什么

        在Spring Security中,授权是指系统根据用户的角色和权限决定用户是否有权访问特定的资源或执行特定的操作。

        Spring Security提供了多种授权机制,包括基于角色的授权和基于权限的授权

                基于角色的授权是指系统将用户分配到不同的角色,每个角色具有不同的权限,然后通过判断用户是否具有特定的角色来进行授权。例如,系统可以定义"ADMIN"角色和"USER"角色,管理员拥有更高的权限,可以访问和操作更多的资源,而普通用户只能访问受限资源。

                基于权限的授权是指系统将具体的权限授予用户,用户可以直接拥有某个权限,而不需要通过角色间接获得。例如,系统可以定义"READ"、"WRITE"、"DELETE"等权限,根据用户所拥有的权限判断其是否有权对资源进行读取、写入或删除操作。

        Spring Security还支持细粒度的授权控制,可以通过注解、表达式或配置文件等方式来定义授权规则。可以使用@PreAuthorize@PostAuthorize@Secured等注解来标注方法或类,以声明访问资源的权限要求。

        通过授权机制,Spring Security可以保护应用程序免受未经授权的访问,确保只有具备合适权限的用户才能访问敏感资源,提高应用程序的安全性

二. 基于单体项目的授权

1.修改User配置角色和权限

这里也可以使用多表联查的方式去做,不过考虑到在数据量过于庞大的情况下,我们常常会进行分库分表,所以我们这里采用的是流的形式来编写的

package com.wh.security.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wh.security.model.*;
import com.wh.security.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author是辉辉啦
 * @create 2023-12-23-14:21
 */
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private IUserService userService;
    @Autowired
    private IUserRoleService userRoleService;
    @Autowired
    private IRoleService roleService;
    @Autowired
    private IRoleModuleService roleModuleService;
    @Autowired
    private IModuleService moduleService;
    /**
     * 实现Spring Security内置的UserDetailService接口,重写loadUserByUsername方法实现数据库的身份校验
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询数据库中用户信息
        User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
        //判断用户是否存在
        if(Objects.isNull(user)) {
            throw new UsernameNotFoundException("该用户不存在!!!");
        }
        //先拿到用户的身份id
        List<Integer> roleIds = userRoleService.list(new QueryWrapper<UserRole>().eq("user_id", user.getId()))
                .stream().map(UserRole::getRoleId)
                .collect(Collectors.toList());
        //根据身份id再拿到身份所对应的角色名称roleName
        List<String> roleName = roleService.list(new QueryWrapper<Role>().in("role_id", roleIds))
                .stream().map(Role::getRoleName)
                .collect(Collectors.toList());
        //然后通过身份id再拿到身份和权限的中间表的id
        List<Integer> moduleIds = roleModuleService.list(new QueryWrapper<RoleModule>().in("role_id", roleIds))
                .stream().map(RoleModule::getModuleId)
                .collect(Collectors.toList());
        //再通过这个中间表的id拿到对应的可访问的模块的url
        List<String> moduleUrls = moduleService.list(new QueryWrapper<Module>().in("id", moduleIds))
                .stream().map(Module::getUrl)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        //把roleName和moduleUrls整合在一起
        roleName.addAll(moduleUrls);
        //把拿到的角色的名称以及角色可访问到的url赋值给user中的哪个管理角色权限的字段
        // roles [管理员,普通用户,book:manager:add,book:manager:list]
        List<SimpleGrantedAuthority> authorities = roleName.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        user.setAuthorities(authorities);
        return user;
    }
}

2.修改SpringSecurity配置类

加一个注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

@EnableGlobalMethodSecurity是Spring Security提供的一个注解,用于启用方法级别的安全性。它可以在任何@Configuration类上使用,以启用Spring Security的方法级别的安全性功能。它接受一个或多个参数,用于指定要使用的安全注解类型和其他选项。以下是一些常用的参数:

  • prePostEnabled:如果设置为true,则启用@PreAuthorize@PostAuthorize注解。默认值为false

  • securedEnabled:如果设置为true,则启用@Secured注解。默认值为false

  • jsr250Enabled:如果设置为true,则启用@RolesAllowed注解。默认值为false

  • proxyTargetClass:如果设置为true,则使用CGLIB代理而不是标准的JDK动态代理。默认值为false

使用@EnableGlobalMethodSecurity注解后,可以在应用程序中使用Spring Security提供的各种注解来保护方法,例如@Secured@PreAuthorize@PostAuthorize@RolesAllowed。这些注解允许您在方法级别上定义安全规则,以控制哪些用户可以访问哪些方法。

注解介绍:

注解 说明
@PreAuthorize 用于在方法执行之前对访问进行权限验证
@PostAuthorize 用于在方法执行之后对返回结果进行权限验证
@Secured 用于在方法执行之前对访问进行权限验证
@RolesAllowed 是Java标准的注解之一,用于在方法执行之前对访问进行权限验证

3.测试

使用管理员角色登录

使用普通角色登录

如果说没有权限或者是退出登录等其他操作,都会是跳到指定的页面

三.基于前后端分离项目的授权 

关于前面的配置都是和上面所说的一样,这里就不过多阐述了,但是前后端分离的项目就是不能够直接跳转页面了,而是要返回响应的内容。所以我们需要写一个专门的响应类

JsonResponseBody

package com.wh.security.resp;

import lombok.Data;

@Data
public class JsonResponseBody<T> {

    private Integer code;
    private String msg;
    private T data;
    private Long total;

    private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data) {
        this.code = jsonResponseStatus.getCode();
        this.msg = jsonResponseStatus.getMsg();
        this.data = data;
    }

    private JsonResponseBody(JsonResponseStatus jsonResponseStatus, T data, Long total) {
        this.code = jsonResponseStatus.getCode();
        this.msg = jsonResponseStatus.getMsg();
        this.data = data;
        this.total = total;
    }

    public static <T> JsonResponseBody<T> success() {
        return new JsonResponseBody<T>(JsonResponseStatus.OK, null);
    }

    public static <T> JsonResponseBody<T> success(T data) {
        return new JsonResponseBody<T>(JsonResponseStatus.OK, data);
    }

    public static <T> JsonResponseBody<T> success(T data, Long total) {
        return new JsonResponseBody<T>(JsonResponseStatus.OK, data, total);
    }

    public static <T> JsonResponseBody<T> unknown() {
        return new JsonResponseBody<T>(JsonResponseStatus.UN_KNOWN, null);
    }

    public static <T> JsonResponseBody<T> other(JsonResponseStatus jsonResponseStatus) {
        return new JsonResponseBody<T>(jsonResponseStatus, null);
    }

}

JsonResponseStatus

package com.wh.security.resp;

import lombok.Getter;

@Getter
public enum JsonResponseStatus {

    OK(200, "OK"),
    UN_KNOWN(500, "未知错误"),
    RESULT_EMPTY(1000, "查询结果为空"),
    NO_ACCESS(3001, "没有权限"),
    NO_LOGIN(4001, "没有登录"),
    LOGIN_FAILURE(5001, "登录失败"),
    ;

    private final Integer code;
    private final String msg;

    JsonResponseStatus(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

}

并且需要在配置类中将登录成功或者权限认证失败等这些需要跳转页面的操作全部都修改掉

修改SpringSecurity配置类

package com.wh.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wh.security.resp.JsonResponseBody;
import com.wh.security.resp.JsonResponseStatus;
import com.wh.security.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.annotation.Resource;
import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    // 自动装配自定义的用户服务实现类
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    // 自动装配数据源
    @Resource
    public DataSource dataSource;
    // 自动装配对象映射器
    @Autowired
    private ObjectMapper objectMapper;
    // 自动装配自定义的登录失败处理器
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    // 创建密码编码器实例
    @Bean
    public PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 创建模拟用户数据的服务实现类
    // @Autowired
    // private UserDetailsService userDetailsService;

    // 获取认证管理器(认证管理器),登录时认证使用(基于数据库方式)
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        // 创建DaoAuthenticationProvider实例
        DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
        // 设置userDetailsService,基于数据库方式进行身份认证
        provider.setUserDetailsService(myUserDetailsService);
        // 配置密码编码器
        provider.setPasswordEncoder(bcryptPasswordEncoder());
        return new ProviderManager(provider);
    }

    // 配置持久化Token方式,注意tokenRepository.setCreateTableOnStartup()配置
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 设置为true要保障数据库该表不存在,不然会报异常哦
        // 所以第二次打开服务器应用程序的时候得把它设为false
        tokenRepository.setCreateTableOnStartup(false);
        return tokenRepository;
    }

    // 配置安全过滤器链(SecurityFilterChain)
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/noAccess", "/toLogin").permitAll()
                //所有请求全部需要登录
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                //设置处理登录请求的接口
                .loginProcessingUrl("/userLogin")
                .usernameParameter("username")
                .passwordParameter("password")
                //登录成功
                .successHandler((req, resp, auth) -> {
                    Object user = auth.getPrincipal();
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.success(user));
                })
                //登录失败
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                //权限不足
                .accessDeniedHandler((req, resp, ex) -> {
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_ACCESS));
                })
                //没有认证
                .authenticationEntryPoint((req, resp, ex) -> {
                    objectMapper
                            .writeValue(resp.getOutputStream(), JsonResponseBody.other(JsonResponseStatus.NO_LOGIN));
                })
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/");
        http.csrf().disable(); //禁用CSRF保护
        return http.build();
    }
}

这样就不会跳页面了,而是返回出对应的数据

好啦,今天的分享就到这了,希望能够帮到你呢!😊😊   

最近更新

  1. TCP协议是安全的吗?

    2023-12-24 16:04:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-24 16:04:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-24 16:04:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-24 16:04:02       20 阅读

热门阅读

  1. 聊聊equals()方法

    2023-12-24 16:04:02       40 阅读
  2. VSC(Visual Studio Code)好用插件推荐

    2023-12-24 16:04:02       39 阅读
  3. 62 贪心算法按要求补齐数组

    2023-12-24 16:04:02       36 阅读
  4. C++函数默认值的用法

    2023-12-24 16:04:02       36 阅读
  5. 蝴蝶算法优化 Matlab 实现

    2023-12-24 16:04:02       41 阅读
  6. 用Python实现乒乓球游戏

    2023-12-24 16:04:02       37 阅读
  7. vivado I/O延迟约束

    2023-12-24 16:04:02       39 阅读
  8. android 13.0 Launcher3定制folder文件夹16宫格实现二

    2023-12-24 16:04:02       33 阅读