一.概述
1.框架概述
Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 的两大核心功能就是认证(authentication)和授权(authorization)
。
- 认证 :你是什么人。
- 授权 :你能做什么。
- 用户 :主要包括用户名称、用户密码和当前用户所拥有的角色信息,可用于实现
认证
操作。 - 角色 :主要包括角色名称、角色描述和当前角色所拥有的权限信息,可用于实现
授权
操作。
常用词汇
- 认证 :authentication
- 授权 :authorization
- 用户 :user
- 角色 :role
- 登录 :login
- 注销 :logout
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.导入所需依赖
<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
默认的账户是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 值
就会是转账按钮所在的页面的 UR
L,通常是以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还提供了r
emember-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();
}