003-云E办_学习SpringSecurity

003-云E办_学习SpringSecurity

一、SrpingSecurity

1 、什么是安全框架

解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常 麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。

2、Spring Security简介

Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决 方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制 反转Inversion of Control,DI:Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系 统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作
Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控 制功能,减少了为系统安全而编写大量重复代码的工作。
使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移 植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题, 也为你提供许多其他有用的、可定制的安全功能。
正如你可能知道的两个应用程序的两个主要区域是“认证”和“授 权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。
“认证”,是建立一个他声明的主体的过程(一 个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录
“授权”,指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。

3、创建demo

在这里插入图片描述

在这里插入图片描述

1.编写代码

当我们项目创建好时,选择了spring web/security时,发现xml文件以及导入了相应的依赖。
我们选择的是2.2.5版本的父部依赖。

2.创建controller

ResponseBody返回的是字符串,不能跳转

如果加了@ResponseBody这个注解,返回的就是字符串或是json对象,跳转不了
要注意的是控制成如果使用了@RestController 那么就是@ResponseBody和@Controller合在了一块。如果真的确定不需要用到转发,重定向之类的,那么就可以使用@RestController。否则还是不用偷懒

//@RestController <--> 包含ResponseBody
@Controller
public class LoginController {

    /*
    * 登录方法:
    * */
    @RequestMapping("/login")
    public String login(){
        System.out.println("执行了登录方法");
        //重定向到主页:
        return "redirect:main.html";
    }
    //在准备两个HTML:login.html / main.html
    // 当浏览器访问“/login"时,给浏览器返回main.html
}

3.创建两个HTML文件:

在resource/static/ login–main.html
在这里插入图片描述

4.测试:

运行Application出现的界面如下:
在这里插入图片描述

  • 那为什么会出现如下界面呢?我们自己写的界面呢?
    其实这一句表明:spring security生效了。有了ssecurity以后,任何操作之前需要通过这个界面进行登录,然后才能做其他工作。

  • 如何登录呢?
    账号:默认的是user
    密码:随机密码:8689f864-bc43-4ef9-b578-7f8ef8c1bfb8
    在运行程序时,在控制台可以看到一条:Using generated security password: 8689f864-bc43-4ef9-b578-7f8ef8c1bfb8后面这个随机数字就是密码了。
    在这里插入图片描述
    上面SpringSecurity界面,输入账号(user),密码(加密),登录成功以后,就是下面咱们写的界面:

ps:登录的次数只有一次。第二次就不会跳转成功了。也就是说明过期了。
在这里插入图片描述

二、 如何自定义逻辑(重要:源码分析)

上述小demo,当我们没有配置SpringSecurity时,账户和密码都是自动生成的。而实际当中账户和密码都应该是从数据库中查询出来的。所以我们应自定义逻辑去控制认证逻辑。

下述总体概括:userDetailsService接口 返回的是UserDetails接口,被user实现。

1、UserDetailsService分析?

idea中查询类的快捷键:ctrl+n
查找接口的实现类:ctrl+h

  • 实现UserDetailsService接口:
    只有一个方法:
    UserDetails loadUserByUsername(String username);
    //通过用户名加载用户(从浏览器传过来username)
    在这里插入图片描述

  • 那返回来的UserDetails是什么?
    也是一个接口,实现了继承了序列号接口(Serializable)
    拥有的方法:
    getAuthorities():获取所有权限到这个用户。不能返回空。
    getPassword():获取密码
    getUsername():获取用户名
    isAccountNonExpired():判断用户是否过期。
    isAccontNonLocked():判断用户是否锁定
    isCredentialsNonExpried():凭证是否过期。(密码是否过期)
    isEnabled():是否可用

  • user 实现了UserDetails接口
    是springSecurity定义的实现类。
    拥有的构造函数:
    有参构造:传入(username,password,autborities权限列表){
    // username是从浏览器传过来的。去数据库查找用户,返回对应用户的相应信息(包括:密码、账户是否过期、锁定、凭证过期。)。比如拿着前端转过来的username,去数据库查找用户,拿到用户密码,再去前端输入的密码进行比较。
    this.获取。判断上述方法。
    }
    下面图片中:username是前端用户传过来的,password是:拿着username去数据库找密码,找到后给password。这个password在和前端输入的密码进行比较。

在这里插入图片描述

2、passwordEncoder详解

passwordEncoder是接口。当我们需要自定义逻辑时,就需要实现该接口。
passwordEncoder接口的方法:

  • String encode(CharSequence rawPassword);
    解释:加密,密码。好的加密方式,推荐用SHA-1算法,或者用hash算法。
    rawPassword:就是原始的密码。进行相应的加密。返回字符串。

  • boolean matches(CharSequence rawPassword, String encodedPassword);
    解释:匹配。rawPasswoed是原始密码,encodedPasswoed是加密的密码。进行判断是否一致。
    在这里插入图片描述

官方推荐用的加密算法是:BCryptPasswordEncoder。
是基于hash算法,是单向的只能加密,不能解密。
在这里插入图片描述
测试passwoedEncoder:

@SpringBootTest
class SpringsecuritydemoApplicationTests {

    @Test
    void contextLoads() {
        /**
         * passwordEncoder是接口
         * BCryptPasswordEncoder 是实现接口类
         *  encode方法是:传入一个密码值,然后进行加密,返回一个字符串
         *  mathes方法是:传入原始密码,传入加密后的面膜,返回是否相等
         *  
         *  加密后的123是:
         *      $2a$10$dKjBIPKkdjOn6xiODHsPKe45/Rsfib53aW4SROZ44kNF745gV1znS
         *  原始密码是否等于加密密码true
         */
        PasswordEncoder pe = new BCryptPasswordEncoder();
        String encode = pe.encode("123");
        System.out.println("加密后的123是:"+encode);
        boolean matches = pe.matches("123", encode);
        System.out.println("原始密码是否等于加密密码"+matches);
    }
}

在这里插入图片描述

三、自定义登录逻辑:

1、创建password实体类

1、但我们自定义登录逻辑时,容器内必须有Password实例,所以要写一个配置类。创建config/Securityconfig

/**
 * 我们自定义登录逻辑时,容器内必须有Password实例,
 * 所以要写一个配置类。创建config/Securityconfig
 *
 * 创建好了实例,那么实现UserDetailsService接口
 */

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder getPw(){
        return new BCryptPasswordEncoder();
    }
}

