• 【SpringBoot】SpringBoot整合SpringSecurity+thymeleaf实现认证授权(配置对象版)


    一.概述

    1.框架概述

    Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 的两大核心功能就是认证(authentication)和授权(authorization)

    • 认证 :你是什么人。
    • 授权 :你能做什么。
    • 用户 :主要包括用户名称、用户密码和当前用户所拥有的角色信息,可用于实现认证操作。
    • 角色 :主要包括角色名称、角色描述和当前角色所拥有的权限信息,可用于实现授权操作。

    常用词汇

    • 认证 :authentication
    • 授权 :authorization
    • 用户 :user
    • 角色 :role
    • 登录 :login
    • 注销 :logout

    权限管理需要三个对象

    • 用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。
    • 角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
    • 权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单
      注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,二者是通过角色建立关联关系的。

    2.环境准备

    请在配套代码中,以及实现相关代码,直接拿来用就行了

    • 源码地址

      • zhangsan:作为产品采购员,只能访问产品管理模块
      • lisi:作为财务管理员,只能访问订单管理模块
      • wangwu:作为系统管理员,可以访问所有模块,并可以对zhangsan和lisi进行访问权限管理
        在这里插入图片描述

    修改配置文件application.yml

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/数据库名称
        username: root
        password: 密码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    二.基本使用

    1.导入所需依赖

    springboot版本

        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.4.2version>
            <relativePath/>
        parent>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    基础依赖

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-thymeleafartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>2.1.4version>
            dependency>
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>5.1.49version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.创建配置对象

    @Configuration
    @EnableWebSecurity//开启Spring Security对WebMVC的支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        //请将对Spring Security的配置方法写在这个类中
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.初次访问

    完成上图操作后,就可以使用Spring Security的功能了,地址栏输入:http://localhost:8080

    • SpringBoot已经为SpringSecurity提供了默认配置,默认所有资源都必须认证通过才能访问。
      在这里插入图片描述
      默认的账户是user,而默认的密码必须看控制台
      在这里插入图片描述

    在这里插入图片描述

    4.配置登录用户

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("{noop}123456").roles("USER");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}123456").roles("ADMIN");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 使用内存方式配置用户以及权限,角色前边千万不能加前缀ROLE_,否则会启动失败

    5.退出当前登录

    如果想要注销,访问:http://localhost:8080/logout就可以了,为了功能完整,请你打开main.html,第16行,修改注销地址为以下这段代码:

    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="btn btn-danger btn-sm" th:href="@{/logout}">注销a>
        li>
    ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.开放内嵌框架

    当你使用用户user密码123456登录的时候,默认就会进入到权限管理系统的后台首页,但是当你点击各个功能模块的时候,会发现localhost拒绝了我们的连接请求。其实这个问题还是挺常见的一个问题,项目中如果用到iframe嵌入网页,然后用到Spring Security,请求就会被拦截,如果你打开F12开发者控制台,你可能就会发现这样一句报错:Refused to display 'http://localhost:8080/user/add' in a frame because it set 'X-Frame-Options' to 'deny'.

    在这里插入图片描述

    Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何Frame页面,设置含义如下:

    • DENY:浏览器拒绝当前页面加载任何frame页面

    • SAMEORIGIN:frame页面的地址只能为同源域名下的页面

    • ALLOW-FROM:origin为允许frame加载的页面地址

    方案如下:

    • 关掉Spring Security对frame的拦截
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭X-Frame-Options响应头
        http.headers().frameOptions().disable();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 将X-Frame-Options设置为SAMEORIGIN,也就是只能是我们同域名下的请求访问,当然了,这种拦截机制肯定是为了保证系统的安全性,如果关掉了,有点太可惜了,这里采用第二种,而不是第一种的关闭。
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置X-Frame-Options响应头为SAMEORIGIN
        http.headers().frameOptions().sameOrigin();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    7.指定登录页面

    想要使用自己的登录界面该怎么办?先打开源码,看看他是怎么写的,按照他的这个模式,我们模仿着写到自己的登录界面中不就好了
    在这里插入图片描述
    内置登录页面很简单,就是一个form表单,里边有两个文本框,一个是账号,一个是密码,还有最下边多了一个特殊的hidden隐藏域,这个隐藏域他是为了防止csrf跨站破坏的,这个值每一次启动项目都不一样,是一个动态值,他是为了标识当前请求一定是我们自己的请求,而不是别的网站仿造的请求,我们的所有请求都需要携带上这个标签上边的value值,我们也称这个值为token值如果使用的是thymeleaf,那么form action会帮我们自动加上csrf 隐藏域,这样我们不用什么特殊处理也就可以登录了

    • 我们找到我们工程中的login.html,里边是一个空的html,请把以下代码复制进入。下边是我们自己定义的一个登录页面。
    DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>自定义登录页title>
        <link rel="stylesheet" th:href="@{css/bootstrap.min.css}">
    head>
    <body>
    <div class="container mt-4">
        <form th:action="@{/login}" method="post">
            <div class="form-group">
                <label for="username">用户:label>
                <input type="text" class="form-control" id="username" name="username" placeholder="请输入用户" required>
            div>
            <div class="form-group">
                <label for="password">密码:label>
                <input type="text" class="form-control" id="password" name="password" placeholder="请输入密码" required>
            div>
            <div class="form-group form-check">
                <input type="checkbox" class="form-check-input" id="autoLogin">
                <label class="form-check-label" for="autoLogin">自动登录label>
            div>
            <button type="submit" class="btn btn-primary">登录button>
        form>
    div>
    <script th:src="@{js/jquery-3.5.1.min.js}">script>
    <script th:src="@{js/bootstrap.bundle.min.js}">script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    修改springSecurity配置类

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置X-Frame-Options响应头为SAMEORIGIN
        http.headers().frameOptions().sameOrigin();
        //放行不用权限的资源(去登录页面当然不需要用权限,否则你都看不到登录界面,还怎么登录,所以去登录界面必须放行)
        http.authorizeRequests().antMatchers("/toLogin").permitAll();
        //拦截需要权限的资源(拦截所有请求,要想访问,登录的账号必须拥有USER和ADMIN的角色才行)
        http.authorizeRequests().antMatchers("/**").hasAnyRole("USER", "ADMIN").anyRequest().authenticated();
        //设置自定义登录界面
        http.formLogin()//启用表单登录
            .loginPage("/login")//登录页面地址,只要你还没登录,默认就会来到这里
            .loginProcessingUrl("/loginProcess")//登录处理程序,Spring Security内置控制器方法
            .usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
            .passwordParameter("password")//登录表单form中密码框输入框input的name名,不修改的话默认是password
            .defaultSuccessUrl("/main")//登录认证成功后默认转跳的路径
            //.successForwardUrl("/main")//登录成功跳转地址,使用的是请求转发
            .failureForwardUrl("/login")//登录失败跳转地址,使用的是请求转发
            .permitAll();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    创建controller

    @Controller
    public class MainController {
        @RequestMapping("/main")
        public String main() {
            return "main";
        }
    
        //跳转到登录页的方法
        @RequestMapping("/login")
        public String toLogin() {
            return "login";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    8.开放静态资源

    Spring Security默认是拦截所有请求,那肯定也包括静态资源css、js、img之类的,因此,静态资源是应该要被放行的,静态资源是不需要进行保护的,我们需要在SecurityConfig配置如下代码来放行静态资源。

    • 否则会导致前端资源加载失败
        @Override
        public void configure(WebSecurity web) throws Exception {
            //配置不被拦截的系统资源
            web.ignoring().antMatchers("/css/**");
            web.ignoring().antMatchers("/img/**");
            web.ignoring().antMatchers("/js/**");
            web.ignoring().antMatchers("/favicon.ico");
            web.ignoring().antMatchers("/error");
            web.ignoring().antMatchers("/swagger-ui.html#/");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    9.指定退出页面

    当你现在想要退出登录,点击右上角咱们之前配置好的注销,你就会神奇的发现,好像不能退出了,这是因为,默认退出会直接跳转到/login自动生成的认证页面,现在,认证页面也就是登录页面,已经改成我们自己的登录页面了,你只要指定了登录页面了,那默认的登录页面自然就不会创建了,因此当你退出的时候也就会报404找不到异常。
    在这里插入图片描述
    修改springSecurity配置类

            //设置自定义登出界面
            http.logout()//启用退出登录
                    .logoutUrl("/logoutProcess")//退出处理程序,Spring Security内置控制器方法,(即前端登出请求地址)
                    .logoutSuccessUrl("/login")//退出成功跳转地址
                    .invalidateHttpSession(true)//清除当前会话
                    .deleteCookies("JSESSIONID")//删除当前Cookie
                    .permitAll();
            //SpringSecurity3.2开始,默认会启动CSRF防护,一旦启动了CSRF防护,“/logout” 需要用post的方式提交,SpringSecurity才能过滤。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    找到main.html,把之前的a标签的get请求,换成form的post请求,并加上隐藏域csrfcsrf不用我们自己加,只要你是用的thymeleaf的form,他会帮我们加上

    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <form th:action="@{/logout}" method="post">
                <input class="btn btn-danger btn-sm" type="submit" value="退出">
            form>
        li>
    ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    三.高级使用

    1.深入跨站请求伪造

    1.1.CSRF的概念

    CSRF跨站点请求伪造(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

    1.2.CSRF的原理

    假设:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,用户C为Web A网站的合法用户。

    • 用户C打开浏览器,访问WEB A,输入用户名和密码请求登录网站WEB A;
    • 用户C在用户信息通过验证后,WEB A产生Cookie信息并返回给浏览器,此时用户登录WEB A成功,可以正常发送请求到WEB A;
    • 用户C未退出WEB A之前,在同一浏览器中,打开一个TAB页访问WEB B;
    • WEB B接收到用户C请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点WEB A;
    • 浏览器在接收到这些攻击性代码后,根据WEB B的请求,在用户C不知情的情况下携带Cookie信息,向WEB A发出请求。WEB A并不知道该请求其实是由WEB B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自WEB B的恶意代码被执行。

    1.3.CSRF的防御

    目前防御 CSRF 攻击主要有三种策略

    • 验证HTTP Referer字段
    • 在请求地址中添加 token 并验证(Spring Security采用)
    • HTTP 头中自定义属性并验证。

    1.验证 HTTP Referer 字段

    • HTTP 头字段 Referer记录了该 HTTP 请求的来源地址。正常情况下访问一个安全受限页面的请求来自于同一个网站

      • 比如需要访问 http://bank.example/withdraw?account=bob&amount=1000000&for=Mallory,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。
      • 如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站
      • 因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求
    • 优点: 简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

    • 缺点:每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

    2.在请求地址中添加 token 并验证

    • CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

    优点:

    • 这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。

    • 在Spring Security中,“GET”, “HEAD”, “TRACE”, "OPTIONS"四类请求可以直接通过,并不会被CsrfFilter过滤器过滤,会被直接放行,但是对于其他过滤器该过滤的还是会过滤的,除去上面四类,包括POST都要被验证携带token才能通过。

    3.在 HTTP 头中自定义属性并验证

    • 这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

    • 然而这种方法的局限性非常大。XMLHttpRequest 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 XMLHttpRequest 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

    1.4.form表单如何添加token

    如果您使用的是thymeleaf,如果使用的是thymeleaf,那么form action会帮我们自动加上csrf 隐藏域,我们不用特殊处理。

    • 如果自己想要设置,我们也可以使用隐藏域自己设置,一般我们不会设置这个,默认就有你设置他干啥,参考代码如下:
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    
    • 1

    1.5.ajax请求如何添加token

    如果您使用的是thymeleaf,则可以直接在head标签内加上一个隐藏域即可。

    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    
    • 1
    • 2

    在这里插入图片描述

    $(function () {
        var token = $("meta[name='_csrf']").attr("content");
        var header = $("meta[name='_csrf_header']").attr("content");
        $(document).ajaxSend(function(e, xhr, options) {
            xhr.setRequestHeader(header, token);
        });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.文件上传避免 CSRF 拦截

    请将MultipartFilter在Spring Security过滤器之前指定。MultipartFilter在Spring Security过滤器之前指定,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序所处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。具体配置代码如下:

    public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
        @Override
        protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
            insertFilters(servletContext, new MultipartFilter());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.如何关闭 CSRF 防御机制

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        ...
        //关闭CSRF跨站点请求仿造保护
        http.csrf().disable();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.完成网站自动登录

    如果我想要关闭浏览器,下次再打开浏览器,权限管理系统会自动根据我上次的登录状态进行登录,这就是登录常用的“自动登录功能”,要想实现自动登录功能,我们需要实现两处关键配置就能使用了,具体操作如下:

    • 打开login.html修改自动登录的name为remember-me,这是一个默认名称,可以修改,但是一般我们就叫这个名
    <div class="form-group form-check">
        <input type="checkbox" class="form-check-input" id="autoLogin" name="remember-me">
        <label class="form-check-label" for="autoLogin">自动登录label>
    div>
    
    • 1
    • 2
    • 3
    • 4

    配置 SecurityConfig 开启自动登录功能

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        ...
        //开启记住我功能(自动登录)
        http.rememberMe()
            .rememberMeParameter("remember-me")//表单参数名,默认参数是remember-me
            .rememberMeCookieName("remember-me")//浏览器存的cookie名,默认是remember-me
            .tokenValiditySeconds(60*60*24*30);//保存30两天,默认是两周
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    登录后关闭浏览器,然后重新打开 http://localhost:8080/ ,发现仍然可以访问,并且这时候不需要登录,他是怎么做到的呢?

    • 其实,在登录成功以后会往当前网站的cookie中写入一个自动登录的token值,当我们下次启动的时候,只要这个cookie没有消失,Spring Security就能拿到这个cookie的中保存的token的值,然后帮我们自动登录认证。
      在这里插入图片描述

    5.保存凭据到数据库

    自动登录功能方便是大家看得见的,但是安全性却令人担忧。因为cookie毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。

    • 此外,Spring Security还提供了remember-me的另一种相对更安全的实现机制:在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到数据库表中验证,如果通过,自动登录才算通过。这样,自动登录功能的安全性就有了保证

    • 需要创建一张用于保存自动登录信息的表,这张表是固定的,包括名称、字段等信息,都不能修改,否则会认识失败。

      CREATE TABLE `persistent_logins` (
      `username` varchar(64) NOT NULL,
      `series` varchar(64) NOT NULL,
      `token` varchar(64) NOT NULL,
      `last_used` timestamp NOT NULL,
      PRIMARY KEY (`series`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    修改springSecurity配置类

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
        ...
        //开启记住我功能(自动登录)
        http.rememberMe()
            .rememberMeParameter("remember-me")//表单参数名,默认参数是remember-me
            .rememberMeCookieName("remember-me")//浏览器存的cookie名,默认是remember-me
            .tokenValiditySeconds(60 * 60 * 24 * 30)//保存30两天,默认是两周
            .tokenRepository(persistentTokenRepository());//使用数据库存储token,防止重启服务器丢失数据,非常重要,没有他不能保存到数据库
    }
    
    //数据源是咱们默认配置的数据源,直接注入进来就行
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    重新进行测试,发现也是可行的,并且这里给出了浏览器和数据库的截图信息:
    在这里插入图片描述
    在这里插入图片描述

    6.展示当前登录用户

    登录成功以后,如何显示出来当前登录成功的用户名呢?

    • 有两种常用方法,他们都必须使用Spring Security的标签库,在使用thymeleaf渲染前端的html时,thymeleaf为SpringSecurity提供的标签属性,首先需要引入thymeleaf-extras-springsecurity5依赖支持。

    1.引入依赖

    <dependency>
      <groupId>org.thymeleaf.extrasgroupId>
      <artifactId>thymeleaf-extras-springsecurity5artifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    2.在main.html文件里面导入标签所对应的名称空间。

    DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
    
    • 1
    • 2
    • 3

    第一种:打开main.html修改第12行

    <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
        权限管理系统,您好:
        <span sec:authentication="principal.username">span>
    a>
    
    • 1
    • 2
    • 3
    • 4

    第二种:打开 main.html 修改第12行

    <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">
        权限管理系统,您好:
        <span sec:authentication="name">span>
    a>
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    7.对接数据库中数据

    目前是在内存中(代码写死的就在内存中)配置好了两个用户(user、admin)以及他们所对应的角色

    在真实场景中,我们就需要使用数据库来保存用户信息,我们如何对接数据库中的数据呢?

    第一步:实现自己的 SysUserDetailsService 接口继承 UserDetailsService

    public interface SysUserDetailsService extends UserDetailsService {
        
    }
    
    • 1
    • 2
    • 3

    第二步:实现自己的SysUserDetailsService接口的loadUserByUsername方法,方法传入一个字符串,代表当前登录的用户名

    @Service
    @Transactional
    public class SysUserDetailsServiceImpl implements SysUserDetailsService {
        @Autowired
        private SysUserMapper sysUserMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //根据用户名去数据库中查询指定用户,这就要保证数据库中的用户的名称必须唯一,否则将会报错
            SysUser sysUser = sysUserMapper.findUserByUsername(username);
            //如果没有查询到这个用户,说明数据库中不存在此用户,认证失败
            if (sysUser == null) {
                throw new UsernameNotFoundException("user not exist");
            }
    
            //获取该用户所对应的所有角色,当查询用户的时候级联查询其所关联的所有角色,用户与角色是多对多关系
            //如果这个用户没有所对应的角色,也就是一个空集合,那么在登录的时候会报 403 没有权限异常,切记这点
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            List<SysRole> sysRoles = sysUser.getSysRoles();
            for (SysRole sysRole : sysRoles) {
                authorities.add(new SimpleGrantedAuthority(sysRole.getName()));
            }
    
            //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证
            //org.springframework.security.core.userdetails.User实现了UserDetails对象,是SpringSecurity内置认证对象
            return new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    第三步:修改配置文件SecurityConfig中的 认证提供者换成咱们自己定义

    @Autowired
    private SysUserDetailsService sysUserDetailsServiceImpl;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserDetailsServiceImpl);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第四步:使用数据库所提供的账户进行登录测试。
    在这里插入图片描述

    8.用户密码进行加密

    第一步:配置加密对象,然后设置给咱们自己的认证提供者

    
    @Configuration
    @EnableWebSecurity//开启Spring Security对WebMVC的支持
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
    	private SysUserDetailsService sysUserDetailsServiceImpl;
    
    	@Autowired
    	private BCryptPasswordEncoder passwordEncoder;
    	
       //........
       //........
       
        @Bean
        public BCryptPasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	    auth.userDetailsService(sysUserDetailsServiceImpl).passwordEncoder(passwordEncoder);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第二步:保存用户的时候,给用户的密码进行加密,修改SysUserServiceImpl

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    
    @Override
    public void save(SysUser sysUser) {
        sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
        sysUserMapper.save(sysUser);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第三步:去掉 SysUserDetailsServiceImpl 中的{noop}

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //...
        //...
        //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证
        //org.springframework.security.core.userdetails.User实现了UserDetails对象,是SpringSecurity内置认证对象
        return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第四步:手动修改数据库中的密码为加密后的密码,我们现在需要知道123456加密后的密文,需要手动生成

    • 注意啊,调用BCryptPasswordEncoder 算法每一次生成都不一样,但是都可以用
    public class CreatePwd {
        public static void main(String[] args) {
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            String encode = bCryptPasswordEncoder.encode("123456");
            System.out.println(encode);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第五步:重新登录权限管理系统,分别使用zhangsan、lisi、wangwu进行登录测试,都可以正常进行登录,但左侧的菜单右侧会报 403 没有权限

    原因:

    • 在进行数据库权限校验的时候,他会默认给你定义的角色加上ROLE_前缀,解决的方法就是给所有角色都加上前缀ROLE_

    加完以后,你数据库中的效果应该如下:
    在这里插入图片描述
    修改完成以后,重新启动,然后分别登录,你将会看到如下截图:
    在这里插入图片描述

    9.动态展示功能菜单

    1.页面菜单动态展示

    使用Spring Security提供的标签库来动态判断,只有拥有指定角色的人,才可以访问我们指定的功能模块

    具体做法如下,找到main.html进行修改:

    <ul class="nav flex-column">
        <li class="nav-item border-bottom" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_PRODUCT')">
            <p><a href="#">产品管理a>p>
            <ul>
                <li><a th:href="@{product/add}" target="container">添加产品a>li>
                <li><a th:href="@{product/findAll}" target="container">产品列表a>li>
            ul>
        li>
        <li class="nav-item border-bottom" sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_ORDER')">
            <p><a href="#">订单管理a>p>
            <ul>
                <li><a th:href="@{order/add}" target="container">添加订单a>li>
                <li><a th:href="@{order/findAll}" target="container">订单列表a>li>
            ul>
        li>
        <li class="nav-item border-bottom" sec:authorize="hasAnyRole('ROLE_ADMIN')">
            <p><a href="#">用户管理a>p>
            <ul>
                <li><a th:href="@{user/add}" target="container">添加用户a>li>
                <li><a th:href="@{user/findAll}" target="container">用户列表a>li>
            ul>
        li>
        <li class="nav-item border-bottom" sec:authorize="hasAnyRole('ROLE_ADMIN')">
            <p><a href="#">角色管理a>p>
            <ul>
                <li><a th:href="@{role/add}" target="container">添加角色a>li>
                <li><a th:href="@{role/findAll}" target="container">角色列表a>li>
            ul>
        li>
    ul>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    2.业务代码动态拦截

    假设一种场景,一个程序员,它使用zhangsan的账户登录系统后,闲来无事,他呢,自己又懂技术,想试试,在地址栏直接输入李四的订单页面,看看能不能进去,结果发现,进去了,这就是纰漏。

    在这里插入图片描述
    我们上一步所实现的只是表面你所看到的,也就是页面上实现了不同用户可以看到不同的菜单,但是在控制器层并没有拦截住,这就是导致问题的根本原因,一般我们的解决办法就是在业务层(控制器层也可以,但是不推荐),给相对应的方法或者相应的类添加角色判断注解,只有拥有相应角色的用户才能访问该方法或者该类

    • 在Spring Security中,一共支持3种注解都可以做到这个效果,而这三种注解的开启都是一个注解上进行开启,我接下来会把三个注解都打开,只使用第一种注解,其余两种会给大家注释掉,要记住,打开的哪个注解,就用哪个注解来限制访问,必须配套使用。这里演示三类注解,实际开发中,用一类即可!
    @SpringBootApplication
    //三种任选其一,不必全开,全开也没事,一定要注意标签的对应关系
    @EnableGlobalMethodSecurity(
            jsr250Enabled = true, //JSR-250注解
            prePostEnabled = true, //spring表达式注解
            securedEnabled = true //SpringSecurity注解,推荐使用
    )
    public class SpringBootSecurityApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringBootSecurityApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    修改OrderServiceImpl:我们就以这个类为例进行讲解,其余剩下的所有的实现都需要标注,可以在方法上标注注解,也可以在类上标注注解

    @Service
    @Transactional
    public class OrderServiceImpl implements OrderService {
        ...
        ...
    
        @RolesAllowed({"ROLE_ADMIN", "ROLE_ORDER"})//JSR-250注解
        //@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ORDER')")//spring表达式注解
        //@Secured({"ROLE_ADMIN", "ROLE_ORDER"})//SpringSecurity注解
        @Override
        public void save(Order Order) {
            int size = orderMap.size();
            int id = ++size;
            Order.setId(id);
            orderMap.put(id, Order);
        }
    
        @RolesAllowed({"ROLE_ADMIN", "ROLE_ORDER"})//JSR-250注解
        //@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_ORDER')")//spring表达式注解
        //@Secured({"ROLE_ADMIN", "ROLE_ORDER"})//SpringSecurity注解
        @Override
        public List<Order> findAll() {
            Collection<Order> Orders = orderMap.values();
            return new ArrayList<>(Orders);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    登录zhangsan,你再次输入lisi的添加订单地址,点击提交挺订单的时候,就会 403 权限不足,如果你连界面都不想展示出来
    在这里插入图片描述

    10.权限不足异常处理

    每次权限不足都出现是Spring Boot自己生成的的403页面,很不友好,当出现403异常以后,如何跳转到我们自定义的页面

    • 在解决问题之前,我们先定义自己的403没有权限的页面,以及通过控制器方法跳转到403.html,以上这几种情况还可以配置404、500等错误页面的跳转,如有需要也可以自行配置。

    在 templates 目录中创建 error 目录,在 error 目录中创建 403.html

    DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>没有权限title>
    head>
    <body>
    <h3>403,没有权限h3>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在 MainController 中添加跳转方法,代码如下:

    //跳转到错误页的方法
    @RequestMapping("/to403")
    public String to403() {
        return "error/403";
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以下几种方法任选其一使用即可,不必全部配置,推荐使用第二种Spring MVC提供的异常处理机制

    第一种: 在 SecurityConfig中配置一下代码即可

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // ...
       // ...   
    
        //异常处理,使用函数表达式的写法可以不用在单独写一个类,非常方便
        http.exceptionHandling()
            .accessDeniedHandler((request, response, ex) -> {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setHeader("Content-Type", "application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
                out.flush();
                out.close();
            });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    第二种: 创建一个包 advice ,然后创建 ExceptionAdvice

    @ControllerAdvice
    public class ExceptionAdvice {
        //别导错类了:org.springframework.security.access.AccessDeniedException
        //只有出现AccessDeniedException异常才调转403.html页面
        @ExceptionHandler(AccessDeniedException.class)
        public String exceptionAdvice() {
            return "forward:/to403";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    11.保证当前登录人数

    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    		//........
            //1、保证当前登录人数——单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
            //maximumSessions 表示配置最大会话数为 1,这样后面的登录就会自动踢掉前面的登录
            //http.sessionManagement().maximumSessions(1).expiredUrl("/login");
    
            //2、 保证当前登录人数——单用户登录,如果有一个登录了,同一个用户在其他地方不能登录,禁止新的登录
            http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    12.开启或关闭CORS

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       // ...
       // ...
        //开启CORS
        http.cors();
         //关闭CORS
       // http.cors().disable();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    【第一篇】SpringSecurity的初次邂逅
    SpringSecurity常用过滤器介绍
    SpringSecurity认证流程分析
    SpringSecurity实现数据库认证

    SpringSecurity详细介绍RememberMe功能
    SpringSecurity详细介绍RememberMe源码流程
    SpringSecurity认证专题之【AuthenticationManager】
    详细介绍OAuth2.0及实现和SpringSecurity的整合应用

  • 相关阅读:
    git常用命令记录
    【docker】docker搭建nginx的ssl模块
    GBase 8d的特性-可用性
    在Kubernetes上部署Spring Boot微服务实践
    数据库设计三大范式
    牛客小白月赛#55
    C++-指针
    华为云云耀云服务器L实例评测|教你搭建第一个Java程序
    六、01【Java 多线程】之重温操作系统
    设计模式学习(九):装饰器模式
  • 原文地址:https://blog.csdn.net/qq877728715/article/details/127572467