Spring Security

2021-01-22/2021-06-24

参考文献:
Spring Security 进阶篇
Spring Security在Spring Boot环境下的学习

示例代码:
链接: https://pan.baidu.com/s/1HYAuU9IwuGbTe9YzjFdv7A 密码: s41m

一、Spring Security 概述

1.1 框架概述

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

1.2 常用术语

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

1.3 常用单词

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

1.4 环境准备

打开基础代码:

请在配套资料中,找到 spring boot 专用基础代码,使用 Idea 打开 spring-boot-security,这只是一个很普通的 spring boot + mybatis 项目,如果你有 spring boot + mybatis 项目的基础,相信你一定能看得懂,我们从左侧的菜单栏可以看到有四个部分,其中“产品管理”、“订单管理”虽然可以进行添加和查询所有,但是,这两个功能并没有和数据库交互,为了防止污染数据库表,让大家看起来很乱,所以我就使用 map 结构在 service 层进行了数据模拟;“用户管理”和“角色管理”我已经实现了最基础的增加和查询所有的功能,因为这并不是我们学习的重点,所以一些基本的配置和页面编写我就帮大家完成了。

我们正好趁此机会看着下边的页面,以这个项目为基础来进行学习 Spring Security,最终实现的效果就是:

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

790ce9760d67082eaa74a02d1296517c.png

然后导入数据库,修改数据库连接并运行项目查看

二、Spring Security 的基础使用

2.1 导入所需依赖

1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-security</artifactId>
4</dependency>

2.2 创建配置对象

因为我们已经引入了spring-boot-starter-security,默认会帮我们自动配置好 Spring Security 的所有配置,我们要是想要修改默认配置,只要重写里边的指定方法即可。我们创建指定配置对象,用于修改默认配置:com.caochenlei.config.SecurityConfig

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

2.3 使用默认账户

当我们做完以上工作,我们就可以使用 Spring Security 的功能了,当你启动你的项目,在地址栏输入:http://localhost:8080,如果能够正常运行,那么,你将会看到如下界面:

b20e2ffbae4a4dbf38c9407009840c35.png

默认账号:user

默认密码:控制台有

5f82d3b1491e60d60265fd0558368b6b.png

2.4 配置登录用户

我们也看到了,使用 Spring Boot 帮我们配置好的账户和密码,难免有些不方便,我们如何自己指定用户和密码以及用户和密码所对应的角色呢,那么就需要用到配置类了,在配置类中,加入下边这段代码。

1@Override
2protected void configure(AuthenticationManagerBuilder auth) throws Exception {
3    // super.configure(auth);
4    auth.inMemoryAuthentication().withUser("user").password("{noop}123456").roles("USER");
5    auth.inMemoryAuthentication().withUser("admin").password("{noop}123456").roles("ADMIN");
6}

这里一定要注意,角色前边千万不能加前缀 ROLE_,否则你会连工程都起不来,这是规定。重新启动项目,访问项目首页,试试新配制的用户和密码好不好用。

2.5 退出当前登录

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

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

2.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'.

916e03de47c3591123e2616a1b9a560a.png

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

  • DENY:浏览器拒绝当前页面加载任何 frame 页面
  • SAMEORIGIN:frame 页面的地址只能为同源域名下的页面
  • ALLOW-FROM:origin为允许frame加载的页面地址

既然清楚了问题的来源,那我们也就好解决这个问题了,有两种解决办法,第一种就是我们关掉 Spring Security 对 frame 的拦截;

另外一种就是将 X-Frame-Options 设置为 SAMEORIGIN,也就是只能是我们同域名下的请求访问,当然了,这种拦截机制肯定是为了保证系统的安全性,如果关掉了,有点太可惜了,我在这里给出两种解决方案的配置,但是我会采用第二种,而不是第一种的关闭。

第一种:

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3    //关闭X-Frame-Options响应头
4    http.headers().frameOptions().disable();
5}

第二种:

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3    //设置X-Frame-Options响应头为SAMEORIGIN
4    http.headers().frameOptions().sameOrigin();
5}

2.7 指定登录界面