2、创建用户实现类

2、有了实例,就要实现userDetailsService接口。创建类:service/UserDetailsServiceImpl实现类。

/**
* pw是config/securityConfig下面的bean
* @Bean
* public PasswordEncoder getPw(){
* return new BCryptPasswordEncoder();
* }
* PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
* BCryptPasswordEncoder类下encode方法,进行加密操作
*
*
* user是实现类,实现了UserDetailsService
* new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
*
* encode:传进去进行加密!!
* 真是定义权限:具体有两个权限:一个admin、一个普通权限
*/
String password = pw.encode(“123”);
return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
(“admin,normal,ROLE_abc,/main.html,/insert,/delete”));
// abc的角色,增加删除的权限

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder pw;
    /**
     * 实现接口,那么就需要实现方法:
     * 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
        if (!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
        // 或者直接把密码放入构造方法中。
        //既然是数据库查询出来的,肯定是加密的密码。

        /**
         * pw是config/securityConfig下面的bean
         * @Bean
         * public PasswordEncoder getPw(){
         *    return new BCryptPasswordEncoder();
         * }
         * PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
         * BCryptPasswordEncoder类下encode方法,进行加密操作
         *
         *
         * user是实现类,实现了UserDetailsService
         *  new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
         *
         *  encode:传进去进行加密!!
         *  真是定义权限:具体有两个权限:一个admin、一个普通权限
         */
        String password = pw.encode("123");
        return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
                        ("admin,normal,ROLE_abc,/main.html,/insert,/delete"));
        // abc的角色,增加删除的权限
    }
}

3、测试

3、启动项目测试:
localhost:8080/login.html
启动项目后第一次访问这个地址,会自动去security进行控制。如果账号密码成功后。第二次访问这个地址,就直接进入该html文件。
PS:这个不会跳转到controller层!!!
在这里插入图片描述
在这里插入图片描述

四、自定义登录页面

上面测试,依然运用了Security自带的页面,但是实际项目都是用自己的页面。

1、初步实现自定义登录页面

修改配置类:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //重写对应的方法:
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交认证. 指定的页面
        http.formLogin().loginPage("/login.html");
    }
    @Bean
    public PasswordEncoder getPw(){
        return new BCryptPasswordEncoder();
    }
}

2、测试:

访问localhost:8080/login.html会发现直接进入了该页面,并没有出现跳转到security自带页面了。
发现问题:我们/login… /main.html 都可以自由访问。这样失去了权限的意义,如何修改呢???

3、真正实现自定义登录页面

修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure方法。
Securityconfig :

