三、Spring Security_15(笔记)

17 篇文章 0 订阅

Spring Security

对于权限的管理,在企业应用程序的开发中,是必不可少的功能,但是能够灵活且强大的权限控制又不是一件容易的事情,所以在自己学习编写权限控制体系的基础上也接触一下成熟的框架,Spring 的全家桶系列 Spring Security 就进入了我们的视线。

1、概念

Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是那个用户。

授权:经过认证后判断当前用户是否有权限进行某个操作。

认证和授权也是 Spring Security 作为安全框架的核心功能

搭建如图项目:

image-20230813210029795

2、入门案例

我们通过一个简单的入门案例来了解 Spring Security 。

引入依赖

我们已父子项目方式搭建,所有的依赖都在父项目中书写,子项目只需继承父项目即可。父项目中的写法:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
</parent>
<dependencies>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>        
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

控制类

书写一个普通的 controller 类

@RestController
@RequestMapping("/first")
public class FirstController {

    @GetMapping("/a")
    public JsonResult a() {
        return ResultTool.success("Success");
    }
}

启动类

@SpringBootApplication
public class Security01Application {
    public static void main(String[] args) {
        SpringApplication.run(Security01Application.class, args);
    }
}

运行效果

当我们还按照过去的方式访问 http://localhost:8080路径时,发现弹出了登录框,这个是 Spring Security 自带的登录页面,要求我们必须登录后才能访问系统资源。

image-20230813161442987

并且在项目启动后,在控制台能看到初始生成的密码。

image-20230813161506238

使用 Spring Security 后访问系统任意资源时,就会跳转到默认登录页面,默认账号是 user ,登录成功后,才能访问才能对目标接口进行访问。以后只要不关闭浏览器或者服务器,都可以直接访问。也可以手动登出,路径是:logout 。

3、登陆认证

我们刚才通过一个简单的案例,了解了 Spring Security 的基本概念。也发现 Spring Security 会在服务器启动时随机生成密码,那么有的童鞋就会想到,能不能自己去定义这个密码,甚至于使用数据库来校验用户登陆。

自定义账号密码

这种方式其实很简单,只需要在配置文件 (application.yml) 中设置账号密码就行。

spring:
  security:
    user:
      name: root
      password: root

还可以在配置类中进行设置,注释上边的写法,在 config 包下创建一个配置类 SecurityConfig 来配置账号密码信息。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder passwordEncoder = getPasswordEncoder();
        String pass = passwordEncoder.encode("123456");
        auth.inMemoryAuthentication().withUser("lisi").password(pass).roles("admin");
    }

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 类是 Spring Security 中的一个加密方法类。BCryptPasswordEncoder 方法采用了 SHA-256+随机盐+密钥对密码进行加密。 SHA 是一种安全 Hash 函数(SHA),是使用最广泛的Hash函数。
加密算法与 hash 算法的区别

加密算法是可逆的,加密算法的基本过程是对原来为明文的数据按某种算法进行处理,使其成为不可读的一段代码为“密文”,但在用相应的密钥进行操作之后就可以得到原来的内容。
hash 算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。

数据库校验方式

前面两种都是写死的,可能对于固定的超级管理员可以用,我们真实的项目场景肯定都是数据库里面的。

需要自定义查询,我们先把SecurityConfig 注释了,重新创建一个类 SpringSecurityConfig 来从数据库中判断账号密码。

引入数据库相关依赖

<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <scope>runtime</scope>
</dependency>
<dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.5.1</version>
</dependency>
<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid-spring-boot-starter</artifactId>
     <version>1.2.9</version>
</dependency>

application.yml

spring:
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/zgh
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: false

书写 bean 包的类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {

    private Integer id;
    private String name;
    private String password;
    private String nickname;
    private String gender;
    private String birthday;
}

书写 mapper 包的类

@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

书写一个 UserServiceImpl 类继承ServiceImpl

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {

    @Resource
    private UserMapper mapper;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("开始进行登陆校验,账号是:{}", username);
        //检验账号是否存在
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("name", username);
        User user = mapper.selectOne(wrapper);
        if (user == null) {
            log.info("账号不存在");
            throw new UsernameNotFoundException("账号不存在");
        }
        List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("admin");
        log.info("校验通过");
        // 发送到下一级
        return new org.springframework.security.core.userdetails.User(username, passwordEncoder.encode(user.getPassword()), auths);
    }
}

书写 SpringSecurityConfig 配置类


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       /*
        String pass = passwordEncoder.encode("123456");
        auth.inMemoryAuthentication().withUser("lisi").password(pass).roles("admin");*/
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
}

输入错误的账户信息

image-20230813174612163

查看控制台:

image-20230813174659763

这时候继续访问刚才的页面,输入数据库的账号和密码

image-20230813175228424

点击登录,跳转页面

image-20230813154054230

查看控制台

image-20230813175932444

流程:当输入 localhost:8080/first/a,敲回车的时候,路径变成 localhost:8080/login,判断你没有登录,进入到login的请求,这是login是controller的一个地址,自己没写controller,它把controller写了,它写了controller之后,再输入账号和密码之后,登录按钮一按,账号和密码就会送到login的controller中去,拿到账号密码之后,把账号给了(调用UserService的实现类中的loadUserByUsername()方法来验证账户是否存在,和密码没有关系),如果验证通过了,就会把账号、加密后的密码、权限,一起封装为一个对象(org.springframework.security.core.userdetails.User(username, passwordEncoder.encode(user.getPassword()), auths);),把这个对象交给下一级,验证密码和数据库中的密码是否相匹配,判断是否通过。

4、自定义登录页面

前后端分离:不一定不是在一个项目中(可在可不在)

以前:jsp页面去请求controller,controller得到数据之后利用response跳转到页面。

分离更多指的是这种情况:Ajax操作,比如前端向后端发送了一个请求,前端项目和后端可在、可不在(得跨域)同一个项目中,通过url网址的方式向后端发送请求,后端接收请求,不能通过 response方式或者转发的方式把前端的页面实现跳转,只能返回给前端一个

成功或者失败的数据,这是显得前端和后端没有关系了,都是借用第三者json来完成数据的交互,这个才叫分离。

我们刚才一直使用 security 自带的登陆页面,但是在实际使用中我们更多的是使用我们自己书写的登陆页面,要想设置其实很简单,如下几步就行。

创建登录页面

<!--
  表单提交用户信息,注意
  1.账号和密码的名字
  2.action的提交地址和配置类中设置一致
-->
<form action="user/login" method="post">
  账号:<input type="text" name="username"/><br/>
  密码:<input type="password" name="password"/><br/>
  <button>登录</button>
</form> 

创建首页页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
我是首页!
</body>
</html>

在配置类(SecurityConfig)中设置登录页面

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//自定义登陆页面
                .loginPage("/login.html") //登录页面设置(指定登录名)
                .loginProcessingUrl("/user/login")//登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()//登陆成功之后,跳转路径
                .and().authorizeRequests()//设置请求的权限
            	//设置哪些路径可以直接访问,不需要认证(登录)
                .antMatchers("/login.html", "/user/login").permitAll()
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

运行结果:

1、启动项目,进行登录

image-20230813193138560

2、去到login页面,login没有请求,返回到上一界面

image-20230813193940461

3、再次点击登录

image-20230813194013594

5、授权管理

在项目中,有很多接口是针对不同角色权限的,如果角色是超级管理员,就拥有访问所有权限的能力,如果不是超级管理员,访问其他接口是不能允许的。 有些用户具有部分权限,就可以访问这些权限所能访问的内容,如果要实现这种效果,我们就需要授权管理。

基于权限

写几个页面

<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>1.html</h1>
</body>
</html>
    
    
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>2.html</h1>
</body>
</html>

    
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>3.html</h1>
</body>
</html>    

    
<!DOCTYPE html>
<html lang="en">
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>4.html</h1>
</body>
</html>

**hasAuthority()**方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false。

配置类

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//自定义登陆页面
                .loginPage("/login.html") //登录页面设置(指定登录名)
                .loginProcessingUrl("/user/login")//登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()//登陆成功之后,跳转路径
                .and().authorizeRequests()//设置请求的权限
                .antMatchers("/login.html", "/user/login").permitAll()//设置哪些路径可以直接访问,不需要认证(登录)
                // 设置当前登录用户访问html页面时需要manager1、manager2、manager3或manager4权限
                .antMatchers("/1.html").hasAuthority("manager1")
                .antMatchers("/2.html").hasAnyAuthority("manager1", "manager2", "manager3")
                 .antMatchers("/3.html").hasAuthority("manager2")
                 .antMatchers("/4.html").hasAnyAuthority("manager1", "manager2")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

授予权限

UserServiceImpl——>loadUserByUsername

// 获取权限 假的(后续会讲解权限问题)
        List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("manager1", "manager2");

运行结果:

访问 html 页面需要权限

访问1.html 只要有 manager1

image-20230813201720411

访问2.html 只要有manager1、manager2、manager3 权限之一

