一、Spring Security简介

Spring Security是一个功能强大且高度可定制的身份验证访问控制框架。Spring Security致力于为Java应用程序提供身份验证和授权的能力。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DDJjGp6V-1645512921586)(image\角色和权限时许.jpg)]

Spring Security两大重要核心功能:用户认证(Authentication)和用户授权(Authorization)

  • 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
  • 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

二、快速开始

使用Springboot工程搭建Spring Security项目。

1.引入依赖
<?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 https://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>2.5.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qf</groupId>
    <artifactId>spring-security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-security-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <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>
    </dependencies>

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

在pom中新增了Spring Security的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.创建测试访问接口

用于访问接口时触发Spring Security登陆页面

@RestController
public class SecurityController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello security!";
    }

}
3.Security登陆页面

访问hello接口,将自动跳转至Security的登陆页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3H59z5h1-1645512921587)(image\login.png)]

默认账号是: user

默认密码是:启动项目的控制台中输出的密码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0lVPfdSR-1645512921587)(image\log.png)]

三、原理剖析

访问hello接口,发现被Spring Security的登陆页面拦截,可以猜到这是触发了Security框架的过滤器。Spring Security本质上就是一个过滤器链。下面讲介绍Security框架的过滤器链。

1.过滤器链

几个主要的过滤器

  • FilterSecurityInterceptor:是一个方法级的权限过滤器,位于过滤器链的最底部。
  • ExceptionTranslationFilter: 异常过滤器,用来处理在认证授权过程中抛出异常。
  • UsernamePasswordAuthenticationFilter: 用于对/login的POST请求做拦截,校验表单中的用户名和密码。
2.过滤器加载过程

Springboot在整合Spring Security项目时会自动配置DelegatingFilterProxy过滤器,若非Springboot工程,则需要手动配置该过滤器。

过滤器如何进行加载的?

Security在DelegatingFilterProxydoFilter()调用了initDelegat()方法,在该方法中调用了WebApplicationContextgetBean()方法,该方法触发FilterChainProxydoFilterInternal方法,用于获取过滤链中的所有过滤器并进行加载。

SpringSecurity过滤器链

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pq0Ah3t7-1645512922139)(image\image-20210909100026482.png)]

3.Security的两个关键接口

在快速开始中发现Spring Security使用了默认的用户名和密码,实际用户名和密码需要自定义,因此会用到以下两个接口。下述两个接口的具体实现将在之后的例子中体现。

3.1 UserDetailsService接口

若需要从数据库中获取用户名和密码,则需要把查询数据库的过程写在这个接口里。完成自定义登录逻辑

3.2 PasswordEncoder接口

在密码的处理上,需要进行编解码器,该接口实现对密码进行加密。

BCryptPasswordEncoder是PasswordEncoder最常用的实现类,是官方推荐使用的

@Test
public void testBCryptPasswordEncoder() {
    //注册的时候对密码进行加密
    BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
    String encode = bcrypt.encode("123456");
    System.out.println(encode);

    //登录验证的时候对密码进行解密(参数1:用户输入的密码 参数2:加密过的密码)
    //这个过程有SpringSecurity完成
    boolean matches = bcrypt.matches("123456", encode);
    System.out.println(matches);
}

四、SpringSecurity认证功能

4.1 自定义用户名和密码

在application.yml文件中配置用户名和密码

# 设置登陆的用户名和密码
spring:
  security:
    user:
      name: admin
      password: 123456

由于Springboot版本的原因,在当前版本下,必须要注入PasswordEncoder对象

//在引导类中添加PasswordEncoder对象

@Bean
public PasswordEncoder getPasswordEncode(){
    return new BCryptPasswordEncoder();
}
4.2 基于内存定义用户名密码

自定义配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //创建多个用户
         auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder.encode("admin"))
                .authorities("admin");
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder.encode("zhangsan"))
                .authorities("user");
        auth.inMemoryAuthentication()
                .withUser("lisi")
                .password(passwordEncoder.encode("lisi"))
                .authorities("user");
    }
}
4.4 基于数据库定义用户名密码

