Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 的两大核心功能就是认证(authentication)和授权(authorization)
。
认证
操作。授权
操作。常用词汇
权限管理需要三个对象
认证
操作。授权
操作。当前权限名称,url地址等信息,可实现动态展示菜单
。角色
建立关联关系的。请在配套代码中,以及实现相关代码,直接拿来用就行了
源码地址
修改配置文件application.yml
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/数据库名称
username: root
password: 密码
springboot版本
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
<relativePath/>
parent>
基础依赖
<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>
@Configuration
@EnableWebSecurity//开启Spring Security对WebMVC的支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//请将对Spring Security的配置方法写在这个类中
}
完成上图操作后,就可以使用Spring Security的功能了,地址栏输入:http://localhost:8080
@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_
,否则会启动失败如果想要注销,访问: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>
当你使用用户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加载的页面地址
方案如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭X-Frame-Options响应头
http.headers().frameOptions().disable();
}
SAMEORIGIN
,也就是只能是我们同域名下的请求访问,当然了,这种拦截机制肯定是为了保证系统的安全性,如果关掉了,有点太可惜了,这里采用第二种,而不是第一种的关闭。@Override
protected void configure(HttpSecurity http) throws Exception {
//设置X-Frame-Options响应头为SAMEORIGIN
http.headers().frameOptions().sameOrigin();
}
想要使用自己的登录界面该怎么办?先打开源码,看看他是怎么写的,按照他的这个模式,我们模仿着写到自己的登录界面中不就好了
内置登录页面很简单,就是一个form表单,里边有两个文本框,一个是账号,一个是密码,还有最下边多了一个特殊的hidden隐藏域,这个隐藏域他是为了防止csrf跨站破坏
的,这个值每一次启动项目都不一样,是一个动态值
,他是为了标识当前请求一定是我们自己的请求,而不是别的网站仿造的请求
,我们的所有请求都需要携带上这个标签上边的value值,我们也称这个值为token值
,如果使用的是thymeleaf,那么form action会帮我们自动加上csrf 隐藏域,这样我们不用什么特殊处理也就可以登录了
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";
}
}
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#/");
}
当你现在想要退出登录,点击右上角咱们之前配置好的注销,你就会神奇的发现,好像不能退出了,这是因为,默认退出会直接跳转到/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>
CSRF跨站点请求伪造(Cross—Site Request Forgery
),跟XSS攻击一样,存在巨大的危害性,你可以这样来理解:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作
,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
假设:其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,用户C为Web A网站的合法用户。
目前防御 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 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。
缺点:每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。事实上,对于某些浏览器,比如 IE6 或 FF2
,目前已经有一些方法可以篡改 Referer 值。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。
2.在请求地址中添加 token 并验证
优点:
这种方法要比检查 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 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。
如果您使用的是thymeleaf,如果使用的是thymeleaf,那么form action会帮我们自动加上csrf 隐藏域
,我们不用特殊处理。
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.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);
});
});
请将MultipartFilter
在Spring Security过滤器之前指定。MultipartFilter在Spring Security过滤器之前指定,这意味着任何人都可以在您的服务器上放置临时文件。但是,只有授权用户才能提交由您的应用程序所处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。具体配置代码如下:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
...
...
//关闭CSRF跨站点请求仿造保护
http.csrf().disable();
}
如果我想要关闭浏览器,下次再打开浏览器,权限管理系统会自动根据我上次的登录状态进行登录,这就是登录常用的“自动登录功能”,要想实现自动登录功能,我们需要实现两处关键配置就能使用了,具体操作如下:
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的值,然后帮我们自动登录认证。自动登录功能方便是大家看得见的,但是安全性却令人担忧。因为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;
}
重新进行测试,发现也是可行的,并且这里给出了浏览器和数据库的截图信息:
登录成功以后,如何显示出来当前登录成功的用户名呢?
Spring Security的标签库
,在使用thymeleaf渲染前端的html时,thymeleaf为SpringSecurity提供的标签属性,首先需要引入thymeleaf-extras-springsecurity5
依赖支持。1.引入依赖
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
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>
目前是在内存中(代码写死的就在内存中)配置好了两个用户(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);
}
第一步:配置加密对象,然后设置给咱们自己的认证提供者
。
@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_
加完以后,你数据库中的效果应该如下:
修改完成以后,重新启动,然后分别登录,你将会看到如下截图:
使用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>
假设一种场景,一个程序员,它使用zhangsan的账户登录系统后,闲来无事,他呢,自己又懂技术,想试试,在地址栏直接输入李四的订单页面,看看能不能进去,结果发现,进去了,这就是纰漏。
我们上一步所实现的只是表面你所看到的,也就是页面上实现了不同用户可以看到不同的菜单
,但是在控制器
层并没有拦截住,这就是导致问题的根本原因,一般我们的解决办法就是在业务层(控制器层也可以,但是不推荐),给相对应的方法或者相应的类添加角色判断注解,只有拥有相应角色的用户才能访问该方法或者该类
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 权限不足,如果你连界面都不想展示出来
每次权限不足都出现是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";
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//........
//1、保证当前登录人数——单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
//maximumSessions 表示配置最大会话数为 1,这样后面的登录就会自动踢掉前面的登录
//http.sessionManagement().maximumSessions(1).expiredUrl("/login");
//2、 保证当前登录人数——单用户登录,如果有一个登录了,同一个用户在其他地方不能登录,禁止新的登录
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}
@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的整合应用