image-20230813201815641

访问3.html 只要有 manager2

image-20230813201931965

访问4.html 只要有manager1、manager2权限之一

image-20230813202032145

基于角色

配置类

image-20230813202636673

//.antMatchers("/3.html").hasAuthority("manager2")
//.antMatchers("/4.html").hasAnyAuthority("manager1", "manager2")
.antMatchers("/3.html").hasRole("man1")
.antMatchers("/4.html").hasAnyRole("man3", "man2")

业务类

image-20230813202704451

// 获取权限 假的(后续会讲解权限问题)
        List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("manager1", "manager2","ROLE_man1");

运行结果

访问3.html需要man1角色,而设置中有man1 角色,所以可以访问

image-20230813203254534

访问4.html需要man2或man3角色,而设置中没有一个 对应的角色,所以不可以访问

image-20230813203931385

配置 403 页面

当没有权限的时候访问会报403,我们可以自定义一个页面

创建一个 403.html 页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
</head>
<body>
<h2>你没有相应的权限访问这个页面!</h2>
</body>
</html>

配置类中设置

image-20230813204153740

 .and().exceptionHandling().accessDeniedPage("/403.html")

运行结果

访问没有权限的4.html

image-20230813205714032

由此可见,设置成功

用户注销

SpringSecurity 的注销功能很简单,只需要一个超链接地址为 /logout 就行。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--我是首页!-->
我是注销页面!
<a href="/logout">注销</a>
</body>
</html>

运行结果:

1、登录

image-20230813195115026

2、注销

image-20230813195151906

3、注销之后返回页面,重新登录

image-20230813195219277

6、分离项目

通过刚才的案例我们了解到了 SpringSecurity 认证授权的效果。可是刚才通过的是表单方式进行提交,在其后的项目中我们可能更多的使用前后端分离效果,那么我们就要返回给前端对应的消息来告诉前端认证授权的情况。

前期准备

搭建项目

image-20230814143735793

导入依赖
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring_boot_commons</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/zgh
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: false

数据库信息

image-20230814141541232

前后端分离项目,需要后端返回消息来标注相应状态,这时候我们创建三个类来对返回的消息统一格式。

统一返回格式
@Data
public class JsonResult<T> implements Serializable {
    
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;
    
    public JsonResult() {
    }
    
    public JsonResult(boolean success) {

        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode(
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }


    public JsonResult(boolean success, T data) {

        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }
}
返回状态码定义
/**
 * @Description: 返回码定义
 * 规定:
 * #1表示成功
 * #1001~1999 区间表示参数错误

 * #2001~2999 区间表示用户错误

 * #3001~3999 区间表示接口异常

 */

public enum ResultCode {

    /* 成功 */
    SUCCESS(200, "成功"),
    
    /* 默认失败 */
    COMMON_FAIL(999, "失败"),

    /* 参数错误:1000~1999 */

    PARAM_NOT_VALID(1001, "参数无效"),

    PARAM_IS_BLANK(1002, "参数为空"),

    PARAM_TYPE_ERROR(1003, "参数类型错误"),

    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */

    USER_NOT_LOGIN(2001, "用户未登录"),

    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),

    USER_CREDENTIALS_ERROR(2003, "密码错误"),

    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),

    USER_ACCOUNT_DISABLE(2005, "账号不可用"),

    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),

    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),

    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),

    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    /* 业务错误 */

    NO_PERMISSION(3001, "没有权限");

    private Integer code;

    private String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
        
    }
    
    public Integer getCode() {
        return code;

    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根据code获取message
     * @return
     */

    public static String getMessageByCode(Integer code) {
        
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();

            }
        }
        return null;
    }
}
handler包
未登录效果

当用户未登录时,会自动进入当前类的 commence 方法,我们在这个方法中返回 JSON 格式的错误信息。

// 用户没有登陆时执行的代码
@Component
@Slf4j
public class NotLoginAuthentication implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        log.info("用户没有登陆");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("用户没有登陆")));

    }
}

登录失败效果

当用户登录失败时(不论是账号未找到,密码错误还是权限问题),都会进入这个类的指定方式,我们在这个方法中返回 JSON 格式的错误信息。

//登录失败的操作

@Component
@Slf4j
public class FailureAuthenticationHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        log.info("账号和密码错误!");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out=response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
    }
}
登录成功效果

同样的道理,对正确的消息返回 JSON 格式信息。

// 登陆成功后要执行的代码

@Component
@Slf4j
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登陆成功");
        // 1. 生成jwt字符串
        // 2. 保存到redis
        // 3. 返回给前端token

        //处理编码方式,防止中文乱码的情况
        response.setContentType("application/json;charset=utf-8");
        //塞到response中返回给前台
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.success("登陆成功!")));
    }
}
配置文件

在配置文件中注册三个效果。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private NotLoginAuthentication authentication;

    @Resource
    private FailureAuthenticationHandler failureAuthenticationHandler;

    @Resource
    private SuccessAuthenticationHandler successAuthenticationHandler;


    @Override
    //模拟数据(前后端分离)
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String pass = getPasswordEncoder().encode("123456");
        auth.inMemoryAuthentication().withUser("admin").password(pass).roles(("manager"));
    }
    //前后端分离
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll()
                .successHandler(successAuthenticationHandler)  //登录成功处理逻辑
                .failureHandler((failureAuthenticationHandler)) //登录失败处理逻辑
                //登录成功处理逻辑
                .and()
                .exceptionHandling().authenticationEntryPoint(authentication)//匿名用户访问无权限资源时的异常处理
                .and()
                .authorizeRequests()
                .antMatchers("/login.html", "/login", "/index.html", "/").permitAll()
                .antMatchers("/1.html").hasAnyAuthority("admin")
                .antMatchers("/2.html").hasAnyAuthority("manager")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

启动类
@SpringBootApplication
public class SpringBootSecurity2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurity2Application.class, args);
    }
}
前端部分

1.html 2.html index.html login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>1.html</h1>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>2.html</h1>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>index.html</h1>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--
  表单提交用户信息,注意
  1.账号和密码的名字
  2.action的提交地址和配置类中设置一致
-->
<form action="/login" method="post">
    账号:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <button>登录</button>
</form>
</body>
</html>
测试结果

登录成功的情况

测试login.html(登录之后不保存登录信息,访问其他页面还是认为没有登录)

页面

image-20230814141728290

image-20230814141825949

后端控制台

image-20230814141901009

登录失败的情况

image-20230814142210345

没有登录就访问其他资源情况

测试index.html(有权限)

image-20230814141328231

测试1.html(无权限)

image-20230814143522417

测试2.html(无权限)

image-20230814143424206

7、进一步拓展

刚才的例子中,我们可以通过几个 Handler 操作未登录时、登录失败和登录成功情况。那么前后端分离情况下,如何保存用户登录状态呢?下边我们通过 Security 的几个过滤器来实现这个功能。

前期准备

其他模块操作

1、将jwt依赖导入公共模块

image-20230814145133180

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- jaxb依赖包 -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.3.0</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>

注意:版本号不要修改,以免出现错误

2、给公共模块创建config包

这里通过 JWT 令牌方式来验证用户登录,这里创建 JwtConfig 类。

package zgh.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import zgh.bean.User;

import java.util.Date;

public class JwtConfig {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
    public static final String APP_SECRET = "1234"; //秘钥,加盐

    //	@param id 当前用户ID
    //	@param issuer 该JWT的签发者,是否使用是可选的
    //	@param subject 该JWT所面向的用户,是否使用是可选的
    //	@param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
    //	@param audience 接收该JWT的一方,是否使用是可选的
    // 生成json token字符串的方法
    public static String getJwtToken(User user) {

        String jwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")    //头部信息
                .setHeaderParam("alg", "HS256")    //头部信息
                //下面这部分是payload部分
                // 设置默认标签
                .setSubject("dailyblue")    //设置jwt所面向的用户
                .setIssuedAt(new Date())    //设置签证生效的时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))    //设置签证失效的时间
                //自定义的信息,这里存储id和姓名信息
                .claim("id", user.getId())  //设置token主体部分 ,存储用户信息
                .claim("name", user.getName())
                .claim("nickName", user.getNickname())
                //下面是第三部分
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
        // 生成的字符串就是jwt信息,这个通常要返回出去
        return jwtToken;
    }