1、准备数据表

  • 数据表中添加id、username、password字段

2、使用Mybatis-plus定义Mapper接口

public interface TbUserMapper extends BaseMapper<TbUser> {
}

3、编写UserService接口以及实现类

public interface UserService {
    TbUser findUserByName(String username);
}
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private TbUserMapper userMapper;
    @Override
    public TbUser findUserByName(String username) {
        QueryWrapper<TbUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",username);
        return userMapper.selectOne(queryWrapper);
    }
}

4、自定义实现UserDetailService接口

@Component("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        TbUser tbUser = userService.findUserByName(s);
        if(tbUser == null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        //获取用户角色或者权限,暂时先手动定义一个,未来从数据库中获取。
        //格式:"admin,user,manager,user_add,user_del"
        List<GrantedAuthority> authorityList = AuthorityUtils
            .commaSeparatedStringToAuthorityList("admin,user");

        //返回User对象,SpringSecurity去进行验证
        User user = new User(tbUser.getUsername(),tbUser.getPassword() ,authorityList );
        return user;
    }
}

5、修改Security配置类使用数据库认证

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通过数据库获取认证用户信息
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder);
    }
}
4.5 自定义登录页面

修改Security配置类

  • 自定义成功跳转页面
  • 自定义失败跳转页面
  • 授权访问
  • 关闭csrf防护
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通过数据库获取认证用户信息
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //定义表单提交
        http.formLogin()
               //自定义登录页面
                .loginPage("/login.html")
                //自定义表单请求的用户名密码的name属性值(默认为username|password)
                //.usernameParameter("username")
                //.passwordParameter("password")
                //定义登录时访问的路径(与表单中action路径一致)
                .loginProcessingUrl("/login")
                //登录成功后默认跳转的页面  (必须走控制器)
                .successForwardUrl("/index")
                //登录成功之后的操作。可以自定义实现如,返回json格式
                //.successHandler()
                //登录失败后默认跳转的页面  (必须走控制器)
                .failureForwardUrl("/errorPage");

        //授权
        http.authorizeRequests()
                //允许未登录允许访问
                .antMatchers("/login.html","/login").permitAll()
                .antMatchers("/index").permitAll()
                .antMatchers("/error.html","/errorPage").permitAll()
                //其他所有的请求必须登录才能访问
                .anyRequest().authenticated();

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

页面跳转必须要转发,定义Controller转发页面

@Controller
public class PageController {

    @RequestMapping("/index")
    public String index(){
        return "redirect:/index.html";
    }
    @RequestMapping("/errorPage")
    public String error(){
        return "redirect:/error.html";
    }

}
4.6 退出登录

设置退出登录的请求地址

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通过数据库获取认证用户信息
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {        
        
       //定义退出登录的请求地址  此注销工作由security完成,默认访问路径就是/logout
        http.logout()
                //设置退出登录的请求地址
                .logoutUrl("/logout")
                //设置退出成功之后的请求地址。允许认证访问
                .logoutSuccessUrl("/logoutPage").permitAll();
        
        //自定义退出登录的响应,  用于返回json
        //http.logout().logoutUrl("/logout").addLogoutHandler();         
    }
}

定义跳转页面

@RequestMapping("/logoutPage")
public String logout(){
    return "redirect:/login.html";
}

前端定义退出登录按钮

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>登录成功 <a href="/logout">退出登录</a></h1>
</body>
</html>
4.7 csrf防护

为了测试顺利,这里临时关闭csrf防护。所谓csrf防护,全称为跨站请求伪造(Cross-site request forgery),是一种网络攻击方式,CSRF攻击利用网站对于用户网页浏览器的信任,挟持用户当前已登陆的Web应用程序,去执行并非用户本意的操作。简而言之,用户通过盗取目标网站保存的cookie中的用户信息,实现非法使用。