虽然默认的登录页面还不错,往往项目中的静态页面已经是前端开发好的,包括登录页面,我们想要使用自己的登录界面该怎么办?我们不妨转换一下思维,使用自带的页面,我们先打开源码,看看他是怎么写的,按照他的这个模式,我们模仿着写到自己的登录界面中不就好了,为了节约大家的时间,我就在下边贴出来了关键部分,你也可以自己打开尝试,如下所示:

178e52304b07d3368d12f7e3ed8be4bd.png

我们会发现,他的这个登录页面没有什么特别的,就是一个 form 表单,里边有两个文本框,一个是账号,一个是密码,还有最下边多了一个特殊的 hidden 隐藏域,这个隐藏域他是为了防止 csrf 跨站破坏的,这个值每一次启动项目都不一样,是一个动态值,他是为了标识当前请求一定是我们自己的请求,而不是别的网站仿造的请求,我们的所有请求都需要携带上这个标签上边的 value 值,我们也称这个值为 token 值,如果使用的是 thymeleaf,那么 form action 会帮我们自动加上 csrf 隐藏域,这样我们不用什么特殊处理也就可以登录了,我们找到我们工程中的 login.html,里边是一个空的 html,请把以下代码复制进入。下边是我们自己定义的一个登录页面。

 1<!DOCTYPE html>
 2<html xmlns:th="http://www.thymeleaf.org">
 3<head>
 4    <title>自定义登录页</title>
 5    <link rel="stylesheet" th:href="@{css/bootstrap.min.css}">
 6</head>
 7<body>
 8<div class="container mt-4">
 9    <form th:action="@{/login}" method="post">
10        <div class="form-group">
11            <label for="username">用户:</label>
12            <input type="text" class="form-control" id="username" name="username" placeholder="请输入用户" required>
13        </div>
14        <div class="form-group">
15            <label for="password">密码:</label>
16            <input type="text" class="form-control" id="password" name="password" placeholder="请输入密码" required>
17        </div>
18        <div class="form-group form-check">
19            <input type="checkbox" class="form-check-input" id="autoLogin">
20            <label class="form-check-label" for="autoLogin">自动登录</label>
21        </div>
22        <button type="submit" class="btn btn-primary">登录</button>
23    </form>
24</div>
25<script th:src="@{js/jquery-3.5.1.min.js}"></script>
26<script th:src="@{js/bootstrap.bundle.min.js}"></script>
27</body>
28</html>

我们编写好自己的登录页面,还得需要告诉 Spring Security 你登录的时候不要使用你自己的登录界面了使用我的,我们只需要在配置对象中编写如下配置即可,然后重新启动项目,我们来看一看效果,是不是可以了。

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

2.8 开放静态资源

最终我们启动后,发现确实来到了我们自己定义的登录页面了,说明我们之前的配置没有任何问题,但是,好像干干巴巴,啥样式都没有,这是为什么呢?如果你能看到样式,你清理一下浏览器缓存或者 CTRL+F5 强制刷新一下,就看不到了,至于原因,我们不难想到,刚才我们只是对跳转到登录页的请求进行了放行,而 Spring Security 默认是拦截所有请求,那肯定也包括静态资源 css、js、img 之类的,因此,静态资源是应该要被放行的,静态资源是不需要进行保护的,我们需要在 SecurityConfig 配置如下代码来放行静态资源。

1@Override
2public void configure(WebSecurity web) throws Exception {
3    web.ignoring().antMatchers("/css/**");
4    web.ignoring().antMatchers("/img/**");
5    web.ignoring().antMatchers("/js/**");
6    web.ignoring().antMatchers("/favicon.ico");
7}

06255a6b3817e32d40dfbb4d9fa9d12e.png

2.9 指定退出页面

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