    /**
     * 判断token是否存在与有效
     * 直接判断字符串形式的jwt字符串
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (null == jwtToken) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 解析JWT
     *
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
        return claims;
    }
}
本模块操作
搭建项目

image-20230814224007785

导入依赖
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

SecurityUser 类,这个类描述用户信息和它的权限信息。

@Data
public class SecurityUser implements UserDetails {
    // 登录用户的基本信息
    private User user;
    //当前权限
    private List<String> authentications;

    //获取权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authentications.forEach(permission -> {
            if (!StringUtils.isEmpty(permission)) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
                authorities.add(authority);
            }
        });
        return authorities;
    }

    //得到密码
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    //得到用户名
    @Override
    public String getUsername() {
        return user.getName();
    }

    //权限是否可用
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //账号是否被锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //账号是否过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //账号是否可见
    @Override
    public boolean isEnabled() {
        return true;
    }
}
mapper包
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
service包

UsersService 类,业务类,负责调用 Mapper 的方法

public interface UserService extends IService<User> {
}

实现类(impl——>UserServiceImpl)

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService, UserService {
    @Resource
    private UserMapper mapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("开始进入service实现登陆操作");
        // 验证账号是否正确
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("name", username);
        User user = mapper.selectOne(wrapper);
        if (StringUtils.isEmpty(user)) {
            log.info("账号不存在");
            throw new UsernameNotFoundException("账号没有找到!");
        }
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        log.info("数据库密码是:{}", user.getPassword());
        // 如果正确,获取权限  假的
        List<String> auths = Arrays.asList("manager1", "manager2", "manager3");
        // 包装UserDetails类并返回
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        securityUser.setAuthentications(auths);
        log.info("账号验证通过,开始验证密码!");
        return securityUser;
    }
}
登录过滤器

这个是一个 Filter ,不需要 Spring 来注入

  • attemptAuthentication 方法 用户登录时触发,获取账号和密码,传递到我们自己书写的 UsersService 中。
  • successfulAuthentication 方法 登录成功后执行的方法,一般存放 token 到 Redis 中,返回成功信息。
  • unsuccessfulAuthentication 方法 登录失败后执行的方法。

后两个如果书写了,上一个案例中的那两个 Handler 就可以不书写了

//登录过滤器
@Slf4j
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    private StringRedisTemplate stringRedisTemplate;

    private PasswordEncoder passwordEncoder;

    public JwtLoginFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
        this.authenticationManager = authenticationManager;
        this.stringRedisTemplate = stringRedisTemplate;
        this.passwordEncoder = passwordEncoder;
        // 关闭登录只允许 post
        this.setPostOnly(false);
        // 设置登陆路径,并且post请求
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("开始用户登陆操作");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String encoderPassword = passwordEncoder.encode(password);
        log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("登陆成功");
        // 1. 生成jwt字符串
        SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
        String token = JwtConfig.getJwtToken(user.getUser());
        // 2. 保存到redis
        stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
        // 3. 返回给前端token
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.success(token)));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("账号或者密码错误");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
    }
}
验证过滤器

这个过滤器会在每次请求(不需要触发的可以在配置文件中设置)时去触发,主要作用是验证用户是否登录。

// 校验用户是否登陆的过滤器
@Slf4j
public class TokenAuthFilter extends BasicAuthenticationFilter {

    private StringRedisTemplate stringRedisTemplate;

    public TokenAuthFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) {
        super(authenticationManager);
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("开始校验用户是否登陆");
        // 1. 获取到前端发送过来的token
        String token = request.getHeader("token");
        log.info("前端发送过来的token:{}", token);
        // 2. 校验token
        // 2.1 是否发送
        if (token != null) {
            // 2.2 是否合法
            boolean flag = JwtConfig.checkToken(token);
            log.info("是否合法:{}", flag);
            if (flag) {
                Claims claims = JwtConfig.parseJWT(token);
                String id = claims.get("id").toString();
                String redisToken = stringRedisTemplate.opsForValue().get("TOKEN:" + id);
                log.info("redis的token:{}", redisToken);
                // 2.3 是否篡改
                if (token.equals(redisToken)) {
                    // 3. 获取权限
                    List<String> list = Arrays.asList("manager1", "manager2", "manager3");
                    List<GrantedAuthority> auths = new ArrayList<>();
                    list.forEach((e) -> {
                        GrantedAuthority authority = new SimpleGrantedAuthority(e);
                        auths.add(authority);
                    });
                    log.info("成功,下放权限!");
                    // 4. 下放权限
                    UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(id, token, auths);
                    // 有权限,则放入权限上下文中
                    SecurityContextHolder.getContext().setAuthentication(upat);
                }
            }
        } else {
            // 清空操作
            SecurityContextHolder.getContext().setAuthentication(null);
        }
        chain.doFilter(request, response);
    }
}
配置文件

注释掉前面的config的前后端分离的部分

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private NotLoginAuthentication authenticationEntryPoint;
    @Resource
    private FailureAuthenticationHandler failureHandler;
    @Resource
    private SuccessAuthenticationHandler successHandler;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private UserDetailsService service;

    // 前后端分离
    /*@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String pass = getPasswordEncoder().encode("123456");
        auth.inMemoryAuthentication().withUser("admin").password(pass).roles("manager");
    }*/

    // 前后端分离
    /*@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll()
                .successHandler(successHandler)  //登录成功处理逻辑
                .failureHandler(failureHandler) //登录失败处理逻辑
                //异常处理(权限拒绝、登录失效等)
                .and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
                .and()
                .authorizeRequests()
                .antMatchers("/login.html", "/login", "/index.html", "/").permitAll()
                .antMatchers("/1.html").hasAnyAuthority("admin")
                .antMatchers("/2.html").hasAnyAuthority("manager")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }*/

    // 进一步拓展
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint) // 未登录 handler
                .and().csrf().disable() // 关闭 csrf 跨域请求
                .cors().and()    // security允许跨域
                .formLogin()
                .loginProcessingUrl("/login")  // 设定登录请求接口
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                .and()
                .authorizeRequests() // 请求设置
                .antMatchers("/login").permitAll() // 配置不需要认证的接口
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .addFilter(new TokenAuthFilter(authenticationManager(), stringRedisTemplate)) // 认证交给 自定义 TokenLoginFilter 实现
                .addFilter(new JwtLoginFilter(authenticationManager(), stringRedisTemplate, getPasswordEncoder()))
                .httpBasic();
    }

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

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
测试结果

APiPost测试:

image-20230814161601602

控制台输出结果

image-20230814161339214

输入正确的用户信息和密码

image-20230814183240453

带上controller测试

controller包
@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
    @GetMapping("/a")
    public JsonResult a() {
        log.info("访问了a");
        return ResultTool.success();
    }
    @GetMapping("/b")
    public JsonResult b() {
        log.info("访问了b");
        return ResultTool.success();
    }
}

携带token的结果

image-20230814185647570

去掉token的结果

image-20230814190209215

关于记录的问题

image-20230814191929933

注意:前端发送过来的token:null时,需要去清空凭证。

在一般的应用项目中,用户只是面对页面,无法去掉token,一般在点击登出操作后,会清空记录,不用担心此问题。

业务流程

有两个过滤器,JwtLoginFilter(登录)和TokenAuthFilter(验证)

JwtLoginFilter的作用是:当用户实现登录操作时候,输入账号和密码时,JwtLoginFilter先进行拦截,得到账号和密码之后,包装成为一个凭证,然后传递下去,进入service层,验证用户的账号是否存在,如果账号存在,把用户和权限包装起来,再生成一个凭证,发送给security,security来进行密码的校验;如果错误,直接返回错误信息(账号没有找到)。当账号和密码认证通过了,继续进入到JwtLoginFilter的successfulAuthentication方法中;当密码校验失败了,会进入wtLoginFilter的unsuccessfulAuthentication方法中。

总结:

登录时,先进filter,filter传给service,service传给security,security会把最终的结果再传给filter,filter会根据结果做出成功或者失败操作。
TokenAuthFilter的作用是:验证是否登录,每次想要访问操作的时候,都得判断有没有登录,首先,先拿到前端送过来handlerd的token字符串,若token为空,没登录;若不为空,验证token是否合法、是否篡改、再和redis中的token字符串比对,如果比对成功,说明是登录用户,然后获取用户的权限,提供一个令牌给用户,此时该用户的状态为登录状态;如果以上条件不满足,就不会提供令牌,因此没有权限,无法访问任何页面。

加载流程

image-20230814202133659

8、方法级别的权限控制

添加方法级别的角色控制,可以通过注解的方式来完成,在需要具有角色或权限方法的上引入 @PreAuthorize 注解。

这里为了方便期间,没有引入数据库,账号和密码都是写死的。

开启权限控制

image-20230814203937873

// 开启方法级别的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)//启动校色认证
控制层的方法上引入注解
@Slf4j
@RestController
@RequestMapping("/first")
public class FirstController {
    @GetMapping("/a")
    @PreAuthorize("hasAnyAuthority('manager1','manager2')")
    public JsonResult a() {
        log.info("访问了a");
        return ResultTool.success();
    }
    @GetMapping("/b")
    @PreAuthorize("hasAnyAuthority('manager1','manager3')")
    public JsonResult b() {
        log.info("访问了b");
        return ResultTool.success();
    }
    @GetMapping("/c")
    @PreAuthorize("hasAnyAuthority('manager2')")
    public JsonResult c() {
        log.info("访问了c");
        return ResultTool.success();
    }
    @GetMapping("/d")
    @PreAuthorize("hasAnyAuthority('manager4')")
    public JsonResult d() {
        log.info("访问了d");
        return ResultTool.success();
    }
}

