授权是根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则可正常访问,没有访问的权限时则会被拒绝访问。
认证是为了保证用户身份的合法性,而授权则是为了更细粒度地对隐私数据进行划分,授权是在认证通过后发生的,以控制不同的用户访问不同的资源。
Spring Security提供了授权方法,开发者通过这些方法进行用户访问控制.
Spring Security授权流程
实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源。Spring Security使用标准Filter建立了对Web请求的拦截,最终实现对资源的授权访问。
①拦截请求。已认证用户访问受保护的Web资源将被SecurityFilterChain中FilterSecurityInterceptor实例对象拦截。
②获取资源访问策略。FilterSecurityInterceptor实例对象会通过SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource实例对象中获取要访问当前资源所需要的权限,权限封装在Collection实例对象中。 SecurityMetadataSource是读取访问策略的抽象,具体读取的内容,就是开发者配置的访问规则。
③FilterSecurityInterceptor通过AccessDecisionManager进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。AccessDecisionManager中包含一系列AccessDecisionVoter,可对当前认证过的身份是否有权访问对应的资源进行投票,AccessDecisionManager根据投票结果做出最终决策。
Spring Security自定义授权
根据授权的位置和形式,通常可以将授权的方式分为Web授权和方法授权,这两种授权方式都会调用AccessDecisionManager进行授权决策。下面分别对这两种自定义授权的方式进行讲解。
1.Web授权
Spring Security的底层实现本质是通过多个Filter形成的过滤器链完成,过滤器链中提供了默认的安全拦截机制,设置安全拦截规则,以控制用户的访问。HttpSecurity是SecurityBuilder接口的实现类,是HTTP安全相关的构建器,Spring Security中可以通过HttpSecurity对象设置安全拦截规则,并通过该对象构建过滤器链。
HttpSecurity可以根据不同的业务场景,对不同的URL采用不同的权限处理策略。当开发者需要配置项目的安全拦截规则时,可以调用HttpSecurity对象对应的方法实现。
HttpSecurity类的常用方法
方法 |
作用 |
authorizeRequests() |
开启基于HttpServletRequest请求访问的限制 |
formLogin() |
开启基于表单的用户登录 |
httpBasic() |
开启基于HTTP请求的Basic认证登录 |
logout() |
开启退出登录的支持 |
sessionManagement() |
开启Session管理配置 |
rememberMe() |
开启“记住我”功能 |
csrf() |
配置CSRF跨站请求伪造防护功能 |
通过authorizeRequests()方法可以添加用户请求控制的规则,这些规则通过用户请求控制的相关方法指定。
用户请求控制的常用方法
方法 |
作用 |
antMatchers(String... antPatterns) |
开启Ant风格的路径匹配 |
mvcMatchers(String... patterns) |
开启MVC风格的路径匹配,与Ant风格类似 |
regexMatchers(String... regexPatterns) |
开启正则表达式的路径匹配 |
and() |
功能连接符 |
anyRequest() |
匹配任何请求 |
rememberMe() |
开启“记住我”功能 |
access(String attribute) |
使用基于SpEL表达式的角色进行匹配 |
方法 |
作用 |
hasAnyRole(String... roles) |
匹配用户是否有参数中的任意角色 |
hasRole(String role) |
匹配用户是否有某一个角色 |
hasAnyAuthority(String... authorities) |
匹配用户是否有参数中的任意权限 |
hasAuthority(String authority) |
匹配用户是否有某一个权限 |
authenticated() |
匹配已经登录认证的用户 |
fullyAuthenticated() |
匹配完整登录认证的用户(非rememberMe登录用户) |
hasIpAddress(String ipaddressExpression) |
匹配某IP地址的访问请求 |
permitAll() |
无条件对请求进行放行 |
通过HttpSecurity类的formLogin()方法开启基于表单的用户登录后,可以指定表单认证的相关设置。
基于表单的身份验证的常见方法
方法 |
作用 |
loginPage(String loginPage) |
指定自定义登录界面,不使用SpringSecurity默认登录界面 |
loginProcessingUrl(String loginProcessingUrl) |
指定处理登录的请求url,为表单提交用户信息的Action |
successForwardUrl(String forwardUrl) |
指定登录成功后默认跳转的路径 |
下面通过案例演示在Spring Boot项目中使用Spring Security的Web授权方式进行权限管理。
(1)导入登录页面
在项目的resources目录的templates文件夹中导入自定义的登录页面login.html。
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>图书管理系统</title>
<link rel="stylesheet" type="text/css" th:href="@{/css/webbase.css}">
<link rel="stylesheet" type="text/css" th:href="@{/css/pages-login-manage.css}">
</head>
<body>
<div class="loginmanage">
<div class="py-container">
<h4 class="manage-title">图书管理系统</h4>
<div class="loginform">
<ul class="sui-nav nav-tabs tab-wraped">
<li class="active">
<h3>账户登录</h3>
</li>
</ul>
<div class="tab-content tab-wraped">
<div id="profile" class="tab-pane active">
<form id="loginform" class="sui-form" th:action="@{/doLogin}" method="post">
<div class="input-prepend"><span class="add-on loginname">用户名</span>
<input type="text" placeholder="用户名" class="span2 input-xfat" name="username">
</div>
<div class="input-prepend"><span class="add-on loginpwd">密码</span>
<input type="password" placeholder="请输入密码" class="span2 input-xfat" name="password">
</div>
<div class="logined">
<a class="sui-btn btn-block btn-xlarge btn-danger"
href='javascript:document:loginform.submit();' target="_self">登 录</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
(2)编辑配置类
在项目的WebSecurityConfig配置类中使用HttpSecurity对象设置安全拦截规则,并创建SecurityFilterChain对象交由Spring管理
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.mvcMatchers("/loginview","/css/**","/img/**").permitAll()
.mvcMatchers("/book/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // 任何请求,登录后可以访问
.and()
.formLogin()
.loginPage("/loginview")
.loginProcessingUrl("/doLogin")
.and()
.csrf().disable()//禁止csrf 跨站请求保护;
.headers().frameOptions().sameOrigin();
return http.build();
}
在WebMvcConfig配置类中添加loginview的视图映射
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("main");
registry.addViewController("/loginview").setViewName("login");
}
}
(3)测试效果
启动项目,在浏览器中通过“http://localhost:8080/”访问项目首页。
在登录页面使用zhangsan的用户信息进行登录。
图书管理需要角色为ROLE_ADMIN的用户才可以访问,用户zhangsan的角色为ROLE_COMMON,在后台首页单击“图书管理”链接。
使用用户lisi进行登录,lisi对应的角色为ROLE_ADMIN,登录成功再次访问“图书管理”。
2.方法授权
Spring Security默认是禁用方法级别的安全控制注解,可以使用@EnableGlobalMethodSecurity注解开启基于方法的安全认证机制,该注解可以标注在任意配置类上。
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,jsr250Enabled = true,
prePostEnabled = true)
public class WebSecurityConfig {……}
使用@Secured、@RolesAllowed和@PreAuthorize注解控制类中所有方法或者单独某个方法的访问权限,以实现对访问进行授权管理。
使用@Secured和@RolesAllowed注解时,只需在注解中指定访问当前注解标注的类或方法所需要具有的角色,允许多个角色访问时,使用大括号对角色信息进行包裹,角色信息之间使用分号分隔即可。
@RequestMapping("list")
@Secured({"ROLE_ADMIN","ROLE_COMMON"})
public String findList() {
return "book_list";
}
@RequestMapping("admin/manage")
@RolesAllowed("ROLE_ADMIN")
public String findManagList() {
return "book_manage";
}
@PreAuthorize注解会在方法执行前进行权限验证,支持SpEL表达式。
@RequestMapping("list")
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_COMMON')")
public String findList() {
return "book_list";
}
@RequestMapping("admin/manage")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String findManagList() {
return "book_manage";
}
@Secured、@RolesAllowed和@PreAuthorize注解都可以对方法的访问进行权限控制。
@Secured为Spring Security提供的注解。
@RolesAllowed为基于JSR 250规范的注解。
@PreAuthorize支持SpEL表达式。
案例:
(1)开启基于方法的安全认证机制
在项目的WebSecurityConfig配置类中开启基于方法的安全认证机制,并将类中HttpSecurity对象设置的访问“图书管理”拦截规则删除,以确保对资源授权地为方法授权,修改后的代码如下所示。
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true ,prePostEnabled = true )
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.mvcMatchers("/loginview","/css/**","/img/**").permitAll()
.anyRequest().authenticated() // 任何请求,登录后才可以访问
.and()
.formLogin()
.loginPage("/loginview")
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable()//禁止csrf 跨站请求保护;
.headers().frameOptions().sameOrigin();
return http.build();
}
}
(2)方法授权
在BookController类的findManagList()方法上使用注解指定访问该方法所需的角色
@Controller
@RequestMapping("book")
public class BookController {
@RequestMapping("list")
public String findList() {
return "book_list";
}
@RequestMapping("admin/manage")
@Secured("ROLE_ADMIN")
public String findManagList() {
return "book_manage";
}
}
(3)测试效果
启动项目,在浏览器中通过“http://localhost:8080/”访问项目首页后,使用用户zhangsan登录系统。
单击左侧的“图书管理”链接。
在用户登录页面使用用户lisi进行登录,登录成功后再次访问“图书管理”。
动态展示菜单
掌握动态展示菜单,能够通过Spring Security的授权管理实现动态展示菜单。
在前面的讲解中,只是通过Spring Security对后台资源的访问根据角色进行权限控制,前端页面并没有做任何处理,不同角色能看到的前端页面是一样的,即使当前用户没有对应的访问权限,依然能看到对应的菜单,用户体验较差。下面在前面案例的基础上,讲解如何使用Spring Security与Thymeleaf整合实现前端页面根据登录用户的角色动态展示菜单。
1.添加依赖
添加Thymeleaf与Spring Security 5的集成包:thymeleaf-extras-springsecurity5
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
2.修改页面代码
打开后台首页main.html,引入Spring Security安全标签,并在页面中根据需求使用Spring Security标签指定为不同角色显示不同的页面内容,实现动态展示控制
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="utf-8">
<title>网上图书馆</title>
<headers>
<frame-options policy="SAMEORIGIN" />
</headers>
<link rel="stylesheet" th:href="@{/css/bootstrap.css}">
<link rel="stylesheet" th:href="@{/css/AdminLTE.css}">
<link rel="stylesheet" th:href="@{/css/_all-skins.min.css}">
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/bootstrap.js}"></script>
<script type="text/javascript">
function SetIFrameHeight() {
var iframeid = document.getElementById("iframe");
if (document.getElementById) {
/*设置 内容展示区的高度等于页面可视区的高度*/
iframeid.height = document.documentElement.clientHeight;
}
}
</script>
</head>
<body class="hold-transition skin-green sidebar-mini">
<div class="wrapper">
<!-- 页面头部 -->
<header class="main-header">
<!-- Logo -->
<a th:href="@{/}" class="logo">
<span class="logo-lg"><b>网上图书馆</b></span>
</a>
<!-- 头部导航 -->
<nav class="navbar navbar-static-top">
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
<li class="dropdown user user-menu">
<a th:if="${session.user !=null}">
<img th:src="@{/img/user.jpg}" class="user-image"
alt="User Image">
<span class="hidden-xs" th:text="${session.user.name}"></span>
</a>
</li>
<li class="dropdown user user-menu">
<a>
<span class="hidden-xs">注销</span>
</a>
</li>
</ul>
</div>
</nav>
</header>
<!-- 页面头部 /-->
<!-- 导航侧栏 -->
<aside class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu">
<li sec:authorize="hasAnyAuthority('ROLE_COMMON','ROLE_ADMIN')">
<a th:href="@{/book/list}" target="iframe">
<i class="fa fa-circle-o"></i>图书阅读
</a>
</li>
<li sec:authorize="hasAuthority('ROLE_ADMIN')">
<a th:href="@{/book/admin/manag}" target="iframe">
<i class="fa fa-circle-o"></i>图书管理
</a>
</li>
</ul>
</section>
<!-- /.sidebar -->
</aside>
<!-- 导航侧栏 /-->
<!-- 内容展示区域 -->
<div class="content-wrapper">
<iframe width="100%" id="iframe" name="iframe" onload="SetIFrameHeight()"
frameborder="0" ></iframe>
</div>
</div>
</body>
</html>
3.效果测试
重启项目。
使用用户zhangsan的信息进行登录,登录后展示后台首页。
使用用户lisi的信息进行登录,登录后展示后台首页。