五、基于角色和权限进行访问控制

5.1 基于内存定义用户角色和权限

1、自定义配置类中添加角色

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通过数据库获取认证用户信息
        //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        //创建多个用户
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder.encode("admin"))
                .roles("admin","user")
                .authorities("user_select","user_add","user_del","user_update");
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder.encode("zhangsan"))
                .roles("user")
                .authorities("user_select");
        auth.inMemoryAuthentication()
                .withUser("wangwu")
                .password(passwordEncoder.encode("wangwu"))
                .roles("user")
                .authorities("user_select");
    }
}
5.2 配置类中通过角色和权限进行访问控制

2、在配置类中通过角色来校验用户具有某个角色,才可以访问

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //创建多个用户
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder.encode("admin"))
                .roles("admin","user")
                .authorities("user_select","user_add","user_del","user_update");
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder.encode("zhangsan"))
                .roles("user")
                .authorities("user_select");
        auth.inMemoryAuthentication()
                .withUser("wangwu")
                .password(passwordEncoder.encode("wangwu"))
                .roles("user")
                .authorities("user_select");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //定义表单提交
        http.formLogin()
                //自定义登录页面
                .loginPage("/login.html")
                //定义登录时访问的路径(与表单中action路径一致)
                .loginProcessingUrl("/login")
                //登录成功后默认跳转的页面  (必须走控制器)
                .successForwardUrl("/index")
                //登录失败后默认跳转的页面  (必须走控制器)
                .failureForwardUrl("/errorPage");

        //授权
        http.authorizeRequests()
                //未登录允许访问
                .antMatchers("/login.html","/login").permitAll()
                .antMatchers("/index").permitAll()
                .antMatchers("/error.html","/errorPage").permitAll()
 				//拥有对应的角色才能访问
//                .antMatchers("/product/**").hasAnyRole("admin","user")
//                .antMatchers("/order/**").hasAnyRole("admin","user")
//                .antMatchers("/user/**").hasAnyRole("admin")
            
            	//拥有对应的权限才能访问
                .antMatchers("/product/**").hasAnyAuthority("user_add","user_del")
                .antMatchers("/order/**").hasAnyAuthority("user_add","user_del")
                .antMatchers("/user/**").hasAnyAuthority("user_select")

                //其他所有的请求必须登录才能访问
                .anyRequest().authenticated();

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

定义前端页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>登录成功</h1>
    <a href="/user">用户操作</a>
    <a href="/product">商品操作</a>
    <a href="/order">订单操作</a>
</body>
</html>

定义页面转发

@Controller
public class PageController {

    @RequestMapping("/index")
    public String index(){
        return "redirect:/index.html";
    }
    @RequestMapping("/errorPage")
    public String error(){
        return "redirect:/error.html";
    }
    @RequestMapping("/product")
    public String product(){
        return "redirect:/success.html";
    }
    @RequestMapping("/user")
    public String user(){
        return "redirect:/success.html";
    }
    @RequestMapping("/order")
    public String order(){
        return "redirect:/success.html";
    }
}
5.3 使用@PreAuthorize注解进行访问控制

2、在Controller中添加@PreAuthorize注解来校验用户具有某个角色,才可以访问方法

@RestController
public class SecurityController {
    
    @RequestMapping("/hiUser")
    @PreAuthorize("hasAnyRole('admin','user')")
    public String hiUser(){
        return "Hello,user!!(需要拥有admin或者user角色)";
    }

    @RequestMapping("/hiAdmin")
    @PreAuthorize("hasAnyRole('admin')")
    public String hiAdmin(){
        return "Hello,admin!!(需要拥有admin角色)";
    }
    
}

3、开启全局方法权限校验

//在配置类或者引导类开启
@EnableGlobalMethodSecurity(prePostEnabled=true)

4、测试

目前有两个角色,admin以及user角色

  • 访问hiUser方法需要admin或者user角色
  • 访问hiAdmin方法需要admin角色
5.4 配置403权限错误页面