在TokenAuthFilter中修改

image-20230814204322092

 List<String> list = Arrays.asList("manager1", "manager2");

在UserServiceImpl中修改

image-20230814204630061

 List<String> auths = Arrays.asList("manager1", "manager2");
效果演示

因为没有写页面,用ApiPost测试效果

没有权限,登录不成功

image-20230814205200032

所以登录将token凭证传入get请求中

image-20230814205328107

登录成功,a可以访问

image-20230814205417470

b也可以成功访问

image-20230814205554979

c也可以成功访问

image-20230814205653806

d不可以访问(没有权限)

image-20230814205951341

只有manager4权限

image-20230814210037688

控制台输出

image-20230814210250936

9、SpringSecurity 基本流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案例中的过滤器。

image-20230814210340795

10、SpringSecurity 拦截器

image-20230814210436728

1 . org.springframework.security.web.context.SecurityContextPersistenceFilter
     首当其冲的一个过滤器,作用之重要,自不必多言。
     SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
     SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
     SecurityContext中存储了当前用户的认证以及权限信息。
     
2 . org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
     此过滤器用于集成SecurityContextSpring异步执行机制中的WebAsyncManager
     
3 . org.springframework.security.web.header.HeaderWriterFilter
    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
    
4 . org.springframework.security.web.csrf.CsrfFilter
    csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
     如果不包含,则报错。起到防止csrf攻击的效果。
     
5. org.springframework.security.web.authentication.logout.LogoutFilter
    匹配 URL/logout的请求,实现用户退出,清除认证信息。
    
6 . org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    认证操作全靠这个过滤器,默认匹配URL/login且必须为POST请求。
    
7 . org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
    
8 . org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
     由此过滤器可以生产一个默认的退出登录页面
     
9 . org.springframework.security.web.authentication.www.BasicAuthenticationFilter
    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
    
10 . org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
    
11 . org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
   针对ServletRequest进行了一次包装,使得request具有更加丰富的API
   
12 . org.springframework.security.web.authentication.AnonymousAuthenticationFilterSecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
   spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
   
13 . org.springframework.security.web.session.SessionManagementFilter
   SecurityContextRepository限制同一用户开启多个会话的数量
   
14 . org.springframework.security.web.access.ExceptionTranslationFilter
   异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
   
15 . org.springframework.security.web.access.intercept.FilterSecurityInterceptor
   获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

11、WebSecurityConfigurerAdapter 接口

/**
 *  spring security 核心配置文件
 */
@Configuration
public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired  //自定义的安全元   数据源     实现FilterInvocationSecurityMetadataSource
    private MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
    @Autowired //自定义访问决策器
    private MyAccessDecisionManager myAccessDecisionManager;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         *  from表单登录设置
         */
        http.formLogin()
                .loginPage("")                      //登录页面                      /login
                .passwordParameter("")              //设置form表单中对应的name参数  默认为 password   下同
                .usernameParameter("")              //
                .defaultSuccessUrl("")            //认证成功后的跳转页面 默认跳转页面   可以设置是否总是默认  不是的话可以跳转与用户的target-url
                .failureUrl("")
                .failureForwardUrl("")            //登录失败 转发 的url
                .successForwardUrl("")              //登录成功 转发 的url  与successHandler对应  即处理完后请求转发的url
                .failureHandler(null)               //自定义的认证失败 做什么处理
                .successHandler(null)               //自定义认证成功 后做的处理    ----- 例如 想记录用户信息判断用户状态等
                .permitAll()                      //对于需要所有用户都可以访问的界面 或者url进行设置
                .loginProcessingUrl("")             //自定义处理认证的url    默认为    /login
                .authenticationDetailsSource(null)  //自定义身份验证的数据源  理解为查出数据库中的密码 和权限(可以不加) 然后再交给security
                修改和替换配置     已经配置好的修改   例如下面修改  安全拦截器的安全数据源
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    public <O extends FilterSecurityInterceptor> O postProcess(
                            O fsi) {
                        fsi.setPublishAuthorizationSuccess(true);
                        //修改成自定义的     安全元数据源  权限的源  !!!!!
                        fsi.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);
                        //修改成自定义的     访问决策器  自定义的
                        fsi.setAccessDecisionManager(myAccessDecisionManager);
                        //使用系统的
                        fsi.setAuthenticationManager(authenticationManager);
                        return fsi;
                    }
                });
        /**
         *  请求认证管理
         */
        http.authorizeRequests()
                .antMatchers("url匹配路径").permitAll()          //url匹配路径 permitAll 运行 全部访问 不用认证
                .accessDecisionManager(null)                                 //访问决策器
                .filterSecurityInterceptorOncePerRequest(true)               //过滤每个请求一次的安全拦截器 ???
                .anyRequest().authenticated()                                //其他的请求 需要认证,
                .antMatchers("/admin/**").hasRole("ADMIN")      //url匹配路径  具有怎样的角色
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")   //url匹配路径    具有怎样的角色 或者是权限
        ;
        /**
         *  anonymous
         *
         *  匿名访问时  存在默认 用户名  annonymousUser
         */
        http.anonymous().disable().csrf().disable();                         //禁止匿名  关闭csrf
        /**
         * 登出操作管理
         */
        http.logout()                                                        //登出处理
                .logoutUrl("/my/logout")
                .logoutSuccessUrl("/my/index")
                .logoutSuccessHandler(null)
                .invalidateHttpSession(true)
                .addLogoutHandler(null)
                .deleteCookies("cookieNamesToClear")
        ;
        /**
         *  session  会话管理
         */
        http.sessionManagement()                                            //session管理
                .maximumSessions(2)                                         //最大session 数量 --用户
                .maxSessionsPreventsLogin(false)                            //超过最大sessin数量后时候阻止登录
                .expiredUrl("/")                                            //会话失效后跳转的url
                .expiredSessionStrategy(null)                               //自定义session 过期错略
                .sessionRegistry(null)                                     //自定义的session 注册 表
        ;
 
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         *   基础的配置
         */
        auth
                /**
                 * 认证 时触发的事件
                 */
                .authenticationEventPublisher(null)
                /**
                 *  用户细节服务
                 *
                 *  认证管理器数据的来源 吧  用户身份凭证信息和 权限信息
                 */
                .userDetailsService(null)
                /**
                 *  密码编辑器 对密码进行加密
                 */
                .passwordEncoder(null)
        ;
    }
 
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Override
    public void configure(WebSecurity web) throws Exception {
        /**
         * 不进行拦截的mvc
         */
        web.ignoring().mvcMatchers();
        /**
         * 添加自定义的 安全过滤器
         */
        web.addSecurityFilterChainBuilder(null);
    }
}

12、综合案例

前期准备

准备数据库

前提:(6张表)

用户、角色(一组权限的统称)、权限(能不能干某件事)三者之间存在多对多的关系

多个角色有多个权限、一个权限隶属于多个角色;

用户可以有多个角色,一个角色可以被多个用户所拥有;

用户可以有多个权限,一个权限可以被多个用户所拥有.

# 权限管理
show databases ;
# 创建数据库
create database powers;
use powers;

# 创建表
create table user
(
    id int primary key auto_increment COMMENT '用户',
    username varchar(20) not null unique COMMENT '姓名',
    password varchar(100) not null COMMENT '密码',
    nickname varchar(20) default '用户' COMMENT '昵称',
    state int default 1 COMMENT '状态'
)COMMENT='用户表';

# 权限表
create table power
(
    id int primary key auto_increment COMMENT '权限ID',
    powername varchar(20) not null unique COMMENT '权限名称',
    powerurl varchar(100) COMMENT '权限URL'
)COMMENT='权限表';


# 角色表
create table role
(
    id int(11) primary key auto_increment COMMENT '角色ID',
    rolename varchar(20) not null unique COMMENT '角色名称'
) COMMENT='角色表';

# 用户-权限表
create table userpower
(
    id int primary key auto_increment COMMENT '用户权限ID',
    uid int COMMENT '用户ID',
    pid int COMMENT '权限ID',
    foreign key(uid) references user(id),
    foreign key(pid) references power(id)
) COMMENT='用户-权限表';
# 角色-权限表
create table rolepower
(
    id int primary key auto_increment COMMENT '角色权限ID',
    rid int  COMMENT '角色ID',
    pid int COMMENT '权限ID',
    foreign key(rid) references role(id),
    foreign key(pid) references power(id)
) COMMENT='角色-权限表';
# 用户-角色表
create table userrole
(
    id int primary key auto_increment COMMENT '用户角色ID',
    rid int  COMMENT '角色ID',
    uid int COMMENT '用户ID',
    foreign key(rid) references role(id),
    foreign key(uid) references user(id)
) COMMENT='用户-角色表';