而我们想要解决这个问题,其实很简单,我们给退出指定一个退出页面,只需要加入以下这段配置,很类似我们配置登录页的时候的代码:

 1@Override
 2protected void configure(HttpSecurity http) throws Exception {
 3    ...
 4    ...
 5    //设置自定义登出界面
 6    http.logout()//启用退出登录
 7        .logoutUrl("/logout")//退出处理程序,Spring Security内置控制器方法
 8        .logoutSuccessUrl("/toLogin")//退出成功跳转地址
 9        .invalidateHttpSession(true)//清除当前会话
10        .deleteCookies("JSESSIONID")//删除当前Cookie
11        .permitAll();
12}

即使加上了指定退出页的配置,当你登录后,点击注销,还是报 404 找不到资源,如果大家是跟着一步一步走来的,那就应该见过下边这个页面,当你在地址栏也好,还是 a 标签中也好,只要请求路径是:http://localhost:8080/logout,你就会看见下边这个是否确认注销的页面,你输入的刚才那个请求并不是真正退出,他还会问你是不是要退出,只有当你点击了这个 Log Out,才是真正退出,你看他的源码,他是向 /logout 发送了一个 post 请求,并且还携带了 csrf 这个隐藏域,那我们是不是就可以仿照他这种形式,修改一下我们自己的退出功能呢。

83197672154dddbeaf60d9e9974b6941.png

找到 main.html,把之前的 a 标签的 get 请求,换成 form 的 post 请求,并加上隐藏域 csrf,csrf 不用我们自己加,只要你是用的thymeleaf 的 form,他会帮我们加上,不信的话,可以启动工程,右键查看源代码,看看是不是会自动生成一个 csrf 隐藏域。

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

三、Spring Security 的高级使用

3.1 深入跨站请求伪造

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

3.1.1 CSRF 的原理

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

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

3.1.2 CSRF 的防御

目前防御 CSRF 攻击主要有三种策略:验证 HTTP Referer 字段;在请求地址中添加 token 并验证(Spring Security采用);在 HTTP 头中自定义属性并验证。

(1) 验证 HTTP Referer 字段

根据 HTTP 协议,在 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 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 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 的最后加上 <input type="hidden" name="csrftoken" value="tokenvalue"/>,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。

该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。

在 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 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

3.1.3 form 表单如何添加 token

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

如果自己想要设置,我们也可以使用隐藏域自己设置,一般我们不会设置这个,默认就有你设置他干啥,参考代码如下:

1<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

3.1.4 ajax 请求如何添加 token

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

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

3.1.5 文件上传避免 CSRF 拦截

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

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

3.1.6 如何关闭 CSRF 防御机制

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

3.2 自动登录

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

打开 login.html 修改自动登录的 name 为 remember-me,这是一个默认名称,可以修改,但是一般我们就叫这个名

1<div class="form-group form-check">
2    <input type="checkbox" class="form-check-input" id="autoLogin" name="remember-me">
3    <label class="form-check-label" for="autoLogin">自动登录</label>
4</div>

配置 SecurityConfig 开启自动登录功能

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

打开 http://localhost:8080/ ,登录以后,我们在关闭浏览器,然后重新打开 http://localhost:8080/ ,发现仍然可以访问,并且这时候不需要登录,他是怎么做到的呢?其实,在登录成功以后会往当前网站的 cookie 中写入一个自动登录的 token 值,当我们下次启动的时候,只要这个cookie没有消失,Spring Security 就能拿到这个 cookie 的中保存的 token 的值,然后帮我们自动登录认证。

a4d8a08088701d7cc390fd29b0e814c4.png

3.3 保存凭据到数据库

自动登录功能方便是大家看得见的,但是安全性却令人担忧。因为 cookie 毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。 此外,Spring Security 还提供了 remember-me 的另一种相对更安全的实现机制:在客户端的 cookie 中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用 cookie 中的加密串,到数据库表中验证,如果通过,自动登录才算通过。这样,自动登录功能的安全性就有了保证,因此,我们需要在数据库中创建一张用于保存自动登录信息的表,这张表是固定的,包括名称、字段等信息,都不能修改,否则会认识失败。

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

接下来,我们需要配置一下,告诉 Spring Security 使用哪一个 dataSource 来操作这个表

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

接下来,我们重新进行测试,发现也是可行的,并且这里给出了浏览器和数据库的截图信息:

