Spring Security 实现权限控制(认证 + 授权全流程)

1.1 认证和授权概念

在实际生产环境中,系统资源不能随意访问,必须先确认用户身份,再分配操作权限,这就是认证授权要解决的问题。

  • 认证:识别用户身份,验证用户名 / 密码、手机号验证码等,让系统知道 "你是谁"。
  • 授权:认证通过后,指定用户可操作的功能、可访问的资源,让系统知道 "你能做什么"。

权限控制本质就是对用户完成认证 + 授权的全流程管理。


1.2 权限模块数据模型

实现权限控制需要5 / 7 张核心表支撑,角色表 t_role 处于核心位置,用户、权限、菜单均与角色为多对多关系。

涉及表结构

  • 用户表 t_user
  • 权限表 t_permission
  • 角色表 t_role
  • 菜单表 t_menu
  • 用户角色关系表 t_user_role
  • 角色权限关系表 t_role_permission
  • 角色菜单关系表 t_role_menu

表使用场景

  • 认证:仅需用户表 t_user,校验用户名 / 密码即可。
  • 授权:需 7 张表联动,根据用户→角色→菜单 / 权限,确定用户可访问资源与操作权限。

1.3 Spring Security 简介

Spring Security 是 Spring 官方提供的强大、高度可定制的认证与授权框架,是 Spring 项目安全管控的事实标准。

核心优势

  • 完整支持认证、授权
  • 防护会话固定、点击劫持、CSRF 攻击
  • 与 Servlet API、Spring Web MVC 无缝集成
  • 易扩展,满足自定义安全需求

Maven 依赖

xml

复制代码
<!--security启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

常用权限框架:Spring Security、Apache Shiro。


1.4 Spring Security 入门案例

1.4.1 工程搭建

xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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">
    <parent>
        <artifactId>ICan_parent</artifactId>
        <groupId>com.hg</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring_security</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
</project>

1.4.2 创建启动类

java

java 复制代码
@SpringBootApplication
@EnableWebSecurity
public class SpringSecurityApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext ac = SpringApplication.run(SpringSecurityApp.class, args);
    }
}

1.4.3 启动测试

访问 http://localhost:8080,Spring Security 自动生成默认登录页面,需输入账号密码登录。

1.4.5 FilterChainProxy 核心过滤器

Spring Boot 启动时,会加载名为 springSecurityFilterChain 的过滤器 FilterChainProxy,所有请求先经过此过滤器,再分发到对应子过滤器处理。

复制代码
@SpringBootApplication
@EnableWebSecurity//启动security
public class SpringSecurityApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext ac = SpringApplication.run(SpringSecurityApp.class, args);
        Object bean = ac.getBean("springSecurityFilterChain");
        //输出class org.springframework.security.web.FilterChainProxy
        System.out.println(bean.getClass());
    }
}

点击进入FilterChainProxy的源码,执行过滤器时会调用这个类的doFiler方法:

再进入doFilterInternal方法里面打断点,观察它的具体的初始化流程:

1.4.6 Spring Security 15 个常用过滤器

  1. SecurityContextPersistenceFilter:初始化安全上下文,保存认证权限信息
  2. WebAsyncManagerIntegrationFilter:集成 Spring 异步机制
  3. HeaderWriterFilter:添加请求头安全信息
  4. CsrfFilter:防跨域请求伪造攻击
  5. LogoutFilter:处理退出登录,清除认证信息
  6. UsernamePasswordAuthenticationFilter:用户名密码认证核心过滤器
  7. DefaultLoginPageGeneratingFilter:生成默认登录页
  8. DefaultLogoutPageGeneratingFilter:生成默认退出页
  9. BasicAuthenticationFilter:解析 Basic 认证请求头
  10. RequestCacheAwareFilter:缓存请求对象
  11. SecurityContextHolderAwareRequestFilter:包装 Request,扩展 API
  12. AnonymousAuthenticationFilter:未登录时创建匿名身份
  13. SessionManagementFilter:限制同一用户会话数量
  14. ExceptionTranslationFilter:统一处理安全异常
  15. FilterSecurityInterceptor:权限校验核心过滤器

1.5 入门案例改进(适配生产环境)