# 给用户表录入测试数据
insert into user values(null,'admin','123456','管理员',1);
insert into user values(null,'zhangsan','123456','张三',1);
insert into user values(null,'lisi','123456','李四',1);
insert into user values(null,'wangwu','123456','王五',1);

# 给权限表录入测试数据
insert into power values(null,'图书查询','book/find');
insert into power values(null,'图书修改','book/update');
insert into power values(null,'图书添加','book/save');
insert into power values(null,'图书删除','book/delete');

insert into power values(null,'用户查询','user/find');
insert into power values(null,'用户修改','user/update');
insert into power values(null,'用户添加','user/save');
insert into power values(null,'用户删除','user/delete');

insert into power values(null,'订单查询','order/find');
insert into power values(null,'订单修改','order/update');
insert into power values(null,'订单添加','order/save');
insert into power values(null,'订单删除','order/delete');

# 给角色表录入测试数据
insert into role values(null,'图书管理');
insert into role values(null,'用户管理');
insert into role values(null,'订单管理');
insert into role values(null,'管理员');

# 给角色-权限表录入测试数据
insert into rolepower values(null,4,1);
insert into rolepower values(null,4,2);
insert into rolepower values(null,4,3);
insert into rolepower values(null,4,4);
insert into rolepower values(null,4,5);
insert into rolepower values(null,4,6);
insert into rolepower values(null,4,7);
insert into rolepower values(null,4,8);
insert into rolepower values(null,4,9);
insert into rolepower values(null,4,10);
insert into rolepower values(null,4,11);
insert into rolepower values(null,4,12);

insert into rolepower values(null,1,9);
insert into rolepower values(null,1,10);
insert into rolepower values(null,1,11);
insert into rolepower values(null,1,12);

insert into rolepower values(null,2,1);
insert into rolepower values(null,2,2);
insert into rolepower values(null,2,3);
insert into rolepower values(null,2,4);

insert into rolepower values(null,3,5);
insert into rolepower values(null,3,6);
insert into rolepower values(null,3,7);
insert into rolepower values(null,3,8);

# 给用户-角色表录入测试数据
insert into userrole values(null,4,1);
insert into userrole values(null,1,2);
insert into userrole values(null,2,3);
insert into userrole values(null,3,3);

# 给用户-权限表录入测试数据
insert into userpower values(null,2,2);
insert into userpower values(null,2,3);
insert into userpower values(null,3,2);
insert into userpower values(null,2,10);

select * from userpower;

select * from user;

select * from role;

select * from power;

注:在SQL语句中,COMMENT关键字用于为某个字段或表添加注释,目的是为了更好地理解和维护数据库中的数据,使其更易于使用和管理。

后端部分

搭建项目

在这里插入图片描述

导入依赖
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.32</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring_boot_commons</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>
application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/powers # 数据库连接地址
    username: root # 登录数据库的用户名
    password: root # 登录数据库的密码
    driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动的类名
    type: com.alibaba.druid.pool.DruidDataSource # 数据库连接池的类型
  redis:
    host: 192.168.65.3 # Redis的主机地址
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志实现方式
    map-underscore-to-camel-case: false # 是否将下划线转换为驼峰命名

查询权限

bean包

Power

@Data
public class Power implements Serializable {
    @TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
    private Integer id ; // 主键
    private String powerName; // 权力名称
    private String powerUrl; // 权力地址
}

Role

@Data
public class Role implements Serializable {
    @TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
    private Integer id; // 主键
    private String roleName; // 角色名称
    @TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
    private Set<Power> powers; // 权力集合
}

SecurityUser

@Data
public class SecurityUser implements UserDetails {

    private User user; // 用户信息实体
    private List<String> powers; // 权力列表

    @Override // 实现 UserDetails 接口的 getAuthorities() 方法
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> list = new ArrayList<>(); // 创建一个 Collection 对象
        powers.forEach((e) -> list.add(new SimpleGrantedAuthority(e))); // 遍历权力列表,将每个权力转化为一个 GrantedAuthority 对象,并添加到 Collection 对象中
        return list; // 返回 Collection 对象
    }

    @Override // 实现 UserDetails 接口的 getPassword() 方法
    public String getPassword() {
        return user.getPassword(); // 返回用户的密码
    }

    @Override // 实现 UserDetails 接口的 getUsername() 方法
    public String getUsername() {
        return user.getUsername(); // 返回用户的用户名
    }

    @Override // 实现 UserDetails 接口的 isAccountNonExpired() 方法
    public boolean isAccountNonExpired() {
        return true; // 返回 true,表示用户的账号未过期
    }

    @Override // 实现 UserDetails 接口的 isAccountNonLocked() 方法
    public boolean isAccountNonLocked() {
        return true; // 返回 true,表示用户的账号未被锁定
    }

    @Override // 实现 UserDetails 接口的 isCredentialsNonExpired() 方法
    public boolean isCredentialsNonExpired() {
        return true; // 返回 true,表示用户的凭证未过期
    }

    @Override // 实现 UserDetails 接口的 isEnabled() 方法
    public boolean isEnabled() {
        return true; // 返回 true,表示用户已启用
    }
}

User

@Data // 自动为该类生成getter、setter、equals、hashcode等方法
public class User implements Serializable {
    @TableId(type = IdType.AUTO) // 表示该字段是数据库中的主键,类型为自增长
    private Integer id; // 用户ID
    private String username; // 用户名
    private String password; // 密码
    private String nickname; // 昵称
    private Integer state; // 状态,0表示禁用,1表示启用
    @TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
    private Set<Power> powers; // 权力集合
    @TableField(exist = false) // 表示该字段在数据库中不存在,为true时,表示该字段为null,为false时,表示该字段存在但不显示在控制台输出中
    private Set<Role> roles; // 角色集合
}
mapper包

UserMapper

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

PowerMapper

@Mapper
public interface PowerMapper extends BaseMapper<Power> {
    @Select("select * from power where id in" +
            "(select pid from userpower where uid=#{id}) " +
            "or id in(select pid from rolepower where rid in " +
            "(select rid from userrole where uid=#{id}))")
    List<Power> findPowerByUserId(int id);
}	
service包

UserService

public interface UserService extends IService<User> {
}

impl——>UserServiceImpl

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private PowerMapper powerMapper;
    @Resource
    private PasswordEncoder bCryptPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = userMapper.selectOne(wrapper);
        if (user == null) {
            throw new UsernameNotFoundException("账号不存在!");
        }
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        SecurityUser securityUser = new SecurityUser();
        securityUser.setUser(user);
        // 获取当前用户的所有权限
        List<Power> powers = powerMapper.findPowerByUserId(user.getId());
        List<String> powerList = new ArrayList<>();
        powers.forEach((e) -> powerList.add(e.getPowerName()));
        securityUser.setPowers(powerList);
        log.info("用户的所有权限信息:{}", powerList);
        return securityUser;
    }
}
filter包

LoginTokenFilter

@Slf4j
public class LoginTokenFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    private StringRedisTemplate stringRedisTemplate;

    private PasswordEncoder passwordEncoder;

    public LoginTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
        this.authenticationManager = authenticationManager;
        this.stringRedisTemplate = stringRedisTemplate;
        this.passwordEncoder = passwordEncoder;
        // 关闭登录只允许 post
        this.setPostOnly(false);
        // 设置登陆路径,并且post请求
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("开始用户登陆操作");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String encoderPassword = passwordEncoder.encode(password);
        log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("登陆成功");
        // 1. 生成jwt字符串
        SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
        User commonsUser = new User();
        commonsUser.setId(user.getUser().getId());
        commonsUser.setNickname(user.getUser().getNickname());
        commonsUser.setUsername(user.getUser().getUsername());
        String token = JwtConfig.getJwtToken(commonsUser);
        // 2. 保存到redis
        stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
        // 3. 返回给前端token
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.success(token)));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("账号或者密码错误");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
    }
}
config包

SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .and().csrf().disable() // 关闭 csrf 跨域请求
                .cors().and()    // security允许跨域
                .formLogin()
                .loginProcessingUrl("/login")  // 设定登录请求接口
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll()
                .and()
                .authorizeRequests() // 请求设置
                .antMatchers("/login").permitAll() // 配置不需要认证的接口
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .addFilter(new LoginTokenFilter(authenticationManager(), stringRedisTemplate, passwordEncoder))  // 配置登陆filter
                .httpBasic();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
}
启动类
@SpringBootApplication
public class SpringBootSecurity3Application {

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurity3Application.class, args);
    }
}
测试结果

1、输入错误

image-20230815000800364

image-20230815000854467

2、输入正确

image-20230815003312772

image-20230815003351091

image-20230815003445633

判断权限

controller包

BookController

@Slf4j
@RestController
@RequestMapping("/book")
public class BookController {

    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('图书查询')")
    public JsonResult find() {
        log.info("执行了查询操作");
        return ResultTool.success("图书查询");
    }

    @PutMapping("/update")
    @PreAuthorize("hasAnyAuthority('图书修改')")
    public JsonResult update() {
        log.info("执行了修改操作");
        return ResultTool.success("图书修改");
    }