/**
 * 我们自定义登录逻辑时,容器内必须有Password实例,
 * 所以要写一个配置类。创建config/Securityconfig
 *
 * 创建好了实例,那么实现UserDetailsService接口
 *
 * extend WebSecurityConfigurerAdapter
 */

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //重写对应的方法:
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交认证.
        http.formLogin()
                //自定义登录页面
                .loginPage("/login.html")
                //当发现/login时认为是登录,必须和表单提交的地址是一样的
                //去执行UserServiceImpl
                .loginProcessingUrl("/login")
                //登录成功后跳转页面,post请求
                .successForwardUrl("/toMain")
                .failureForwardUrl("/toError");


        http.authorizeRequests()
                //login.html不需要被认证
                .antMatchers("/login.html").permitAll()
                //登录失败的页面不需要被认证
                .antMatchers("/error.html").permitAll()
                //所有请求必须都被认证,必须登录后被访问
                .anyRequest().authenticated();

        //关闭scrf防护,类似防火墙
        http.csrf().disable();


    }

    @Bean
    public PasswordEncoder getPw(){
        return new BCryptPasswordEncoder();
    }
}

successForwardUrl() :登录成功后跳转地址
loginPage() :登录页面
loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
antMatchers() :匹配内容
permitAll() :允许

html 页面:
在这里插入图片描述
controller:

@Controller
public class LoginController {

    /**
     * 成功后跳转页面
     * @return
     */
    @RequestMapping("/toMain")
    public String toMain(){
        return "redirect:/main.html";
    }


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

service

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder pw;
    /**
     * 实现接口,那么就需要实现方法:
     * 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
        if (!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
        // 或者直接把密码放入构造方法中。
        //既然是数据库查询出来的,肯定是加密的密码。

        /**
         * user是实现类,实现了UserDetailsService
         *  new user(前端传过来的用户名,数据库的密码,权限列表)
         *
         *  encode:传进去进行加密!!
         *  真是定义权限:具体有两个权限:一个admin、一个普通权限
         */
        String password = pw.encode("123");
        return new User(username,password,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
    }
}

html中的Form表单别名

form表单中的 method=post 和name=usename/password都是固定的
那么如何设置别名呢??

<body>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--
  拿到登录的form表单
  sumbit提交按钮,调用/login方法

  method=post/name=usename/password都是固定的
-->
<form action="/login" method="post">
    用户名:<input type="text" name="username123" /><br/>
    密码:<input type="password" name="password123" /><br/>
    <input type="submit" value="登录" />
</form>
</body>
</html>

在SecurtyConfig.java 设置别名:必须和form表单一致。
在这里插入图片描述

4、自定义登录成功处理器:

1.跳转成功处理器:

现在都是前后端分离,不会用controller做跳转了
做法:
成功处理器:
去创建Hander类,创建类并实现AuthenticationSuccessHandler接口
失败处理器:
去创建Hander类,创建类并实现AuthenticationFailureHandler接口

hander/myAuthenticationSuccessHander.java
成功处理器:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String url;
    //有参构造,当有new这个类时,直接传值即可。
    public MyAuthenticationSuccessHandler(String url ) {
        this.url = url;
    }
    //实现接口,重写方法
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       //response重定义,放入一个URL
        response.sendRedirect(url);
    }
}

2.跳转失败的处理器:

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String url;

    public MyAuthenticationFailureHandler(String url) {
        this.url = url;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}

3.在securityConfig的配置类中:

succesHandler传 (AuthenticationSuccessHandler类型)
failureHandler传(AuthenticationFailureHandler类型)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //重写对应的方法:
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交认证.
        http.formLogin()
                //设置login.html表单别名
                .usernameParameter("username123")
                .passwordParameter("password123")
                //自定义登录页面
                .loginPage("/login.html")
                //当发现/login时认为是登录,必须和表单提交的地址是一样的
                //去执行UserServiceImpl
                .loginProcessingUrl("/login")
                /*登录成功后跳转页面,必须是post请求(post像服务器发送数据)
                所以去controller写了跳转页面的方法。
                现在都是前后端分离,不会用controller做跳转了
                做法:创建Hander类,实现AuthenticationSuccessHandler接口
                succesHandler传:    AuthenticationSuccessHandler接口类型
                 */
                .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
                .failureHandler(new MyAuthenticationFailureHandler("error.html"));

                /* 登录成功后的处理器,不能和successForwardUrl共存
                forwardurl不能在外跳转。
                //.successForwardUrl("/toMain")
                //.failureForwardUrl("/toError");
                 */
                 
                 http.authorizeRequests()
                //login.html不需要被认证
                .antMatchers("/login.html").permitAll()
                //登录失败的页面不需要被认证
                .antMatchers("/error.html").permitAll()
                //所有请求必须都被认证,必须登录后被访问
                .anyRequest().authenticated();

        //关闭scrf防护,类似防火墙
        http.csrf().disable();
    }

4.测试:

这次跳转并没有去controller进行跳转,而是通过处理器进行跳转。
成功跳转到:http://www.baidu.com
在这里插入图片描述
跳转成功:
在这里插入图片描述
错误的密码,跳转到error.html:
在这里插入图片描述
在这里插入图片描述

5、http.authorizeRequests()…请求授权下的注解

1.anyRequest()详解:

除了上面定义多个antMatchers
任何请求 必须都被认证,也就是说必须登录后才能被访问
.anyRequest().authenticated();

2.antMatchers详解:(最常用的放行)

方法如下:
public c antMatchaes(Sgring... antPatterns)
参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。

http.authorizeRequests()
/**
 * antMatchers 匹配内容--》单独匹配
 * login.html不需要被认证
 * permitAll允许访问,
 * -----------
 * antMatchers的讲解:
 * 	public C antMatchers(String... antPatterns) { //可以加多个目录
 * 		Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
 * 		return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
 *     }
 *  ?匹配一个字符
 *  *:匹配0个多个字符
 *  **:匹配0个多个目录
 *
 *  一般静态资源就会放行(js/html/img/css)
 *
 */
.antMatchers("/js/**","css/**","images/**").permitAll()
.antMatchers("/login.html").permitAll()
//登录失败的页面不需要被认证
.antMatchers("/error.html").permitAll()
/***
 *  除了上面定义多个antMatchers
 *  任何请求 必须都被认证,也就是说必须登录后才能被访问
 */
.anyRequest().authenticated();

测试:放行静态资源:
在这里插入图片描述
在这里插入图片描述

3.regexMatchers()详解:

使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数, antMatchers() 参数是 ant 表达式,regexMatchers() 参数是正则表达式。
演示所有以.js 结尾的文件都被放行。
.regexMatchers( ".+[.]js").permitAll()

  • 两个参数时使用方式:
    无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表示请求方式,当设置HttpMethod 后表示只有设定的特定的请求方式才执行对应的权限设置。

1、可也任意指定请求方式:但是必须请求类型是匹配的。要不然也会拦截
2、例子中:去controller请求。这样访问时,8080/demo 。返回一个字符串
在这里插入图片描述

4.mvcMatchers()详解:

mvcMatchers()适用于配置了 servletPath 的情况。

  • servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在
    application.properties 中添加下面内容设置 ServletPath
    spring.mvc.servlet.path=/xxxx

  • 在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers()返回值特有的方法,antMatchers()和regexMatchers()没有这个方法。在 servletPath() 中配置了 servletPath 后,mvcMatchers()直接写 SpringMVC中@RequestMapping()中设置的路径即可。
    .mvcMatchers("/demo").servletPath("/xxxx").permitAll()

  • 如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效
    .antMatchers("/xxxx/demo").permitAll()

测试:
在这里插入图片描述
在这里插入图片描述

http.authorizeRequests()
// 在yml配置了:所有的URL都需要前面加上xxxx,才能访问
//.mvcMatchers("/demo").servletPath("/xxxx").permitAll()
// 上面和下面等同
 .antMatchers("/xxxx/demo").permitAll()

 //登录失败的页面不需要被认证
 .antMatchers("/error.html").permitAll()

在这里插入图片描述

5.内置访问控制方法

Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。在 Spring Security 中提供了多种内置控制。

  • permitAll()《-----》经常用
    permitAll()表示所匹配的 URL 任何人都允许访问。
  • authenticated() 《-----》经常用
    authenticated()表示所匹配的 URL 都需要被认。
  • anonymous()
    anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter
    链中
  • denyAll()
    denyAll()表示所匹配的 URL 都不允许被访问。
  • rememberMe() 《-----》经常用
    被“remember me”的用户允许访问。登录界面的“记住我”,只有点了“记住我”才能进行访问。
  • fullyAuthenticated()
    如果用户不是被 remember me 的,才可以访问。如果点了“记住我”就不能访问,只能手动输入账号密码才能点进来。

6.用户和角色权限判断

除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后(登录成功以后,有无权限去干的一件事情),判断用户是否具有特定的要求。 【例如:网站会员,有无权限使用某些操作】

  • hasAuthority(String)
    判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。
    在这里插入图片描述

  • 在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。

只有admin权限才能访问main1.html
.antMatchers("/main1.html").hasAuthority("admin")
  • hasAnyAuthority(String …) 多个权限。。
    如果用户具备给定权限中某一个,就允许访问。
    下面代码中由于大小写和用户的权限不相同,所以用户无权访问
    .antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")

测试:
从login.html登录成功后,任何权限都能访问到main,main有个链接到Main1。
第一次:我设为:admin1权限才能登录,我用admin登录报错显示:403没有权限。
第二次: 我设为:admin/admin1权限才能登录,可以登录了。