649db38bbef907728eff514d7cab9723.png

19e87dc799012a8c11bf29e2f8d79522.png

3.4 展示当前登录用户

登录成功以后,如何显示出来当前登录成功的用户名呢?我们这里给出两种常用方法,他们都必须使用 Spring Security 的标签库,在使用 thymeleaf 渲染前端的 html 时,thymeleaf 为 SpringSecurity 提供的标签属性,首先需要引入 thymeleaf-extras-springsecurity5 依赖支持。

(1) 在pom 文件中的引入springsecurity的标签依赖thymeleaf-extras-springsecurity5。

1<dependency>
2  <groupId>org.thymeleaf.extras</groupId>
3  <artifactId>thymeleaf-extras-springsecurity5</artifactId>
4</dependency>

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

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

第一种:打开 main.html , 修改

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

第二种:打开 main.html, 修改

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

修改完成,看看页面能够显示当前用户:

c71deb43035e559c18d6c21fd2873fad.png

3.5 对接数据库中数据

我们现在已经在内存中(代码写死的就在内存中)配置好了两个用户(user、admin)以及他们所对应的角色,但是,在真实的企业开发中,这些信息显然是不能保存在配置文件中的,因为要动态添加删除用户以及角色,我们就需要使用数据库来保存,现在 Spring Security 默认是走的配置对象中的账户和密码,我们如何对接数据库中的数据呢?

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

1public interface SysUserDetailsService extends UserDetailsService {   
2}

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

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

第三步:修改配置文件 SecurityConfig 中的认证提供者换成咱们自己定义的,然后重新启动权限管理系统使用数据库中的账户登录即可。

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

第四步:使用数据库所提供的账户进行登录测试。

3.6 用户密码进行加密

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

 1@SpringBootApplication
 2public class SpringBootSecurityApplication {
 3
 4    public static void main(String[] args) {
 5        SpringApplication.run(SpringBootSecurityApplication.class, args);
 6    }
 7
 8    @Bean
 9    public BCryptPasswordEncoder passwordEncoder(){
10        return new BCryptPasswordEncoder();
11    }
12}
 1@Autowired
 2private SysUserDetailsService sysUserDetailsServiceImpl;
 3
 4@Autowired
 5private BCryptPasswordEncoder passwordEncoder;
 6
 7@Override
 8protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 9    auth.userDetailsService(sysUserDetailsServiceImpl).passwordEncoder(passwordEncoder);
10}

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

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

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

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

第四步:手动修改数据库中的密码为加密后的密码,我们现在需要知道 123456 加密后的密文,需要手动生成,注意啊,每一次生成都不一样,但是都可以用

1public class CreatePwd {
2    public static void main(String[] args) {
3        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
4        String encode = bCryptPasswordEncoder.encode("123456");
5        System.out.println(encode);
6    }
7}

56dadc76c73308e85418c04f25059e1e.png

第五步:重新启动权限管理系统,分别使用 zhangsan、lisi、wangwu 进行登录测试,发现都可以正常进行登录,我在创建表的时候默认就给他们分配了权限。

但是注意:前方高能,你能够登录,但是肯定你点击左侧的菜单右侧会报 403 没有权限,还记得之前我说过,咱们在配置类中拦截的所有资源所对应的角色是不能带前缀 ROLE 否则会出问题,这是框架内部的一个机制,你打开数据库,会发现目前所有的角色都是没有 ROLE 前缀的,那不是对的吗,出错就出错在这里,在你对接数据库的时候,权限校验的时候,他会默认给你定义的角色加上 ROLE 前缀,因此,你就应该知道,你为什么没有权限了,数据库中的角色可没有 ROLE,解决的方法就是给所有角色都加上前缀 ROLE,加完以后,你数据库中的效果应该如下:

59256b88e023ce072fa5d8260d0e5c23.png

修改完成以后,重新启动,然后分别登录,你将会看到如下截图:

zhangsan:用户权限和产品权限

867f83f0acd743ee1317d1b297e1681c.png

lisi:用户权限和订单权限

b68bb5b06e2c790e8eb2020f9d7c5da5.png