    @PostMapping("/save")
    @PreAuthorize("hasAnyAuthority('图书添加')")
    public JsonResult save() {
        log.info("执行了添加操作");
        return ResultTool.success("图书添加");
    }

    @DeleteMapping("/delete")
    @PreAuthorize("hasAnyAuthority('图书删除')")
    public JsonResult delete() {
        log.info("执行了删除操作");
        return ResultTool.success("图书删除");
    }
}

OrderController

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {


    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('订单查询')")
    public JsonResult find() {
        log.info("执行了查询操作");
        return ResultTool.success("订单查询");
    }

    @PutMapping("/update")
    @PreAuthorize("hasAnyAuthority('订单修改')")
    public JsonResult update() {
        log.info("执行了修改操作");
        return ResultTool.success("订单修改");
    }

    @PostMapping("/save")
    @PreAuthorize("hasAnyAuthority('订单添加')")
    public JsonResult save() {
        log.info("执行了添加操作");
        return ResultTool.success("订单添加");
    }

    @DeleteMapping("/delete")
    @PreAuthorize("hasAnyAuthority('订单删除')")
    public JsonResult delete() {
        log.info("执行了删除操作");
        return ResultTool.success("订单删除");
    }
}

UserController

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('用户查询')")
    public JsonResult find() {
        log.info("执行了查询操作");
        return ResultTool.success("用户查询");
    }

    @PutMapping("/update")
    @PreAuthorize("hasAnyAuthority('用户修改')")
    public JsonResult update() {
        log.info("执行了修改操作");
        return ResultTool.success("用户修改");
    }

    @PostMapping("/save")
    @PreAuthorize("hasAnyAuthority('用户添加')")
    public JsonResult save() {
        log.info("执行了添加操作");
        return ResultTool.success("用户添加");
    }

    @DeleteMapping("/delete")
    @PreAuthorize("hasAnyAuthority('用户删除')")
    public JsonResult delete() {
        log.info("执行了删除操作");
        return ResultTool.success("用户删除");
    }
}
filter包

IsLoginFilter

@Slf4j
public class IsLoginFilter extends BasicAuthenticationFilter {

    private StringRedisTemplate stringRedisTemplate;

    public IsLoginFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) {
        super(authenticationManager);
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("开始校验用户是否登陆");
        // 1. 获取到前端发送过来的token
        String token = request.getHeader("token");
        log.info("前端发送过来的token:{}", token);
        // 2. 校验token
        // 2.1 是否发送
        if (token != null) {
            // 2.2 是否合法
            boolean flag = JwtConfig.checkToken(token);
            log.info("是否合法:{}", flag);
            if (flag) {
                Claims claims = JwtConfig.parseJWT(token);
                String id = claims.get("id").toString();
                String redisToken = stringRedisTemplate.opsForValue().get("TOKEN:" + id);
                log.info("redis的token:{}", redisToken);
                // 2.3 是否篡改
                if (token.equals(redisToken)) {
                    // 3. 获取权限 从redis中获取权限
                    String s = stringRedisTemplate.opsForValue().get("POWER:" + id);
                    List<String> list = JSONArray.parseArray(s, String.class);
                    List<GrantedAuthority> auths = new ArrayList<>();
                    list.forEach((e) -> {
                        GrantedAuthority authority = new SimpleGrantedAuthority(e);
                        auths.add(authority);
                    });
                    log.info("成功,下方权限!");
                    // 4. 下方权限
                    UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(id, token, auths);
                    // 有权限,则放入权限上下文中
                    SecurityContextHolder.getContext().setAuthentication(upat);
                }
            }
        } else {
            // 清空操作
            SecurityContextHolder.getContext().setAuthentication(null);
        }
        chain.doFilter(request, response);
    }
}

LoginTokenFilter

@Slf4j
public class LoginTokenFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    private StringRedisTemplate stringRedisTemplate;

    private PasswordEncoder passwordEncoder;

    public LoginTokenFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate, PasswordEncoder passwordEncoder) {
        this.authenticationManager = authenticationManager;
        this.stringRedisTemplate = stringRedisTemplate;
        this.passwordEncoder = passwordEncoder;
        // 关闭登录只允许 post
        this.setPostOnly(false);
        // 设置登陆路径,并且post请求
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("开始用户登陆操作");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String encoderPassword = passwordEncoder.encode(password);
        log.info("账号:{},密码:{},加密后密码:{}", username, password, encoderPassword);
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("登陆成功");
        // 1. 生成jwt字符串
        SecurityUser user = (SecurityUser) authResult.getPrincipal(); // 获取到登陆成功后保存的user
        User commonsUser = new User();
        commonsUser.setId(user.getUser().getId());
        commonsUser.setNickname(user.getUser().getNickname());
        commonsUser.setUsername(user.getUser().getUsername());
        String token = JwtConfig.getJwtToken(commonsUser);
        // 2. 保存到redis
        //存储令牌
        stringRedisTemplate.opsForValue().set("TOKEN:" + user.getUser().getId(), token, 1, TimeUnit.DAYS);
        //存储权限
        stringRedisTemplate.opsForValue().set("POWER:"+user.getUser().getId(),JSONArray.toJSONString(user.getPowers()),1, TimeUnit.DAYS);
        // 3. 返回给前端token
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.success(token)));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("账号或者密码错误");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("账号或者密码错误")));
    }
}

1.登陆成功后把权限信息存到redis中

image-20230815114511266

//存储权限     stringRedisTemplate.opsForValue().set("POWER:"+user.getUser().getId(),JSONArray.toJSONString(user.getPowers()),1, TimeUnit.DAYS);

2.从redis中获取权限

image-20230815114741763

// 3. 获取权限 从redis中获取权限
 String s = stringRedisTemplate.opsForValue().get("POWER:" + id);
 List<String> list = JSONArray.parseArray(s, String.class);
config

SecurityConfig

image-20230815115141302

.addFilter(new IsLoginFilter(authenticationManager(),stringRedisTemplate))
handler包

NotAccessHandler

@Component
@Slf4j
public class NotAccessHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.info("用户没有登录");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail("资源无法访问!")));
    }
}

再给SecurityConfig加如下内容

image-20230815115814190

 @Resource
    private NotAccessHandler notAccessHandler;

.authenticationEntryPoint(notAccessHandler)// 未登录 handler
测试结果

1、先登录

image-20230815101052658

2、携带token访问成功

image-20230815101123538

3、控制台信息

image-20230815101156355

4、测试没有用户查询的权限

image-20230815120415882

image-20230815120926812

image-20230815121013809

操作权限

SecurityConfig

放行前端部分

image-20230815150418225

.antMatchers("/login", "/login.html", "/", "/index.html", "/assort.html", "/js/*", "/img/*", "/css/*").permitAll() // 配置不需要认证的接口
前端部分

导入js包

image-20230815150749060

assort.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!-- 引入elementUI样式 -->
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <style>
    .el-header {
      background-color: #B3C0D1;
      color: #333;
      line-height: 60px;
    }

    .el-aside {
      color: #333;
    }
  </style>