// hasauthority只有admin权限才能登录:
//.antMatchers("/main1.html").hasAuthority("admin1")
// hasanyauthority只有admin,admin1的权限才能登录。any可以指定多个。
.antMatchers("/main1.html").hasAnyAuthority("admin1","admin")

在这里插入图片描述
在这里插入图片描述

  • hasRole(String)
    如果用户具备给定角色就允许访问。否则出现 403。
    参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。

在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。

给用户赋予角色:
在这里插入图片描述

在配置类中直接写 abc 即可。
```.antMatchers("/main1.html").hasRole(“abc”)``

7.IP地址判断

通过IP地址对权限的个控制。项目的后台管理系统,只能在指定的服务器进行登录操作。这台服务器地址是固定的。那么就可以通过ip地址权限控制。
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
当我们绑定了127.0.0.1,用locahost:登录成功后,从main跳转到main1时,发现报错403了
在这里插入图片描述
现在用127.0.0.1去跳转main1.html,发现成功了:
(忽略页面上面的话语)
在这里插入图片描述

6. 解决403权限不足,页面展示:

而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权
限受限。
新建类实现 AccessDeniedHandler

//组成
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //1.设置状态码403
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //3.相应的请求头
        response.setHeader("Content-Type","application/json;charset=utf-8");
        //2.写一段话,相应给浏览器
        PrintWriter writer = response.getWriter();
        writer.write("{\"status\":\"error\",\"msg\":\"权限不足\"}");
        writer.flush();
        writer.close();
    }
}

修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
myAccessDeniedHandler 是在配置类中进行自动注入的。

@Autowired
private MyAccessDeniedHandle myAccessDeniedHandle;
//异常处理
http.exceptionHandling()
     .accessDeniedHandler(myAccessDeniedHandler);

测试进入:main1.html,不在是403报错了,而是友好提示:

在这里插入图片描述

7.基于表达式的访问控制

1、access()方法使用

之前学习的登录用户权限判断实际上底层实现都是调用access(的内置表达式)
在这里插入图片描述
可以通过 access() 实现和之前学习的权限控制完成相同的功能。
以 hasRole 和 和 permitAll 举例
在这里插入图片描述
在这里插入图片描述

2、access实现自定义表达式

虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。
判断登录用户是否具有访问当前 URL 权限。
新建接口及实现类
MyService.java

public interface MyService {
    /**
     * 允许的意思
     * request:是为了去拿对应的主体和权限。
     * authentication:权限的意思
     */
    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

实现接口:MyServiceImpl

@Service
public class MyServiceImpl implements MyService{
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        //获得主体,就是User对象
        Object obj = authentication.getPrincipal();
        if(obj instanceof UserDetails){
            //如果
            UserDetails userDetails= (UserDetails) obj;
            //得到权限
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            //判断权限,是否包含URL.包含一个
            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
        }
        return  false;
    }
}

修改配置类:
在 access 中通过@bean的id名.方法(参数)的形式进行调用配置类中修改如下:

http.authorizeRequests()
.antMatchers("/login.html").permitAll()
/***
*  除了上面定义多个antMatchers
*  任何请求 必须都被 认证,也就是说必须登录后才能被访问
*  ---
*  access实现自定义表达式
*  做一个demo,判断登录到用户是否有访问当前页面的权限。
*/
//.anyRequest().authenticated();
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");

在这里插入图片描述

8、基于注解的访问控制

在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity 进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500

这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

1、secured判断是否具有角色

  • @Secured
    @Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。

  • 开启注解
    在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加
    @EnableGlobalMethodSecurity(securedEnabled = true)

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {
   public static void main(String[] args) {
      SpringApplication.run(SpringsecurityDemoApplication.class, args);
   }
}

在控制器方法上添加@Secured 注解

/**
* 成功后跳转页面
* @return
*/
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
   return "redirect:/main.html";
}

配置类

Override
protected void configure(HttpSecurity http) throws Exception {
   //表单提交
   http.formLogin()
         //自定义登录页面
         .loginPage("/login.html")
         //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
         .loginProcessingUrl("/login")
         //登录成功后跳转页面,POST请求
         .successForwardUrl("/toMain")
         
   //url拦截
   http.authorizeRequests()
         //login.html不需要被认证
         .antMatchers("/login.html").permitAll()
         //所有请求都必须被认证,必须登录后被访问
         .anyRequest().authenticated();
   //关闭csrf防护
   http.csrf().disable();
}

2、@PreAuthorize访问方法在执行之前先判断权限

@PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和
access()方法参数取值相同,都是权限表达式。
开启注解

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityDemoApplication {
   public static void main(String[] args) {
      SpringApplication.run(SpringsecurityDemoApplication.class, args);
   }
}

添加@PreAuthorize
在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式

/**成功后跳转页面
* @return
* */
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
   return "redirect:/main.html";
}

9、RememberMe功能实现(记住我)

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问.

添加依赖
Spring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 Spring-JDBC。现在使用 MyBatis 框架很多,而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需要添加 MySQL 驱动

配置数据源
在 application.properties 中配置数据源。请确保数据库中已经存在shop数据库

spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?
useUnicode=true&characterEncoding=UTF8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= root

在这里插入图片描述

编写配置
ServiceConfig.java

//记住我
http.rememberMe()
     //失效时间:10秒,单位秒
     .tokenValiditySeconds(10)
     //自定义登录逻辑
     .userDetailsService(userDetailsService)
     //持久层对象
     .tokenRepository(persistentTokenRepository);

 @Bean
public PersistentTokenRepository getPersistentTokenRepository(){
   JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
   jdbcTokenRepository.setDataSource(dataSource);
   //自动建表,第一次启动时需要,第二次启动时注释掉
   //jdbcTokenRepository.setCreateTableOnStartup(true);
   return jdbcTokenRepository;
}

添加网页记住我:

<form action="/login" method="post">
   用户名:<input type="text" name="username" /><br/>
   密码:<input type="password" name="password" /><br/>
   记住我:<input type="checkbox" name="remember-me" value="true" /><br/>
    <input type="submit" value="登录" />
</form>

测试:

  • 数据库的变化:

创建数据库时:只是创建了一个数据库。并没有创建表。
当运行程序时,发现数据库多了表,但没有内容。
在这里插入图片描述
输入账号和密码后:
在这里插入图片描述

  • 登录变化:
    第一次登录完成后,可以看到数据库有了数据。然后我们关闭浏览器,从新打开浏览器,直接到localhost:8080/main.html。发现不用登录了,可以直接到达该页面。
    在这里插入图片描述

10、Thymeleaf中SpringSecurity的使用

Spring Security 可以在一些视图技术中进行控制显示效果。例如: JSP 或 Thymeleaf 。在非前后端分离且使用Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。
Thymeleaf 对 Spring Security 的 支 持 都 放 在 thymeleaf-extras-springsecurityX 中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。

<!--thymeleaf springsecurity5 依赖-->
<dependency>
   <groupId>org.thymeleaf.extras</groupId>
   <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在 html 页面中引入 thymeleaf 命名空间和 security 命名空间

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

新建demo.html
在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
   登录账号:<span sec:authentication="name"></span><br/>
   登录账号:<span sec:authentication="principal.username"></span><br/>
   凭证:<span sec:authentication="credentials"></span><br/>
   权限和角色:<span sec:authentication="authorities"></span><br/>
   客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
   sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>

编写Controller
thymeleaf 页面需要控制转发,在控制器类中编写下面方法

RequestMapping("/demo")
public String demo(){
   return "demo";
}

在这里插入图片描述
2、通过权限和角色,在页面展示:
在页面中根据用户权限和角色判断页面中显示的内容。
可以控制哪些按钮,被哪些权限、角色使用。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

11、退出

销毁http的对象
清除认证
退出成功的处理器。

在main.html中添加:

<a href="/logout">退出</a>

在这里插入图片描述
当我们只在HTML中添加了代码,并没有去config中添加配置代码:
在这里插入图片描述
在securityConfig中添加:自定义登录退出页面

//退出
http.logout()
	//必须和main.html退出登录的一样("/logout"). 默认就是logout
	//.logoutUrl("/login")
	//跳出去登录页面。
	.logoutSuccessUrl("/login.html");

在这里插入图片描述

五、csrf跨站请求伪造

在配置类中一直存在这样一行代码: http.csrf().disable(); 如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护。

学习阶段直接关闭csrf。
工作在登录页面中:去服务器中取到生成的token,然后在访问的时候再带回服务器。服务器会判断带回来的token和生成的token是否一致。一致则访问。

1、什么是CSRF跨站请求伪造

例子说明跨站请求伪造:
用户A通过网站给B转钱。正常操作
用户C伪装用户A,通过网站给C转钱。非正常操作。
也就是说用户C把用户A的钱,转给自己了。

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

2、Spring Security中的CSR

从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。

1、修改配置类
在配置类中注释掉 CSRF 防护失效

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

当我们关闭权限运行,在正确密码登录:发现无法登陆。
是因为:我们在登录访问的时候,要求携带参数名csrf,token值参数。如果没有的话,那就不让进行正常访问。

2、新建在/templates/login.html
添加隐藏域:
取服务器的{_csrf.token}
判断:如果有放进去,没有则空。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<-- 隐藏域 -->
<form action="/login" method="post">
    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
    用户名:<input type="text" name="username123" /><br/>
    密码:<input type="password" name="password123" /><br/>
    记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
    <input type="submit" value="登录" />
</form>
</body>
</html>

在这里插入图片描述
登录进来了以后:_csrf:就是服务器生成的token
在这里插入图片描述
在这里插入图片描述

六:该学习过程涉及的代码:

1、config/SecurityConfig

/**
* 我们自定义登录逻辑时,容器内必须有Password实例,
* 所以要写一个配置类。创建config/SecurityConfig
*
* 创建好了实例,那么实现UserDetailsService接口
*
* extend WebSecurityConfigurerAdapter
*
*
* successForwardUrl() :登录成功后跳转地址
* loginPage() :登录页面
* loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
* antMatchers() :匹配内容
* permitAll() :允许
*/

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private MyAccessDeniedHandle myAccessDeniedHandle;

   @Autowired
   private UserDetailsService userDetailsService;
   @Autowired
   public DataSource dataSource;

   @Autowired
   private PersistentTokenRepository persistentTokenRepository;

   //重写对应的方法:
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //表单提交认证.
       http.formLogin()
               //设置login.html表单别名
               .usernameParameter("username123")
               .passwordParameter("password123")
               //自定义登录页面
//                .loginPage("/login.html")
               //关闭crsf,去controller访问跳转
               .loginPage("/showLogin")

               //当发现/login时认为是登录,必须和表单提交的地址是一样的
               //去执行UserServiceImpl
               .loginProcessingUrl("/login")
               /*登录成功后跳转页面,必须是post请求(post像服务器发送数据)
               所以去controller写了跳转页面的方法。
               现在都是前后端分离,不会用controller做跳转了
               做法:创建Handler类,实现AuthenticationSuccessHandler接口
               successHandler传:    AuthenticationSuccessHandler接口类型

               .successHandler(new MyAuthenticationSuccessHandler("main.html"))
               .failureHandler(new MyAuthenticationFailureHandler("error.html"));*/


               /* 登录成功后的处理器,不能和successForwardUrl共存
               forwardUrl不能在外跳转。
               ---
               此时测试:serured注解:跳转到controller进行跳转:*/

               .successForwardUrl("/toMain")
               .failureForwardUrl("/toError");



       //授权认证
       http.authorizeRequests()

               /*
                * antMatchers 匹配内容--》单独匹配
                * login.html不需要被认证
                * permitAll允许访问,
                * -----------
                * antMatchers的讲解:
                * 	public C antMatchers(String... antPatterns) { //可以加多个目录
                * 		Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
                * 		return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
                *     }
                *  ?匹配一个字符
                *  *:匹配0个多个字符
                *  **:匹配0个多个目录
                *
                *  一般静态资源就会放行(js/html/img/css)
                *  antMatchers:下面的网页都可以访问。
                */
               //.antMatchers("/js/**","css/**","images/**").permitAll()
               //.antMatchers("/login.html").permitAll()
               //放行showLogin
               .antMatchers("/showLogin").permitAll()

               //mvc路径判断:
               // 在yml配置了:所有的URL都需要前面加上xxxx,才能访问.
               //.mvcMatchers("/demo").servletPath("/xxxx").permitAll()
               // 上面和下面等同
              // .antMatchers("/xxxx/demo").permitAll()

               //权限判断:
               // hasauthority只有admin权限才能登录:
               //.antMatchers("/main1.html").hasAuthority("admin1")
               // hasanyauthority只有admin,admin1的权限才能登录。any可以指定多个。
               //.antMatchers("/main1.html").hasAnyAuthority("admin1","admin")

               //角色判断:
               //.antMatchers("/main1.html").hasRole("abc")

               //固定ip地址访问:
               //.antMatchers("/main1.html").hasIpAddress("127.0.0.1")

               //登录失败的页面不需要被认证
               //.antMatchers("/error.html").permitAll()
               .antMatchers("/error.html").access("permitAll()")

               /*
                *  除了上面定义多个antMatchers
                *  任何请求 必须都被 认证,也就是说必须登录后才能被访问
                *  ---
                *  access实现自定义表达式
                *  做一个demo,判断登录到用户是否有访问当前页面的权限。
                */
               .anyRequest().authenticated();
               // access实现自定义表达式
               //.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");


       //关闭scrf防护,类似防火墙
//        http.csrf().disable();

       //异常处理:
       http.exceptionHandling()
               .accessDeniedHandler(myAccessDeniedHandle);

       //记住我
       http.rememberMe()
               //登录逻辑交给哪个对象
               .userDetailsService(userDetailsService)
               // 持久层对象
               .tokenRepository(persistentTokenRepository);

       //退出
       http.logout()
               //必须和main.html退出登录的一样("/logout")
               //.logoutUrl("/logout")
               //跳出去登录页面。
               .logoutSuccessUrl("/login.html");


   }

   @Bean
   public PasswordEncoder getPw(){
       return new BCryptPasswordEncoder();
   }

   @Bean
   public PersistentTokenRepository getPersistentTokenRepository(){
       JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
       jdbcTokenRepository.setDataSource(dataSource);
       //自动建表,第一次启动时需要,第二次启动时注释掉
       //jdbcTokenRepository.setCreateTableOnStartup(true);
       return jdbcTokenRepository;
   }
}

2、controller

Controller/LoginController

//@RestController
@Controller
public class LoginController {

    /*
    * 登录方法:
    *
    @RequestMapping("/login")
    public String login(){
        System.out.println("执行了登录方法");
        //重定向到主页:
        return "redirect: main.html";

    }*/
    //在准备两个HTML:login.html / main.html
    // 当浏览器访问“/login"时,给浏览器返回main.html


    /**
     * 成功后跳转页面
     * @return
     * 角色:
     */


    //@Secured("ROLE_abc")
    //preauthorize的表达式允许ROLE_开头,也可以不开头,配置类不允许开头
    @RequestMapping("/toMain")
    @PreAuthorize("hasRole('abc')")
    public String toMain(){
        return "redirect:/main.html";
    }

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


    @RequestMapping("/demo")
    public String demo(){
        return "demo";
    }

    /**
     * 关闭csrf
     * 去templates的login.html去访问
     * @return
     */
    @RequestMapping("/showLogin")
    public String showLogin(){
        return "login";
    }

}

3、hendler: 成功跳转、失败跳转、403跳转:

hendler/MyAccessDeniedHandle 403跳转

//组成
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //1.设置状态码403
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //3.相应的请求头
        response.setHeader("Content-Type","application/json;charset=utf-8");
        //2.写一段话,相应给浏览器
        PrintWriter writer = response.getWriter();
        writer.write("{\"status\":\"error\",\"msg\":\"权限不足\"}");
        writer.flush();
        writer.close();
    }
}

hendler/MyAuthenticationFailureHandle 失败跳转

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String url;

    public MyAuthenticationFailureHandler(String url) {
        this.url = url;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}

hendler/MyAuthenticationFailureHandle 成功跳转

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String url;
    //有参构造,当有new这个类时,直接传值即可。
    public MyAuthenticationSuccessHandler(String url ) {
        this.url = url;
    }
    //实现接口,重写方法
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
       //response重定义,放入一个URL
        response.sendRedirect(url);
    }
}

4、service

service/MyService

public interface MyService {
    /**
     * 允许的意思
     * request:是为了去拿对应的主体和权限。
     * authentication:权限的意思
     */
    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

MyServiceImpl

@Service
public class MyServiceImpl implements MyService{
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        //获得主体,就是User对象
        Object obj = authentication.getPrincipal();
        if(obj instanceof UserDetails){
            //如果
            UserDetails userDetails= (UserDetails) obj;
            //得到权限
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            //判断权限,是否包含URL.包含一个
            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
        }
        return  false;
    }
}

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder pw;
    /**
     * 实现接口,那么就需要实现方法:
     * 传一个username,就是浏览器传过来的,根据用户名去数据库查询用户
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.查询数据库判断用户名是否存在,如果不存在就会抛异常
        if (!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 2.如果存在用户则将查询出来的密码尽心解析,与前端比较。
        // 或者直接把密码放入构造方法中。
        //既然是数据库查询出来的,肯定是加密的密码。

        /**
         * pw是config/securityConfig下面的bean
         * @Bean
         * public PasswordEncoder getPw(){
         *    return new BCryptPasswordEncoder();
         * }
         * PasswordEncoder是一个接口,该结构被BCryptPasswordEncoder实现了
         * BCryptPasswordEncoder类下encode方法,进行加密操作
         *
         *
         * user是实现类,实现了UserDetailsService
         *  new user(前端传过来的用户名,数据库的密码,权限列表以逗号分隔)
         *
         *  encode:传进去进行加密!!
         *  真是定义权限:具体有两个权限:一个admin、一个普通权限
         */
        String password = pw.encode("123");
        return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList
                        ("admin,normal,ROLE_abc,/main.html,/insert,/delete"));
        // abc的角色,增加删除的权限
    }
}

5、static/*.html

在这里插入图片描述
在这里插入图片描述

6、templates/*.html

在这里插入图片描述



  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值