方式1:在配置类中添加权限错误页面

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //通过数据库获取认证用户信息
        //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        //创建多个用户
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder.encode("admin"))
                .roles("admin","user")
                .authorities("user_select","user_add","user_del","user_update");
        auth.inMemoryAuthentication()
                .withUser("zhangsan")
                .password(passwordEncoder.encode("zhangsan"))
                .roles("user")
                .authorities("user_select");
        auth.inMemoryAuthentication()
                .withUser("wangwu")
                .password(passwordEncoder.encode("wangwu"))
                .roles("user")
                .authorities("user_select");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {


        //配置没有权限出异常跳转的页面
        http.exceptionHandling().accessDeniedPage("/403.html");
        //自定义返回结果,用于返回json数据
        //http.exceptionHandling().accessDeniedHandler();

        //定义退出登录的请求地址  此注销工作由security完成,默认访问路径就是/logout
        http.logout()
                //设置退出登录的请求地址
                .logoutUrl("/logout")
                //设置退出成功之后的请求地址。允许认证访问
                .logoutSuccessUrl("/logoutPage").permitAll();
        //自定义退出登录的响应,  用于返回json
        //http.logout().logoutUrl("/logout").addLogoutHandler();

        //定义表单提交
        http.formLogin()
                //自定义登录页面
                .loginPage("/login.html")
                //自定义表单请求的用户名密码的name属性值(默认为username|password)
                //.usernameParameter("username")
                //.passwordParameter("password")
                //定义登录时访问的路径(与表单中action路径一致)
                .loginProcessingUrl("/login")
                //登录成功后默认跳转的页面  (必须走控制器)
                .successForwardUrl("/index")
                //登录成功之后的操作。可以自定义实现如,返回json格式
                //.successHandler()
                //登录失败后默认跳转的页面  (必须走控制器)
                .failureForwardUrl("/errorPage");

        //授权
        http.authorizeRequests()
                //允许未登录允许访问
                .antMatchers("/login.html","/login").permitAll()
                .antMatchers("/index").permitAll()
                .antMatchers("/error.html","/errorPage").permitAll()

//                .antMatchers("/product/**").hasAnyRole("admin","user")
//                .antMatchers("/order/**").hasAnyRole("admin","user")
//                .antMatchers("/user/**").hasAnyRole("admin")
                .antMatchers("/product/**").hasAnyAuthority("user_add","user_del")
                .antMatchers("/order/**").hasAnyAuthority("user_add","user_del")
                .antMatchers("/user/**").hasAnyAuthority("user_select")

                //其他所有的请求必须登录才能访问
                .anyRequest().authenticated();

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

方式2:自定义异常处理器 判断异常类型为:AccessDeniedException表示没有权限

5.5 其他注解介绍
  • @Secured注解
  • 注解用于校验用户具有某个角色,才可以访问方法
  • @PreAuthorize
  • 进入方法前的权限验证
  • @PostAuthorize
  • 在方法访问之后进行校验,实际使用并不多
  • @PostFilter
  • 权限验证之后对数据进行过滤,只能获取满足条件的数据

六、RBAC权限管理

6.1 数据库设计

RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。

RBAC数据库模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-icbUcAXg-1645512922140)(image\image-20210908123402205.png)]

6.2 连接数据查询用户拥有角色和权限

security配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("myUserDetailService")
    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.formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .successForwardUrl("/index")
                .failureForwardUrl("/errorPage");

        http.authorizeRequests()
                .antMatchers("/login.html","/login").permitAll()
                .antMatchers("/index").permitAll()
                .antMatchers("/errorPage").permitAll()
                .anyRequest().authenticated();

        http.csrf().disable();
    }
}

UserDetailService自定义登录逻辑类