</head>
<body>
<div id="app">
  <el-container style="height: 800px; border: 1px solid #eee">
    <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
      <el-menu :default-openeds="['1', '3']">
        <el-submenu index="1">
          <template slot="title"><i class="el-icon-message"></i>用户管理</template>
          <el-menu-item-group>
            <el-menu-item index="1-1">用户查询</el-menu-item>
            <el-menu-item index="1-2">用户添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
          <el-menu-item-group>
            <el-menu-item index="2-1">分类查询</el-menu-item>
            <el-menu-item index="2-2">分类添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="3">
          <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
          <el-menu-item-group>
            <el-menu-item index="3-1">图书查询</el-menu-item>
            <el-menu-item index="3-2">图书添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
      </el-menu>
    </el-aside>

    <el-container>
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>查看</el-dropdown-item>
            <el-dropdown-item>新增</el-dropdown-item>
            <el-dropdown-item>删除</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <span>王小虎</span>
      </el-header>

      <el-main>
        {{message}}
        <el-table :data="tableData">
          <el-table-column prop="id" label="编号" width="60">
          </el-table-column>
          <el-table-column prop="name" label="分类名" width="120">
          </el-table-column>
          <el-table-column prop="desc" label="备注" width="190">
          </el-table-column>
          <el-table-column prop="state" label="状态" width="60">
          </el-table-column>
          <el-table-column label="操作">
            <template slot-scope="scope">
              <el-button
                      size="mini" @click="showUpdateDialog(scope.row)">编辑
              </el-button>
              <el-button
                      size="mini"
                      type="danger" @click="deleteAssort(scope.row)">删除
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-main>
    </el-container>
  </el-container>

  <!--修改的模态框-->
  <el-dialog title="编辑分类" :visible.sync="showUpdateAssortDialog">
    {{errorMsg}}
    <el-form :model="updateForm">
      <el-form-item label="分类名称" :label-width="formLabelWidth">
        <el-input v-model="updateForm.name" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="备注信息" :label-width="formLabelWidth">
        <el-input v-model="updateForm.desc" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="showUpdateAssortDialog=false">取 消</el-button>
      <el-button type="primary" @click="updateAssort">确 定</el-button>
    </div>
  </el-dialog>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
  new Vue({
    el: '#app',
    data() {
      return {
        //这里定义变量
        tableData: [
          {
            id: 1,
            name: 'aa',
            desc: 'bb',
            state: 1
          }
        ],
        showUpdateAssortDialog: false,
        formLabelWidth: '120px',
        updateForm: {
          id: '',
          name: '',
          desc: ''
        },
        isExist: false,
        errorMsg: '',
        message: ''
      }
    },
    methods: {
      loadAssortData() {
        let _this = this
        // 从后端获取到数据,把数据给tableData赋值
        axios.get('/user/find',{
          headers:{
            token:window.localStorage.getItem('token')
          }
        }).then((response) => {
          // 获取到后端返回的数据
          let d = response.data
          if (d.success) {
            _this.message = d.data
          } else {
            alert(response.data.error);
            _this.message = d.error
          }
          //_this.tableData = d.data
        })
      },
      deleteAssort(row) {
        let _this = this
        this.$confirm('此操作将永久删除该分类, 是否继续?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          // 点击确定了 调用后端的删除操作并传递id,成功后刷新页面
          // axios.get('assort/delete?id='+row.id)
          axios.get('/user/delete', {
            params: {
              id: row.id
            },
            headers:{
              token:window.localStorage.getItem('token')
            }
          }).then((response) => {
            if (response.data.success) {
              this.$message({
                type: 'success',
                message: '已成功删除'
              });
              _this.message = response.data.data
            } else {
              alert(response.data.error);
              _this.message = response.data.error
            }
            //_this.tableData = response.data.data
            //console.log(_this.tableData)
            //_this.loadAssortData()

          })
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消删除'
          });
        });
      },
      showUpdateDialog(row) {
        this.updateForm.name = row.name
        this.updateForm.desc = row.desc
        this.updateForm.id = row.id
        this.showUpdateAssortDialog = true
      },
      updateAssort() {
        this.isExist = true
        if (this.isExist) {
          let _this = this
          axios.get('/user/update',{
            headers:{
              token:window.localStorage.getItem('token')
            }
          }).then((response) => {
            if (response.data.success) {
              this.$message({
                type: 'success',
                message: '已成功修改'
              });
              _this.message = response.data.data
            } else {
              alert(response.data.error);
              _this.message = response.data.error
            }
            //_this.loadAssortData()
            _this.showUpdateAssortDialog = false

          })
        }
      },
      checkNameIsExist() {
        // 获取到名称
        let name = this.updateForm.name
        let _this = this
        // 发送给后端
        axios.get('assort/exist', {
          params: {
            name: name
          }
        }).then((response) => {
          let success = response.data.success
          if (success) {
            _this.isExist = true
            _this.errorMsg = ''
          } else {
            _this.isExist = false
            _this.errorMsg = response.data.error
          }
        })
      }
    },
    created() {
      // 页面加载后执行
      this.loadAssortData()
    }
  })
</script>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!-- 引入elementUI样式 -->
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <style>
    .el-header {
      background-color: #B3C0D1;
      color: #333;
      line-height: 60px;
    }

    .el-aside {
      color: #333;
    }
  </style>
</head>
<body>
<div id="app">
  <el-container style="height: 800px; border: 1px solid #eee">
    <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
      <el-menu :default-openeds="['1', '3']">
        <el-submenu index="1">
          <template slot="title"><i class="el-icon-message"></i>用户管理</template>
          <el-menu-item-group>
            <el-menu-item index="1-1">用户查询</el-menu-item>
            <el-menu-item index="1-2">用户添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
          <el-menu-item-group>
            <el-menu-item index="2-1">分类查询</el-menu-item>
            <el-menu-item index="2-2">分类添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="3">
          <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
          <el-menu-item-group>
            <el-menu-item index="3-1">图书查询</el-menu-item>
            <el-menu-item index="3-2">图书添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
      </el-menu>
    </el-aside>

    <el-container>
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>查看</el-dropdown-item>
            <el-dropdown-item>新增</el-dropdown-item>
            <el-dropdown-item>删除</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <span>王小虎</span>
      </el-header>

      <el-main>
        <img src="img/background.webp"/>
      </el-main>
    </el-container>
  </el-container>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>
  new Vue({
    el: '#app'
  })
</script>

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    {{error}}<br/>
    账号:<input type="text" v-model="username"/><br/>
    密码:<input type="password" v-model="password"/><br/>
    <button @click="login">登陆</button>
</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                username: '',
                password: '',
                error: ''
            }
        },
        methods: {
            login() {
                let data = new URLSearchParams()
                data.append('username', this.username)
                data.append('password', this.password)
                let _this = this
                axios({
                    method: 'post',
                    url: '/login',
                    data: data
                }).then((response) => {
                    if (response.data.success) {
                        window.localStorage.setItem('token', response.data.data)
                        location.href = 'assort.html';
                    } else {
                        _this.error = response.data.error
                    }
                })
            }
        }
    })
</script>
heandler包

Fail403Handler

@Component
public class Fail403Handler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.println(JSONArray.toJSONString(ResultTool.fail( "资源无法访问!")));
    }
}

SecurityConfig

image-20230815152153615

 @Resource
    private Fail403Handler fail403Handler;
    
 .accessDeniedHandler(fail403Handler)
测试结果

出现问题

image-20230815153558439

将UserController里面修改为

image-20230815153959176

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('用户查询')")
    public JsonResult find() {
        log.info("执行了查询操作");
        return ResultTool.success("用户查询");
    }

    @GetMapping("/update")
    @PreAuthorize("hasAnyAuthority('用户修改')")
    public JsonResult update() {
        log.info("执行了修改操作");
        return ResultTool.success("用户修改");
    }

    @GetMapping("/save")
    @PreAuthorize("hasAnyAuthority('用户添加')")
    public JsonResult save() {
        log.info("执行了添加操作");
        return ResultTool.success("用户添加");
    }

    @GetMapping("/delete")
    @PreAuthorize("hasAnyAuthority('用户删除')")
    public JsonResult delete() {
        log.info("执行了删除操作");
        return ResultTool.success("用户删除");
    }
}

再次测试

点击编辑

image-20230815154331850

点击删除

image-20230815154453039

控制台

image-20230815154630764

修改权限1

为数据添加数据

image-20230815192525772

insert into power values (null,'权限查询','power/find');
insert into power values (null,'权限修改','power/update');

image-20230815192744457

insert into rolepower values(null,4,13);
insert into rolepower values(null,4,14);

image-20230815193642022

UserController

image-20230815193826303

  @Resource
    private UserService userService;

    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('用户查询')")
    public JsonResult find() {
        log.info("执行了查询操作");
        return ResultTool.success(userService.list());
    }

给公共模块的ResuTool添加,NotAccessHandler和Fail403Handler修改

image-20230815200257219

 public static JsonResult fail(int code,String msg) {
        return new JsonResult(false, msg, code, null);
    }
    
    
out.println(JSONArray.toJSONString(ResultTool.fail(401,"用户没有登陆!")));


 out.println(JSONArray.toJSONString(ResultTool.fail(403, "资源无法访问!")));

前端部分

image-20230815200636205

测试1

页面效果:

image-20230815201007435

控制台输出:

image-20230815201132214

再加一个按钮,进行权限操作,跳转到另一个页面,可以编辑其他用户的权限问题(当点击按钮的时候,会把该用户的权限显示出来,但不能修改自己权限,加个if判断就可以)

image-20230815202833028

 <el-button
             size="mini"
             type="danger" @click="showPower(scope.row.id)">权限操作
 </el-button>
showPower(uid) {
              window.sessionStorage.setItem('UID', uid)
              location.href = '/power.html'
 },

再写一个power.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!-- 引入elementUI样式 -->
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <style>
    .el-header {
      background-color: #B3C0D1;
      color: #333;
      line-height: 60px;
    }

    .el-aside {
      color: #333;
    }
  </style>
</head>
<body>
<div id="app">
  <el-container style="height: 800px; border: 1px solid #eee">
    <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
      <el-menu :default-openeds="['1', '3']">
        <el-submenu index="1">
          <template slot="title"><i class="el-icon-message"></i>用户管理</template>
          <el-menu-item-group>
            <el-menu-item index="1-1">用户查询</el-menu-item>
            <el-menu-item index="1-2">用户添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
          <el-menu-item-group>
            <el-menu-item index="2-1">分类查询</el-menu-item>
            <el-menu-item index="2-2">分类添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
        <el-submenu index="3">
          <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
          <el-menu-item-group>
            <el-menu-item index="3-1">图书查询</el-menu-item>
            <el-menu-item index="3-2">图书添加</el-menu-item>
          </el-menu-item-group>
        </el-submenu>
      </el-menu>
    </el-aside>

    <el-container>
      <el-header style="text-align: right; font-size: 12px">
        <el-dropdown>
          <i class="el-icon-setting" style="margin-right: 15px"></i>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item>查看</el-dropdown-item>
            <el-dropdown-item>新增</el-dropdown-item>
            <el-dropdown-item>删除</el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
        <span>王小虎</span>
      </el-header>

      <el-main>
        {{powers}}
      </el-main>
    </el-container>
  </el-container>