原生入门案例存在 4 个问题:所有资源都需认证、需要自定义的登录页、明文配置账号、密码明文存储,需针对性优化。

1.5.1 配置可匿名访问资源

java 复制代码
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    //配置认证信息来源
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    //忽略静态资源,匿名访问
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/pages/**");
    }

    //HTTP请求安全配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

效果:pages目录下的文件可以在没有认证的情况下任意访问

1.5.2 使用自定义登录页面

1.自定义 login.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <h3>自定义登录页面</h3>
    <form action="/login" method="post">
        username:<input type="text" name="username"><br>
        password:<input type="password" name="password"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

​

2.安全配置优化

java 复制代码
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/login.html");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .defaultSuccessUrl("/pages/index.html",true); //登录成功,总是返回的页面
 //权限配置
    http.authorizeRequests()
        .anyRequest().authenticated();

    //关闭CSRF防护
    http.csrf().disable();
    }
}

效果: 使用http://localhost:8080/访问,此时就能访问自定义的登录页面。


1.6 从数据库查询用户信息

生产环境需从数据库动态加载用户,需实现 UserDetailsService 接口,框架自动调用完成认证。

java 复制代码
@Service
public class UserServiceImpl implements UserDetailsService {
    //模拟数据库用户数据
    private static Map<String, UserInfo> mapSql = new HashMap<>();
    static {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        mapSql.put("admin",new UserInfo("admin", passwordEncoder.encode("111")));
        mapSql.put("test",new UserInfo("test",passwordEncoder.encode("222")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = mapSql.get(username);
        if (userInfo == null){
            return null;
        }
        //明文密码{noop}
        String password = "{noop}"+userInfo.getPassword();
        //权限校验码
        List<GrantedAuthority> list = new ArrayList<>();
        list.add(new SimpleGrantedAuthority("add"));
        list.add(new SimpleGrantedAuthority("delete"));
        list.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        return new User(username, password, authorityArrayList);
    }
}

UserInfo 用户实体类:

java 复制代码
//用户实体
public class UserInfo {
    String username;
    String password;

    public UserInfo(String username,String password){
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

配置认证管理器WebSecurityConfig.class :

复制代码
@Autowired
private UserService userService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}

1.7 密码加密(BCrypt)

明文密码不安全,采用 BCryptPasswordEncoder 加密,随机盐混入密文,匹配无需单独存盐。

加密特点 :同一密码每次加密结果不同,matches() 方法可正确匹配。

1.配置加密 Bean

WebSecurityConfig.class

复制代码
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}

2.WebSecurityConfig.class 中指定密码加密对象

3.修改UserService实现类

java 复制代码
@Service
public class UserServiceImpl implements UserDetailsService {
    //模拟向mysql数据库中插入数据
    private static Map<String, UserInfo> mapSql = new HashMap<>();
    static {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        mapSql.put("admin",new UserInfo("admin", passwordEncoder.encode("111")));
        mapSql.put("test",new UserInfo("test",passwordEncoder.encode("222")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //模拟从数据库中查询用户
        UserInfo userInfo = mapSql.get(username);
        if (userInfo == null){
            return null;
        }
        //模拟查询数据库中用户的密码  去掉明文标识{noop}
        String password = userInfo.getPassword();
        //权限校验码
        List<GrantedAuthority> authorityArrayList = new ArrayList<>();
        authorityArrayList.add(new SimpleGrantedAuthority("add"));
        authorityArrayList.add(new SimpleGrantedAuthority("delete"));
        authorityArrayList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        return new User(username, password, authorityArrayList);
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

​

1.8 配置多种权限校验规则

按权限 / 角色细粒度控制页面访问:

为了测试方便,首先在项目中创建a.html、b.html、c.html几个页面

WebSecurityConfig.class:

java 复制代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义登录页
        http.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .defaultSuccessUrl("/pages/index.html",true); //登录成功,总是返回的页面
        //权限配置
        http.authorizeRequests()
                //a.html页面需add权限访问
                .mvcMatchers("/pages/a.html").hasAuthority("add")
                //b.html页面需delete权限访问
                .mvcMatchers("/pages/b.html").hasAuthority("delete")
                //c.html需ADMIN角色访问
                .mvcMatchers("/pages/c.html").hasRole("ADMIN")
                .anyRequest().authenticated();  //任意请求必须认证过的
        //关闭跨站请求防护
        http.csrf().disable();
    }

为方便测试,改造UserServiceImpl

java 复制代码
@Service
public class UserServiceImpl implements UserDetailsService {
    private static Map<String, UserInfo> mapSql = new HashMap<>();
    static {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        mapSql.put("admin",new UserInfo("admin", passwordEncoder.encode("111")));
        mapSql.put("test",new UserInfo("test",passwordEncoder.encode("222")));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = mapSql.get(username);
        if (userInfo == null){
            return null;
        }
        //密码
        String password = userInfo.getPassword();
        //权限校验码
        List<GrantedAuthority> authorityArrayList = new ArrayList<>();
        if ("admin".equals(username)) {
            authorityArrayList.add(new SimpleGrantedAuthority("add"));
        }
        if ("test".equals(username)) {
            authorityArrayList.add(new SimpleGrantedAuthority("delete"));
        }
        if ("admin".equals(username)) {
            authorityArrayList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
        return new User(username, password, authorityArrayList);
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

此时分别用admin用户和test用户登录测试效果。


1.9 注解方式权限控制

更灵活的方法级权限控制,开启注解后直接在接口上标注。

1.开启注解支持

java 复制代码
@Component
@EnableGlobalMethodSecurity(prePostEnabled = true)  //开启权限注解支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
}

2.Controller类权限注解

java 复制代码
@RestController
@RequestMapping("/hello")
public class HelloController {
    @RequestMapping("/add")
    @PreAuthorize("hasAuthority('add')")//表示用户必须拥有add权限才能调用当前方法
    public String add(){
        System.out.println("add...");
        return "success";
    }

    @RequestMapping("/delete")
    @PreAuthorize("hasAuthority('delete')")//表示用户必须拥有delete权限 才能调用当前方法
    public String delete(){
        System.out.println("delete.....");
        return "success";
    }
    @RequestMapping("/admin")
    @PreAuthorize("hasRole('ROLE_ADMIN')")//表示用户必须拥有ROLE_ADMIN角色 才能调用当前方法
    public String admin(){
        System.out.println("admin.....");
        return "success";
    }
}

1.10 退出登录配置

请求 /logout 自动退出,清除认证状态,跳转至登录页。

复制代码
@Override
protected void configure(HttpSecurity http) throws Exception {
    //原有配置...
    //退出登录配置
    http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html");
}

总结

本文基于 Spring Security 完整实现了认证 + 授权权限控制:

  1. 理清认证、授权核心概念与 7 张表数据模型
  2. 快速搭建入门案例,掌握 FilterChainProxy 过滤器链
  3. 适配生产环境:匿名资源、自定义登录页、数据库查用户、BCrypt 加密
  4. 细粒度权限控制:URL 配置 + 方法注解两种方式
  5. 完成登录、退出全流程配置

可直接基于此方案,对接移动端短信登录、后台管理系统权限管控。

相关推荐
weixin_408099672 小时前
【完整教程】天诺脚本如何调用 OCR 文字识别 API?自动识别屏幕文字实战(附代码)
前端·人工智能·后端·ocr·api·天诺脚本·自动识别文字脚本
金銀銅鐵2 小时前
[Java] 如何通过 cglib 的 FastClass 调用一个类中的“任意”方法?
java·后端
阿维的博客日记2 小时前
为什么会增加TreeMap和TreeSet这两类,有什么核心优势吗?可以解决什么核心痛点?
java·treeset·treemap
dllxhcjla2 小时前
黑马头条1
java
宠友信息2 小时前
一套基于uniapp+springboot完整社区系统是如何实现的?友猫社区源码级功能解析
java·spring boot·后端·微服务·微信·uni-app
humors2212 小时前
各厂商工具包网址
java·数据库·python·华为·sdk·苹果·工具包
无限进步_3 小时前
【C++&string】大数相乘算法详解:从字符串加法到乘法实现
java·开发语言·c++·git·算法·github·visual studio
海兰3 小时前
使用 Spring AI 打造企业级 RAG 知识库第二部分:AI 实战
java·人工智能·spring
難釋懷3 小时前
缓存同步
spring·缓存·mybatis