@Component("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private UserService userService;

    @Autowired
    private PermissionService permissionService;

    @Autowired
    TbPermissionMapper permissionMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        TbUser tbUser = userService.findUserByName(username);
        if(tbUser == null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        //根据用户名查询用户对应的角色
        List<String> roleList = permissionService.selectRole(username);
        String authorityStr = "";
        if(roleList != null && roleList.size() > 0){
            for (int i = 0; i < roleList.size(); i++) {
                authorityStr+= "ROLE_"+roleList.get(i) +",";
            }
        }
        //根据用户名查询用户对应的权限
        List<String> permissionList = permissionMapper.selectAllPermission(username);
        if(permissionList != null && permissionList.size() >0){
            for (int i = 0; i < permissionList.size(); i++) {
                authorityStr+= permissionList.get(i)+",";
            }
        }
        //将用户角色和权限拼接到特定格式的字符串中
        if(authorityStr.endsWith(","))
            authorityStr = authorityStr.substring(0,authorityStr.length()-1);

        //获取用户角色或者权限。格式:"ROLE_admin,ROLE_user,ROLE_manager,user_add,user_del"
        List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(authorityStr);

        //返回User对象,SpringSecurity去进行验证
        User user = new User(tbUser.getUsername(),tbUser.getPassword() ,authorityList );
        return user;
    }
}

定义mapper接口以及自定义映射文件

public interface TbPermissionMapper extends BaseMapper<TbPermission> {
    List<String> selectAllPermission(String username);
}
public interface TbRoleMapper extends BaseMapper<TbRole> {
    List<String> selectRole(String username);
}
<select id="selectAllPermission" resultType="String">
    select distinct tb_permission.permission_name
    from tb_user
    left join tb_user_role on tb_user_role.uid = tb_user.id
    left join tb_role on tb_role.role_id = tb_user_role.rid
    left join tb_role_permission on tb_role_permission.rid = tb_role.role_id
    left join tb_permission on tb_permission.permission_id = tb_role_permission.pid
    where tb_user.username = #{username}
</select>
<select id="selectRole" resultType="string">
    select tb_role.role_name
    from tb_user
    left join tb_user_role on tb_user_role.uid = tb_user.id
    left join tb_role on tb_role.role_id = tb_user_role.rid
    where tb_user.username = #{username}
</select>

定义Controller测试权限

@PreAuthorize("hasAnyRole('admin')")
@RequestMapping("user/list")
@ResponseBody
public String toUser(){
    return "拥有权限可以访问";
}

七、权限管理系统

7.1 基于SpringSecurty实现权限管理

Controller处理器

@Controller
public class IndexController {
    @Autowired
    private PermissionService permissionService;
    @RequestMapping("home")
    public String index(Model model){
        //获取登录认证主体
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        //查询用户菜单
        List<TbMenu> permissionMenu = 
            permissionService.selectPermissionMenu(user.getUsername());
        model.addAttribute("permissionMenu",permissionMenu);
        return "index";
    }

    @RequestMapping("index")
    public String redirect(){
        return "redirect:/home";
    }
}

根据用户查询角色和权限Service

@Service
public class PermissionServiceImpl implements PermissionService {
    @Autowired
    private TbPermissionMapper permissionMapper;
    @Autowired
    private TbRoleMapper roleMapper;

    @Override
    public List<TbMenu> selectPermissionMenu(String username) {
        //用户菜单
        List<TbMenu> menuList = new ArrayList<>();
        //一级菜单列表
        List<TbPermission> permissionList1 = permissionMapper.selectPermission(username);
        //获取二级菜单列表
        if(permissionList1 != null && permissionList1.size() > 0){
            for (TbPermission tbPermission : permissionList1) {
                QueryWrapper<TbPermission> queryWrapper = new QueryWrapper<>();
                queryWrapper.ne("permission_show","0");
                queryWrapper.eq("permission_parent_id",tbPermission.getPermissionId());
                List<TbPermission> permissionList2 = permissionMapper.selectList(queryWrapper);
                menuList.add(new TbMenu(tbPermission,permissionList2));
            }
        }
        return menuList;
    }
    @Override
    public List<String> selectRole(String username) {
        return roleMapper.selectRole(username);
    }
}