</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
  new Vue({
    el: '#app',
    data() {
      return {
        uid: 0,
        powers: []
      }
    },
    methods: {
      loadUserPowerById() {
        console.log(window.localStorage.getItem('token'))
        let _this = this
        axios.get('/power/find', {
          params: {
            id: _this.uid
          },
          headers: {
            token: window.localStorage.getItem('token')
          }
        }).then((response) => {
          if (response.data.success) {
            _this.powers = response.data.data
          } else {
            console.log(response.data.error)
          }
        })
      }
    },
    created() {
      this.uid = window.sessionStorage.getItem('UID')
      if (this.uid === undefined || this.uid === 0) {
        location.href = 'assort.html'
      }
      this.loadUserPowerById()
    }
  })
</script>

PowerService

public interface PowerService extends IService<Power> {

    JsonResult findPowerByUserId(int id);

}

PowerServiceImpl

@Service
public class PowerServiceImpl extends ServiceImpl<PowerMapper, Power> implements PowerService {
    @Resource
    private PowerMapper powerMapper;


    @Override
    public JsonResult findPowerByUserId(int id) {
        return ResultTool.success(powerMapper.findPowerByUserId(id));
    }
}

PowerController

@Slf4j
@RestController
@RequestMapping("/power")
public class PowerController {
    @Resource
    private PowerService service;

    @GetMapping("/find")
    @PreAuthorize("hasAnyAuthority('权限查询')")
    public JsonResult find(int id) {
        return service.findPowerByUserId(id);
    }
}

SecurityConfig

.antMatchers("/login", "/login.html", "/", "/index.html", "/assort.html","/power.html", "/js/*", "/img/*", "/css/*").permitAll() // 配置不需要认证的接口 
测试2

image-20230815210431328

修改权限2

修改power.html

image-20230815211018082

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 引入elementUI样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <style>
        .el-header {
            background-color: #B3C0D1;
            color: #333;
            line-height: 60px;
        }

        .el-aside {
            color: #333;
        }
    </style>
</head>
<body>
<div id="app">
    <el-container style="height: 800px; border: 1px solid #eee">
        <el-aside width="200px" style="background-color: rgb(238, 241, 246)">
            <el-menu :default-openeds="['1', '3']">
                <el-submenu index="1">
                    <template slot="title"><i class="el-icon-message"></i>用户管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="1-1">用户查询</el-menu-item>
                        <el-menu-item index="1-2">用户添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="2">
                    <template slot="title"><i class="el-icon-menu"></i>分类管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="2-1">分类查询</el-menu-item>
                        <el-menu-item index="2-2">分类添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
                <el-submenu index="3">
                    <template slot="title"><i class="el-icon-setting"></i>图书管理</template>
                    <el-menu-item-group>
                        <el-menu-item index="3-1">图书查询</el-menu-item>
                        <el-menu-item index="3-2">图书添加</el-menu-item>
                    </el-menu-item-group>
                </el-submenu>
            </el-menu>
        </el-aside>

        <el-container>
            <el-header style="text-align: right; font-size: 12px">
                <el-dropdown>
                    <i class="el-icon-setting" style="margin-right: 15px"></i>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>查看</el-dropdown-item>
                        <el-dropdown-item>新增</el-dropdown-item>
                        <el-dropdown-item>删除</el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
                <span>王小虎</span>
            </el-header>

            <el-main>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item><a href="/assort.html">用户管理</a></el-breadcrumb-item>
                    <el-breadcrumb-item><a href="/power.html">权限管理</a></el-breadcrumb-item>
                </el-breadcrumb>
                <h3>你的权限</h3>
                <span v-for="p in allPowers">
                    <input type="checkbox" :value="p.id" v-model="checkedPower"/>
                    <label>{{p.powerName}}</label>
                </span>
                <br/>
                <button @click="changePower">修改权限</button>
            </el-main>
        </el-container>
    </el-container>

</div>
</body>
</html>
<!--引入vue-->
<script src="js/vue.js"></script>
<!--引入axios-->
<script src="js/axios.min.js"></script>
<!-- 引入elementUI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                uid: 0,
                allPowers: [],  // 所有的权限
                powers: [],   // 我的权限
                checkedPower: []  //被选中的权限  id
            }
        },
        methods: {
            loadAllPowers() {
                let _this = this
                axios.get('/power/findall', {
                    headers: {
                        token: window.localStorage.getItem('token')
                    }
                }).then((response) => {
                    if (response.data.success) {
                        _this.allPowers = response.data.data
                    } else {
                        console.log(response.data.error)
                    }
                })
            },
            loadUserPowerById() {
                console.log(window.localStorage.getItem('token'))
                let _this = this
                axios.get('/power/find', {
                    params: {
                        id: _this.uid
                    },
                    headers: {
                        token: window.localStorage.getItem('token')
                    }
                }).then((response) => {
                    if (response.data.success) {
                        _this.powers = response.data.data
                        for (let i = 0; i < _this.powers.length; i++) {
                            _this.checkedPower[i] = _this.powers[i].id;
                        }
                    } else {
                        console.log(response.data.error)
                    }
                })
                console.log(_this.powers);
                console.log(_this.checkedPower);
            },
            changePower() {
                let _this = this
                let data = new URLSearchParams()
                data.append("uid", _this.uid)
                data.append("checkedPower", _this.checkedPower)
                axios({
                    method: 'put',
                    url: 'power/update',
                    data: data,
                    headers: {
                        token: window.localStorage.getItem('token')
                    }
                }).then((response) => {
                    location.href = 'power.html'
                })
            }
        },
        created() {
            let _this = this
            this.uid = window.sessionStorage.getItem('UID')
            if (this.uid === undefined || this.uid === 0) {
                location.href = 'assort.html'
            }
            // 加载我的权限
            this.loadUserPowerById()
            // 加载所有权限
            setTimeout(function () {
                _this.loadAllPowers()
            }, 1000)

        }
    })
</script>

image-20230815211507939

问题1

只是显示该用户的权限,没有显示所有权限

解决:得到所有权限,对号选中的就是该用户权限

PowerController

 //得到所有权限
    @GetMapping("/findall")
    public JsonResult find() {
        return ResultTool.success(service.list());
    }

image-20230815212254511

又会出现一个问题,复选框没有打勾

image-20230815212616208

解决:将powers的权限赋给checkedPower

image-20230815212910398

问题2

线程错乱了

解决:

image-20230815213118585

成功了

image-20230815213208969

问题3

当点击修改权限的时候怎么修改?

解决:把所有权限全删了,再添加自己的权限

PowerController

@PutMapping("/update")
    public JsonResult update(int uid, String[] checkedPower) {
        log.info("uid:{}", uid);
        log.info("checkedPower:{}", Arrays.toString(checkedPower));
        return service.updatePowerByUserId(uid, checkedPower);
    }

PowerService

public interface PowerService extends IService<Power> {

    JsonResult findPowerByUserId(int id);

    JsonResult updatePowerByUserId(int uid,String[] newPower);

}

RoleMapper

@Mapper
public interface PowerMapper extends BaseMapper<Power> {
    @Select("select * from power where id in(select pid from userpower where uid=#{id}) or id in(select pid from rolepower where rid in (select rid from userrole where uid=#{id}))")
    List<Power> findPowerByUserId(int id);

    @Delete("delete from userpower where uid=#{id}")
    void deletePowerByUserId(int id);

    @Insert("insert into userpower(id,uid,pid) values(null,#{uid},#{pid})")
    void savePower(@Param("uid") int uid, @Param("pid") String pid);
}

PowerServiceImpl

@Service
public class PowerServiceImpl extends ServiceImpl<PowerMapper, Power> implements PowerService {
    @Resource
    private PowerMapper powerMapper;
    @Resource
    private RoleMapper roleMapper;

    @Override
    public JsonResult findPowerByUserId(int id) {
        return ResultTool.success(powerMapper.findPowerByUserId(id));
    }

    @Transactional(rollbackFor = SQLException.class)
    public JsonResult updatePowerByUserId(int uid, String[] newPower) {
        // 删除原来权限
        powerMapper.deletePowerByUserId(uid);
        roleMapper.deleteRoleByUserId(uid);
        // 添加新权限
        for (int i = 0; i < newPower.length; i++) {
            String pid = newPower[i];
            powerMapper.savePower(uid, pid);
        }
        return ResultTool.success("更新成功!");
    }
}

发现修改成功

image-20230815214802988

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值