一、权限控制

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

@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 配置可匿名访问资源

@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

<!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.安全配置优化

@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 接口,框架自动调用完成认证。

@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 用户实体类:

//用户实体
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实现类

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

    @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

@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.开启注解支持

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

2.Controller类权限注解

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

二、权限控制实战

权限控制核心是认证(登录)与授权(权限校验),本项目基于7 张权限表(用户、角色、权限、菜单及关联表)实现数据支撑。

1.1 导入 Spring Security 依赖

ICan_backend工程的pom.xml添加启动器:

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

1.2 实现认证逻辑(UserDetailsService)

自定义用户认证服务,查询用户、角色、权限信息并封装为UserDetails

java

@Component
public class SpringSecurityUserService implements UserDetailsService {

    @Reference
    UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findUserByUsername(username);
        if(user==null){
            return null;
        }
        List<GrantedAuthority> list = new ArrayList<>();
        Set<Role> roles = user.getRoles();
        for(Role role:roles){
            // 添加角色权限
            list.add(new SimpleGrantedAuthority(role.getKeyword()));
            Set<Permission> permissions = role.getPermissions();
            for(Permission permission:permissions){
                // 添加资源权限
                list.add(new SimpleGrantedAuthority(permission.getKeyword()));
            }
        }
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username,user.getPassword(),list);
        return userDetails;
    }
}

1.3 业务层与数据层实现

1.3.1 UserService 接口

java

public interface UserService {
    User findUserByUsername(String username);
}

1.3.2 UserServiceImpl 实现类

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserDao userDao;
    @Autowired
    RoleDao roleDao;
    @Autowired
    PermissionDao permissionDao;

    @Override
    public User findUserByUsername(String username) {
        User user = userDao.findByUsername(username);
        if(user==null){
            return null;
        }
        Integer userId = user.getId();
        Set<Role> roles = roleDao.findByUserId(userId);
        if(roles!=null||roles.size()>0){
            for(Role role:roles){
                Integer roleId = role.getId();
                Set<Permission> permissions = permissionDao.findByRoleId(roleId);
                if(permissions!=null||permissions.size()>0){
                   role.setPermissions(permissions);
                }
            }
            user.setRoles(roles);
        }
        return user;
    }
}

1.3.3 Dao 层与 Mapper 文件

UserDao

public interface UserDao {
     User findByUsername(String username);
}

UserDao.xml

<mapper namespace="com.hg.dao.UserDao">
    <select id="findByUsername" parameterType="String" resultType="com.hg.pojo.User">
          select * from t_user where username = #{username}
    </select>
</mapper>

RoleDao

public interface RoleDao {
    Set<Role> findByUserId(int id);
}

RoleDao.xml

xml

<mapper namespace="com.hg.dao.RoleDao">
    <select id="findByUserId" parameterType="int" resultType="com.hg.pojo.Role">
        SELECT *  FROM t_role WHERE  id  IN(
              SELECT role_id  FROM t_user_role WHERE user_id=#{userId})
    </select>
</mapper>

PermissionDao

public interface PermissionDao {
    Set<Permission> findByRoleId(int roleId);
}

PermissionDao.xml

<mapper namespace="com.hg.dao.PermissionDao">
    <select id="findByRoleId" parameterType="int" resultType="com.hg.pojo.Permission">
        SELECT *  FROM t_permission WHERE id IN(
              SELECT  permission_id FROM t_role_permission WHERE role_id=#{roleId}
    )
    </select>
</mapper>

1.4 Spring Security 配置类

创建配置类,完成认证、授权、登录、登出配置:

java

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

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 自定义用户认证 + 密码加密
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域
        http.cors();
        // 关闭CSRF防护
        http.csrf().disable();
        // 登录配置
        http.formLogin().loginProcessingUrl("/login")
                .successHandler((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    Object user = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                    response.getWriter().write(new ObjectMapper().writeValueAsString(new Result(true, "登陆成功", user)));
                })
                .failureHandler((request, response, e) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(new Result(false, "登陆失败")));
                });
        // 所有请求都需要认证
        http.authorizeRequests().anyRequest().authenticated();
        // 登出配置
        http.logout().logoutUrl("/logout")
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(new Result(true, "登出成功")));
                })
                .invalidateHttpSession(true);
    }

    // 密码加密器BCrypt
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

1.5 接口权限控制(@PreAuthorize)

在 Controller 方法上添加权限注解,示例(CheckItemController):

java

// 新增检查项
@RequestMapping("/add")
@PreAuthorize("hasAuthority('CHECKITEM_ADD')")
public Result add(@RequestBody CheckItem checkItem){
    try{
        checkItemService.addCheckItem(checkItem);
    }catch(Exception e){
        e.printStackTrace();
        return new Result(false, MessageConstant.ADD_CHECKITEM_FAIL);
    }
    return new Result(true, MessageConstant.ADD_CHECKITEM_SUCCESS);
}

// 分页查询
@RequestMapping("/findPage")
@PreAuthorize("hasAuthority('CHECKITEM_QUERY')")
public PageResult findPage(@RequestBody QueryPageBean queryPageBean){
    PageResult pageResult=checkItemService.findPage(queryPageBean);
    return pageResult;
}

// 删除检查项
@RequestMapping("/delete")
@PreAuthorize("hasAuthority('CHECKITEM_DELETE')")
public Result delete(Integer id){
    try{
        checkItemService.deleteCheckItemById(id);
    }catch (Exception e){
        e.printStackTrace();
        return new Result(false, MessageConstant.DELETE_CHECKITEM_FAIL);
    }
    return new Result(true, MessageConstant.DELETE_CHECKITEM_SUCCESS);
}

// 修改检查项
@RequestMapping("/edit")
@PreAuthorize("hasAuthority('CHECKITEM_EDIT')")
public Result edit(@RequestBody CheckItem checkItem){
    try{
        checkItemService.updateCheckItem(checkItem) ;
    }catch(Exception e){
        e.printStackTrace();
        return new Result(false, MessageConstant.EDIT_CHECKITEM_FAIL);
    }
    return new Result(true, MessageConstant.EDIT_CHECKITEM_SUCCESS);
}

1.6 前端权限不足提示

捕获 403 异常,提示无权限:

javascript

// 权限不足提示
showMessage(r){
    if(r == 'Error: Request failed with status code 403'){
        this.$message.error('无访问权限');
        return;
    }else{
        this.$message.error('未知错误');
        return;
    }
},
// 删除操作
handleDelete(row) {
    this.$confirm("你确定要删除当前数据吗?","提示",{
        type:'warning'
    }).then(()=>{
        axios.get("/checkitem/delete.do?id=" + row.id).then((res) => {
            if(res.data.flag){
                this.$message({
                    type:'success',
                    message:res.data.message
                });
                this.findPage();
            }else{
                this.$message.error(res.data.message);
            }
        }).catch((r)=>{
            this.showMessage(r);
        });
    }).catch(()=>{
        this.$message({
            type:'info',
            message:'操作已取消'
        });
    });
}

1.7 用户登出功能

1.页面添加登出链接:

html

<el-dropdown-item divided>
    <span style="display:block;"><a href="/logout.do">退出</a></span>
</el-dropdown-item>

2.Security 配置简化版登出(可选):

java

http.logout()
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login.html")
    .invalidateHttpSession(true);

可直接基于此方案,对接后台管理系统权限管控。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