wangwu:所有权限

74bc0e94256a8f81a2113d3ea02bad7e.png

为什么这三个账户都能登录成功,以下几个方面很重要:

  1. Spring Security 已经配置了加密登录,我们手动把数据库中的 123456 改成了密文,登录的时候才可以保证认证成功。

  2. 认证成功了可不一定能访问我们系统的资源,必须拥有相对应的角色,虽然角色是我们自己定义的,但是请你不要忘记,我们一开始,使用死的配置进行配置用户的时候,那个时候,只有拥有 USER 和 ADMIN 这样的用户才能访问系统资源,这就是为什么 zhangsan 和 lisi 必须要有 USER 角色了。

    1//拦截需要权限的资源
    2http.authorizeRequests().antMatchers("/**").hasAnyRole("USER", "ADMIN").anyRequest().authenticated();
    

3.7 动态展示功能菜单

3.7.1 页面菜单动态展示

细心的你应该发现了,无论是 zhangsan、lisi、wangwu 中的哪一个人登录进去,左侧的菜单都是下边这个样子,完全没有实现我们的效果

e5df3c828a635b3281ed1b4cff03fd85.png

我们可以使用 Spring Security 提供的标签库来动态判断,只有拥有指定角色的人,才可以访问我们指定的功能模块,具体做法如下,找到 main.html 进行修改:

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

我们保存以后,重新启动权限管理系统,再次分别登录zhangsan、lisi、wangwu,看看左侧菜单栏发生了什么变化

zhangsan:

c03ae469d32b07d5a49e67e055212d63.png

Lisi:

2fdbd09e58ad17bcadcde41f853b8b91.png

Wangwu:

e012f49a0c028b681be8b5e6609959c0.png

3.7.2 业务代码动态拦截

我们发现虽然界面上效果好像可以了,但是,难道就真的可以了吗?还有没有什么纰漏,我们假设一种场景,一个程序员,它使用 zhangsan 的账户登录系统后,闲来无事,他呢,自己又懂技术,想试试,在地址栏直接输入李四的订单页面,看看能不能进去,结果发现,进去了,这就是纰漏。

74eecc71b1baf5aff55cd3db7245c6b8.png

我们上一步所实现的只是表面你所看到的,也就是视图上实现了不同用户可以看到不同的菜单,但是在控制器层并没有拦截住,这就是导致问题的根本原因,一般我们的解决办法就是在业务层(控制器层也可以,但是不推荐),给相对应的方法或者相应的类添加角色判断注解,只有拥有相应角色的用户才能访问该方法或者该类,在 Spring Security 中,一共支持三种注解都可以做到这个效果,而这三种注解的开启都是一个注解上进行开启,我接下来会把三个注解都打开,只使用第一种注解,其余两种会给大家注释掉,要记住,打开的哪个注解,就用哪个注解来限制访问,必须配套使用。这里演示三类注解,实际开发中,用一类即可!

在主启动类上添加以下配置

 1@SpringBootApplication
 2//三种任选其一,不必全开,全开也没事,一定要注意标签的对应关系
 3@EnableGlobalMethodSecurity(
 4        jsr250Enabled = true, //JSR-250注解
 5        prePostEnabled = true, //spring表达式注解
 6        securedEnabled = true //SpringSecurity注解,推荐使用
 7)
 8public class SpringBootSecurityApplication {
 9    public static void main(String[] args) {
10        SpringApplication.run(SpringBootSecurityApplication.class, args);
11    }
12
13    @Bean
14    public BCryptPasswordEncoder passwordEncoder() {
15        return new BCryptPasswordEncoder();
16    }
17}

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

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

完成以后,重新启动权限管理系统,然后登录 zhangsan,你再次输入 lisi 的添加订单地址,看看还能不能访问,你会发现,添加订单界面还在,但是当你点击提交挺订单的时候,就会 403 权限不足,如果你连界面都不想展示出来,请你在控制层上标注相对应的注解即可。

ca8414ef74d4dd5a4085cd961addbb7e.png

3.8 权限不足异常处理