Mapper及映射文件

public interface TbPermissionMapper extends BaseMapper<TbPermission> {
    //根据用户名查询用户的一级菜单
    List<TbPermission> selectPermission(String username);
}
<select id="selectPermission" resultType="TbPermission">
    select distinct tb_permission.*
    from tb_user
    left join tb_user_role on tb_user_role.uid = tb_user.id
    left join tb_role on tb_role.role_id = tb_user_role.rid
    left join tb_role_permission on tb_role_permission.rid = tb_role.role_id
    left join tb_permission on tb_permission.permission_id = tb_role_permission.pid
    where tb_user.username = #{username} and permission_parent_id = '10'
</select>

前端这里选择的是Thymeleaf+layui实现

<!-- 左侧导航区域(可配合layui已有的垂直导航) -->
<ul class="layui-nav layui-nav-tree" lay-filter="test">
    <li class="layui-nav-item layui-nav-itemed" th:each="menu:${permissionMenu}" >
        <a  href="javascript:;" th:text="${menu.permission.permissionDesc}"></a>
        <dl class="layui-nav-child">
            <dd th:each="m:${menu.menuList}">
                <a th:href="@{${m.permissionUrl}}" 
                   target="content" th:text="${m.permissionDesc}"></a>
            </dd>
        </dl>
    </li>
</ul>

 <!-- 内容主体区域 -->
<div style="padding: 15px;" >
    <iframe name="content" width="100%" height="600px"></iframe>
</div>

解决不允许显示在iframe的问题

http.headers().frameOptions().disable();
http.headers().cacheControl();
7.2 Thymeleaf中支持的Security标签

导入依赖

<!--Spring Security和thymeleaf的整合依赖-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

常见的标签

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5" >
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <!--获取登录用户信息-->
        <!--判断是否登录-->
        <div sec:authorize="isAuthenticated()">
            <!--获取登录的用户名-->
            <h2><span sec:authentication="name"></span>,您好 您的身份是
                <!--获取用户的角色和权限-->
                <span sec:authentication="principal.authorities"></span>
            </h2>
        </div>

        <!--基于角色的判断-->
        <button sec:authorize="hasAnyRole('ROLE_admin')">admin角色可以操作</button>
        <button sec:authorize="hasAnyRole('ROLE_user')">user角色可以操作</button>
        <button sec:authorize="hasAnyRole('ROLE_admin','ROLE_user')">admin或者user角色可以操作</button>


        <!--基于权限的判断-->
        <button sec:authorize="hasAnyAuthority('product_list')">list查看权限</button>
        <button sec:authorize="hasAnyAuthority('product_add')">添加</button>
        <button sec:authorize="hasAnyAuthority('product_del')">删除</button>
        <button sec:authorize="hasAnyAuthority('product_edit')">修改</button>
    </body>
</html>
<meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!--获取登录用户信息-->
    <!--判断是否登录-->
    <div sec:authorize="isAuthenticated()">
        <!--获取登录的用户名-->
        <h2><span sec:authentication="name"></span>,您好 您的身份是
            <!--获取用户的角色和权限-->
            <span sec:authentication="principal.authorities"></span>
        </h2>
    </div>

    <!--基于角色的判断-->
    <button sec:authorize="hasAnyRole('ROLE_admin')">admin角色可以操作</button>
    <button sec:authorize="hasAnyRole('ROLE_user')">user角色可以操作</button>
    <button sec:authorize="hasAnyRole('ROLE_admin','ROLE_user')">admin或者user角色可以操作</button>


    <!--基于权限的判断-->
    <button sec:authorize="hasAnyAuthority('product_list')">list查看权限</button>
    <button sec:authorize="hasAnyAuthority('product_add')">添加</button>
    <button sec:authorize="hasAnyAuthority('product_del')">删除</button>
    <button sec:authorize="hasAnyAuthority('product_edit')">修改</button>
</body>