【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.导入所需依赖

springboot版本

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/>
    </parent>

基础依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>

        <!--重点-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.创建配置对象

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

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");
}
  • 使用内存方式配置用户以及权限,角色前边千万不能加前缀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>

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();
}

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

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>

修改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();
}

创建controller

@Controller
public class MainController {
    @RequestMapping("/main")
    public String main() {
        return "main";
    }

    //跳转到登录页的方法
    @RequestMapping("/login")
    public String toLogin() {
        return "login";
    }
}

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

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才能过滤。

找到main.html,把之前的a标签的get请求,换成form的post请求,并加上隐藏域csrf,csrf不用我们自己加,只要你是用的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.深入跨站请求伪造

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.5.ajax请求如何添加token

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

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

在这里插入图片描述

$(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);
    });
});

2.文件上传避免 CSRF 拦截

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

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

3.如何关闭 CSRF 防御机制

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
    ...
    //关闭CSRF跨站点请求仿造保护
    http.csrf().disable();
}

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>

配置 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两天,默认是两周
}

登录后关闭浏览器,然后重新打开 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;
    

修改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;
}

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

6.展示当前登录用户

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

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

1.引入依赖

<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

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

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

第一种:打开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>

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

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

在这里插入图片描述

7.对接数据库中数据

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

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

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

public interface SysUserDetailsService extends UserDetailsService {
    
}

第二步:实现自己的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);
    }
}

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

@Autowired
private SysUserDetailsService sysUserDetailsServiceImpl;

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

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

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

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

@Autowired
private BCryptPasswordEncoder passwordEncoder;

@Override
public void save(SysUser sysUser) {
    sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
    sysUserMapper.save(sysUser);
}

第三步:去掉 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);
}

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

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

第五步:重新登录权限管理系统,分别使用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>

在这里插入图片描述

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

修改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);
    }
}

登录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>

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

//跳转到错误页的方法
@RequestMapping("/to403")
public String to403() {
    return "error/403";
}

以下几种方法任选其一使用即可,不必全部配置,推荐使用第二种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();
        });
}

在这里插入图片描述

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

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

在这里插入图片描述

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

12.开启或关闭CORS

@Override
protected void configure(HttpSecurity http) throws Exception {
   // ...
   // ...
    //开启CORS
    http.cors();
     //关闭CORS
   // http.cors().disable();
}

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

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

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
1. 引入依赖 在pom.xml文件中引入Spring Security依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` 2. 添加配置类 在项目中添加一个配置SecurityConfig,继承自WebSecurityConfigurerAdapter。 ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/register").permitAll() // 登录注册页面不需要验证 .anyRequest().authenticated() // 其他页面需要验证 .and() .formLogin() .loginPage("/login") // 登录页面 .defaultSuccessUrl("/index") // 登录成功后的默认跳转页面 .and() .logout() .logoutUrl("/logout") // 退出登录的URL .logoutSuccessUrl("/login") // 退出登录后跳转到的页面 .invalidateHttpSession(true) .deleteCookies("JSESSIONID"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() // 在内存中存储用户信息 .withUser("admin").password("{noop}admin").roles("ADMIN") .and() .withUser("user").password("{noop}user").roles("USER"); } } ``` 3. 实现登录与注册功能 创建登录页面login.html和注册页面register.html,使用Thymeleaf模板引擎渲染页面。 在Controller中添加登录和注册的请求处理方法。 ``` @Controller public class UserController { @GetMapping("/login") public String login() { return "login"; } @PostMapping("/login") public String loginSuccess() { return "redirect:/index"; } @GetMapping("/register") public String register() { return "register"; } @PostMapping("/register") public String registerSuccess() { return "redirect:/login"; } } ``` 4. 实现用户鉴权功能 在SecurityConfig中重写configure(AuthenticationManagerBuilder auth)方法,实现用户信息的认证。 这里使用inMemoryAuthentication()方法在内存中存储用户信息,实际应用中可以使用数据库存储。 ``` @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() // 在内存中存储用户信息 .withUser("admin").password("{noop}admin").roles("ADMIN") .and() .withUser("user").password("{noop}user").roles("USER"); } ``` 在Controller中添加需要鉴权的请求处理方法,并在方法上添加@PreAuthorize注解指定需要的权限。 ``` @Controller public class UserController { @GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public String admin() { return "admin"; } @GetMapping("/user") @PreAuthorize("hasRole('USER')") public String user() { return "user"; } } ``` 5. 配置登录认证SecurityConfig中重写configure(HttpSecurity http)方法,配置登录认证信息。 ``` @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/register").permitAll() // 登录注册页面不需要验证 .anyRequest().authenticated() // 其他页面需要验证 .and() .formLogin() .loginPage("/login") // 登录页面 .defaultSuccessUrl("/index") // 登录成功后的默认跳转页面 .and() .logout() .logoutUrl("/logout") // 退出登录的URL .logoutSuccessUrl("/login") // 退出登录后跳转到的页面 .invalidateHttpSession(true) .deleteCookies("JSESSIONID"); } ``` 6. 测试 启动应用,在浏览器中访问http://localhost:8080/login,输入用户名和密码进行登录,登录成功后跳转到首页。 访问http://localhost:8080/admin和http://localhost:8080/user,根据用户角色的不同,页面会有不同的显示。如果访问没有权限的页面,会自动跳转到登录页面。在登录状态下访问/logout可以退出登录。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墩墩分墩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值