大家也发现了,每次权限不足都出现 403 页面,这个错误页面是 Spring Boot 自己生成的白页,非常的难看,很不友好,当出现 403 异常以后,如何跳转到我们自定义的页面,接下来,我将提供两种形式来解决,

  • 第一种是 Spring Security 提供的解决方式
  • 第二种是 Spring MVC 提供的解决方式

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

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

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

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

afaaa94b2ad21d9217d8183bd0f61dd0.png

第二种:

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

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

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

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

com.caochenlei.controller 中创建一个包 advice ,然后创建 ExceptionAdvice

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

b1af9f142cd742db2ca959ba2c0cd262.png

3.9 保证当前登录人数

有时候我们为了安全,也可以设置同一个账户,只能同时有一个人在线,我们只需要简单的配置就能实现。

第一种:单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3    ...
4    ...
5
6    //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
7    http.sessionManagement().maximumSessions(1).expiredUrl("/toLogin");
8}

第二种:单用户登录,如果有一个登录了,同一个用户在其他地方不能登录

1@Override
2protected void configure(HttpSecurity http) throws Exception {
3    ...
4    ...
5    //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
6    http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
7}

3.10 开启或关闭 CORS

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源(协议 + 域名 + 端口)服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS 需要浏览器和服务器同时支持。它的通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现 CORS 通信的关键是服务器。只要服务器实现了对 CORS 的支持,就可以跨源通信。

开启 CORS

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

关闭 CORS

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

四、SSM 集成 Spring Security

示例代码:SpringSecurity.zip

4.1 基础配置

pom.xml

 1<dependencies>
 2    <dependency>
 3        <groupId>org.springframework.security</groupId>
 4        <artifactId>spring-security-web</artifactId>
 5        <version>5.0.1.RELEASE</version>
 6    </dependency>
 7    <dependency>
 8        <groupId>org.springframework.security</groupId>
 9        <artifactId>spring-security-config</artifactId>
10        <version>5.0.1.RELEASE</version>
11    </dependency>
12</dependencies>

Web.xml

 1<!-- 配置加载类路径的配置文件 -->
 2<context-param>
 3    <param-name>contextConfigLocation</param-name>
 4    <param-value>classpath:applicationContext.xml,classpath:spring-security.xml</param-value>
 5</context-param>
 6<filter>
 7    <filter-name>springSecurityFilterChain</filter-name>
 8    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 9</filter>
10<filter-mapping>
11    <filter-name>springSecurityFilterChain</filter-name>
12    <url-pattern>/*</url-pattern>
13</filter-mapping>

4.2 Spring-security

认证授权由 spring-security 来控制 controller 层,我们写 service 和 dao 层就好

如果我们不自己 写登录界面,可以使用框架默认给我们提供的登录界面

 1<?xml version="1.0" encoding="UTF-8"?>
 2<beans xmlns="http://www.springframework.org/schema/beans"
 3       xmlns:security="http://www.springframework.org/schema/security"
 4       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 5       xsi:schemaLocation="http://www.springframework.org/schema/beans
 6    http://www.springframework.org/schema/beans/spring-beans.xsd
 7    http://www.springframework.org/schema/security
 8    http://www.springframework.org/schema/security/spring-security.xsd">
 9
10    <!-- 开启方法级权限控制注解使用,稍后讲解 -->
11    <security:global-method-security pre-post-annotations="enabled" jsr250-annotations="enabled"
12                                     secured-annotations="enabled"/>
13
14    <!-- 配置不拦截的资源 -->
15    <security:http pattern="/login.jsp" security="none"/>
16    <security:http pattern="/failer.jsp" security="none"/>
17    <security:http pattern="/css/**" security="none"/>
18    <security:http pattern="/img/**" security="none"/>
19    <security:http pattern="/plugins/**" security="none"/>
20
21    <!--
22        配置具体的规则
23        auto-config="true"	不用自己编写登录的页面,框架提供默认登录页面
24        use-expressions="false"	是否使用SPEL表达式
25    -->
26    <security:http auto-config="true" use-expressions="true">
27        <!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" -->
28        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
29
30        <!-- 定义跳转的具体的页面 
31            登录界面
32            登录url
33            默认主页
34            登录失败界面
35            登录成功界面
36        -->
37        <security:form-login
38                login-page="/login.jsp"
39                login-processing-url="/login.do"
40                default-target-url="/pages/main.jsp"
41                authentication-failure-url="/failer.jsp"
42                authentication-success-forward-url="/pages/main.jsp"
43        />
44
45        <!-- 关闭跨域请求 -->
46        <security:csrf disabled="true"/>
47        <!-- 退出 -->
48        <security:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/login.jsp"/>
49
50    </security:http>
51
52    <!-- 配置加密类 -->
53    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
54
55    <!-- 切换成数据库中的用户名和密码 -->
56    <security:authentication-manager>
57        <!--   user-service-ref 为实现用户登录的类  -->
58        <security:authentication-provider user-service-ref="userService">
59            <!-- 配置加密的方式-->
60            <security:password-encoder ref="passwordEncoder"/>
61        </security:authentication-provider>
62    </security:authentication-manager>
63    
64    <!-- <bean id="webexpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />-->
65    <!-- 提供了入门的方式,在内存中存入用户名和密码
66    <security:authentication-manager>
67    	<security:authentication-provider>
68    		<security:user-service>
69    			<security:user name="admin" password="{noop}admin" authorities="ROLE_USER"/>
70    		</security:user-service>
71    	</security:authentication-provider>
72    </security:authentication-manager>
73    -->
74
75</beans>

4.3 使用数据库认证

在 Spring Security 中如果想要使用数据进行认证操作,有很多种操作方式,这里我们介绍使用UserDetails、UserDetailsService来完成操作。

UserDetails

1public interface UserDetails extends Serializable {
2  Collection<? extends GrantedAuthority> getAuthorities();
3  String getPassword();
4  String getUsername();
5  boolean isAccountNonExpired();
6  boolean isAccountNonLocked();
7  boolean isCredentialsNonExpired();
8  boolean isEnabled();
9}

UserDetails 是一个接口,我们可以认为 UserDetails 作用是于封装当前进行认证的用户信息,但由于其是一个接口,所以我们可以对其进行实现,也可以使用 Spring Security 提供的一个 UserDetails 的实现类 User 来完成,以下是 User 类的部分代码

1public class User implements UserDetails, CredentialsContainer { 
2    private String password; 
3    private final String username; 
4    private final Set<GrantedAuthority> authorities; // 用户所拥有的权限
5    private final boolean accountNonExpired; //帐户是否过期 
6    private final boolean accountNonLocked; //帐户是否锁定 
7    private final boolean credentialsNonExpired; //认证是否过期 
8    private final boolean enabled; //帐户是否可用
9

UserDetailsService

1public interface UserDetailsService { 
2    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 
3}

我们需要定义一个 service 来实现 UserDetilsService,这个 service 的 loadUserByUsername 返回 UserDetails 的实现类对象 User

看一个例子

1public interface IUserService extends UserDetailsService {}
 1@Service("userService")
 2@Transactional
 3public class UserServiceImpl implements IUserService {
 4
 5    @Autowired
 6    private IUserDao userDao;
 7
 8    @Override
 9    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
10        UserInfo userInfo = null;
11        userInfo = userDao.findByUsername(username);
12        //处理自己的用户对象封装成UserDetails
13        User user = new User(userInfo.getUsername(), userInfo.getPassword(), userInfo.getStatus() != 0, true, true, true, getAuthority(userInfo.getRoles()));
14        return user;
15    }
16
17    //作用就是返回一个List集合,集合中装入的是角色描述
18    public List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {
19        List<SimpleGrantedAuthority> list = new ArrayList<>();
20        for (Role role : roles) {
21            list.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
22        }
23        return list;
24    }
25}

4.4 服务器端方法级权限控制

在服务器端我们可以通过 Spring security 提供的注解对方法来进行权限控制。Spring Security 在方法的权限控制上

支持三种类型的注解,JSR-250 注解、@Secured 注解和支持表达式的注解。

4.4.1 开启注解使用

这三种注解默认都是没有启用的,需要单独通过 global-method-security 元素的对应属性进行启用

配置文件开启

1<security:global-method-security pre-post-annotations="enabled" jsr250-annotations="enabled"
2                                 secured-annotations="enabled"/>

注解开启

注解开启

@EnableGlobalMethodSecurity :Spring Security 默认是禁用注解的,要想开启注解,需要在继承 WebSecurityConfifigurerAdapter的类上加 @EnableGlobalMethodSecurity 注解,并在该类中将 AuthenticationManager 定义为 Bean。

4.4.2 JSP-250 注解

  • @RolesAllowed 表示访问对应方法时所应该具有的角色,例如 @RolesAllowed({"USER", "ADMIN"})
  • @PermitAll 表示允许所有的角色进行访问,也就是说不进行权限控制
  • @DenyAll 是和 PermitAll 相反的,表示无论什么角色都不能访问

4.4.3 支持表达式注解

  • @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问

    1@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
    2void changePassword(@Param("userId") long userId ){ } 
    3// 这里表示在 changePassword 方法执行之前,判断方法参数 userId 的值是否等于 principal 中保存的当前用户的 userId
    4// 或者当前用户是否具有 ROLE_ADMIN 权限,两种符合其一,就可以访问该方法。
    
  • @PostAuthorize 允许方法调用,但是如果表达式计算结果为 false ,将抛出一个安全性异常

    1@PostAuthorize 
    2User getUser("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')");
    
  • @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果

  • @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值

4.4.4 Secured 注解

@Secured 注解标注的方法进行权限控制的支持,其值默认为 disabled。

1@Secured("IS_AUTHENTICATED_ANONYMOUSLY") 
2public Account readAccount(Long id); 
3@Secured("ROLE_TELLER")

4.5 页面标签权限控制

在 jsp 页面中我们可以使用 spring security 提供的权限标签来进行权限控制

4.5.1 导入

maven 导入

1<dependency>
2    <groupId>org.springframework.security</groupId>
3    <artifactId>spring-security-taglibs</artifactId>
4    <version>version</version>
5</dependency>

页面导入

1<%@taglib uri="http://www.springframework.org/security/tags" prefix="security"%>

4.5.2 常用标签

authentication 标签

1<security:authentication property="" htmlEscape="" scope="" var=""/>
  • property: 只允许指定 Authentication 所拥有的属性,可以进行属性的级联获取,如“principle.username”,不允许直接通过方法进行调用
  • htmlEscape:表示是否需要将 html 进行转义。默认为 true。
  • scope:与 var 属性一起使用,用于指定存放获取的结果的属性名的作用范围,默认我 pageContext。Jsp 中拥有的作用范围都进行进行指定
  • var: 用于指定一个属性名,这样当获取到了 authentication 的相关信息后会将其以 var 指定的属性名进行存放,默认是存放在 pageConext 中

authorize 标签

authorize 是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示

1<security:authorize access="" method="" url="" var=""></security:authorize>
  • access: 需要使用表达式来判断权限,当表达式的返回结果为 true 时表示拥有对应的权限
  • method:method 属性是配合 url 属性一起使用的,表示用户应当具有指定 url 指定 method 访问的权限,method 的默认值为GET,可选值为 http 请求的7种方法
  • url:url 表示如果用户拥有访问指定 url 的权限即表示可以显示 authorize 标签包含的内容
  • var:用于指定将权限鉴定的结果存放在 pageContext 的哪个属性中

accesscontrollist

accesscontrollist 标签是用于鉴定 ACL 权限的。其一共定义了三个属性:hasPermission、domainObject 和 var,其中前两个是必须指定的。

1<security:accesscontrollist hasPermission="" domainObject="" var=""></security:accesscontrollist>
  • hasPermission:hasPermission 属性用于指定以逗号分隔的权限列表
  • domainObject:domainObject 用于指定对应的域对象
  • var:var则是用以将鉴定的结果以指定的属性名存入pageContext中,以供同一页面的其它地方使用
评论
发表评论
       
       
取消