SpringSecurity
在Web开发中,安全第一位。
安全不是一个功能性需求,也就是说如果不设置安全方面的功能,网站也一样能跑起来。
安全应该在什么时候考虑起来呢?应该在设计之初,因为不能存在安全的问题,比如网站漏洞,隐私泄露等,而且一旦架构确定,增加安全可能需要改动一些代码,所以在设计网站开发的一开始,就要考虑安全的问题。
涉及到安全方面的框架有Shiro和SpringSecurity,它们是很相似的,功能差不多类似,都有认证和授权这两个主要的功能。
SpringSecurity是一个强大的并且可定制化的身份认证和权限控制的框架
Spring Security是针对Spring项目的安全框架,也是SpringBoot底层安全模块默认的技术选型,它可以实现强大的Web安全控制。
以前我们做安全的时候用的都是拦截器,过滤器,有大量的原生代码,是很冗余的,我们从MVC框架到Spring然后到SpringBoot都是一路在简化代码,SpringSecurity也是抱着这样的目的。
创建一个新的SprngBoot项目,这里引用了web依赖和themleaf的依赖。
导入素材,几个页面和css,js,这里供测试的素材来自于秦老师,素材是一个简单的demo,这个demo用于权限功能的展示,首页仅有一个登录链接,跳向一个登录页面,以及三个Level的链接,跳向levelx-x的页面,里面仅仅是levelx-x的一段文字。我们在自己的项目里写一个路由功能的Controller,用于页面跳转,然后启动SpringBoot。
我们想要达到的效果:Level3的页面level1,level2是不能访问的;登录以后应该仅看到level等级对应的内容,而不是全部内容。
我们对权限的控制是基于AOP的思想横切进去,跟我们原来的功能代码无关。
配置SpringSecurity
我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。
从SpringBoot的官网上可以找到教程,具体参见 Securing a Web Application 以及Spring Security Reference
需要记住三个类
- WebSecurityConfiguationAdapter: 自定义Security策略
- AuthenticationMangerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity策略
Spring Security的两个主要目的是 认证和授权(访问控制)
这个概念是通用的,而不只是在Spring Security存在,Shiro也是基于这种思想。
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 自定义Security策略
自定义配置需要用到一个自定义的Config类,来继承WebSecurityConfiguationAdapter。
重写WebSecurityConfiguationAdapter的configure(HttpSecurity)方法,它可以定义哪些URL路径可以被保护,哪些路径不需要被保护。
👇是网页翻译spring官方文档得来的结果,写的还蛮详细。
所以现在我们来写一个 自定义的Config,继承了WebSecurityConfigurerAdapter 类,然后重写configure(HttpSecurity)方法,这个方法的主要作用是授权,它定义了哪些URL路径应该被保护,http.formLogin()的方法定义了没有权限会跳向登录页面。
/**
* 自定义Security策略
* WebSecurityConfig类使用@EnableWebSecurity进行注释,以启用Spring Security的web安全支持并提供Spring MVC集成。
* 它还扩展了WebSecurityConfigurerAdapter并覆盖了它的一些方法来设置web安全配置的一些细节。
*
* @author Claw
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 授权
* configure(HttpSecurity)方法定义了哪些URL路径应该保护,哪些不应该保护。
* 具体来说,将 /和首页路径配置为不需要任何身份验证。所有其他路径都必须经过身份验证。
* 如果用户验证失败,页面将被重定向到/login?error,页面将显示相应的错误消息。
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 请求授权的规则
*/
http
.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//没有权限会跳向登录页面,需要开启登录的页面
http.formLogin();
}
}
当定义好它以后,启动SpringBoot,进入首页没有问题,但再点击leve的页面就需要权限认证了,它会带我们去向Security自己的登录页面,而不是自己写的登录页面。
现在我们需要用户名和密码来登录,这个需要我们自己来定义,通常来说,是从数据库来获取角色和密码,但是现在它提供了一个方法,可以让我们把用户存放在内存中。
HttpSecurity的源码注释非常详细,它教我们如何去使用。
需要重写configure(AuthenticationManagerBuilder auth)这个方法,需要注意在SpringSecurity 5.0+中新增了很多加密方法 它会要求password进行密码加密,如果不这么做的话,在点击登录的时候会报一个passwordEncoder为null的错误,加密的方式有很多种,这里使用了BCryptPasswordEncoder()提供的加密方式。
补充:在查看官方文档的时候发现官方推荐的是 DelegatingPasswordEncoder这个类提供加密方式。
定义的vip3等级最高,它可以扮演vip1,vip2,vip3的角色,访问所有页面,vip2包括了vip1的角色,能访问level1和level2的所有页面,vip1只有vip1角色的授权,只能访问level1的页面。
/**
* 认证
* springboot 2.1.x可以直接使用
* 在SpringSecurity 5.0+中新增了很多加密方法 它会要求password进行密码加密
* inMemoryAuthentication在内存中设置用户和密码和所对应的角色。
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//inMemoryAuthentication是从内存中存放数据来使用,正常应该是从数据库取
//如果从数据库取数据,应该用到是auth.jdbcAuthentication这个方法
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("vip1").password(new BCryptPasswordEncoder().encode("123")).roles("vip1")
.and().withUser("vip2").password(new BCryptPasswordEncoder().encode("123")).roles("vip2", "vip1")
.and().withUser("vip3").password(new BCryptPasswordEncoder().encode("123")).roles("vip3", "vip1", "vip2");
auth.jdbcAuthentication();
}
定义以后启动测试 点击level1的界面,需要认证,登录以后进入了level-1-1的页面,ok没问题,level-1-2的也没问题。但进入level2就报出了403的错误,因为拿了vip1的账号的登录,权限只有level1的页面可以进入。
接下来我们考虑下注销的事情,http对象里当然也有注销的方法,并且非常简单,HttpSecurity这个类的注释也仍然详细。
于是在confgure方法加入这两行就好了,添加了注销功能,并且在注销成功后重定向到首页。
http //注销
.logout()
//注销成功后重定向到指定页面
.logoutSuccessUrl("/");
目前首页并没有注销的功能,注释里有说到访问/logout就是注销的访问路径,那么只需要在首页添加注销的访问路径就好了。
<!--注销-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
security有csrf防护(跨站请求伪造)功能,这会让在注销的时候报出404的错误。官方文档说了为了防止伪造注销请求,应该保护注销HTTP请求不受CSRF攻击。防止伪造注销请求是必要的,这样恶意用户就无法读取受害者的敏感信息。
在请求注销的时候可以使用post请求,或者在仅在学习的时候关闭这个功能
//关闭csrf
http.csrf().disable();
测试,图是后来录的,此时已经写完了展示对应权限的功能,注销成功后已经看不到level模块的内容。
目前为止的登录页面是security自己自带的页面,访问路径为/login 把它换成我们自己写的页面的话使用http对象的方法即可。
//没有权限会跳向登录页面,需要开启登录的页面
http
.formLogin()
.loginPage("/toLogin");
当然对应的HTML中表单的action的访问路径也要改成我们自己定义的。
<div class="ui form">
<form th:action="@{/toLogin}" method="post">
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon"></i>
</div>
</div>
<input type="submit" class="ui blue submit button"/>
</form>
</div>
SpringSecurity整合Thymeleaf
接下来需要做的事情就是使具有对应权限的用户只看到对应权限的页面,也就是vip1只能看到level1的页面,vip3能看见leve1,2,3的页面。
以前通常通过模板引擎的if标签来判断,jsp的话能够从Session里面来取得用户数据然后在页面通过if标签来进行判断哪些页面能够展示。
Thymeleaf当然也可以,它还可以与Secuirty整合,不过这不是一个常见的用法。
需要导入thymeleaf和security的整合包,有版本的差异,需要注意自己Security的版本,因为我是5.0以上,所以我选择了 thymeleaf-extras-springsecurity5
<!--thymeleaf和security的整合-->
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
现在在首页加上权限判断:如果未登录,显示登录功能;如果已登录,显示用户名和权限。
首先不要忘记添加命名空间的约束。
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
thymeleaf与security的整合后在本次学习中用到的几个方法
<!-- 是否登陆 -->
<div sec:authorize="isAuthenticated()">
<!-- 用户姓名 -->
<span sec:authentication="name"></span>
<!-- 用户权限 -->
<span sec:authentication="principal.authorities"></span>
在Html中进行判断
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--登录注销-->
<div class="right menu">
<!--如果没有被认证(登录):显示登录功能-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!--如果已认证(登录),显示用户名,权限-->
<div class="right menu"sec:authorize="isAuthenticated()">
用户名:<span sec:authentication="name"></span>
权限:<span sec:authentication="principal.authorities"></span>
<!--是否登录-->
</div>
<div class="right menu" sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
</div>
</div>
未登录
已登录
实现这个后,我们可以实现权限对应的页面展示了,用到了sec:authorize="hasRole()”方法来判断是否有对应的权限。
<div>
<br>
<div class="ui three column stackable grid">
<div class="column">
<!--Vip1用户对应的页面-->
<div class="ui raised segment" sec:authorize="hasRole('vip1')">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<!--Vip2用户对应的页面-->
<div class="column">
<div class="ui raised segment" sec:authorize="hasRole('vip2')">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<!--Vip3用户对应的页面-->
<div class="column">
<div class="ui raised segment" sec:authorize="hasRole('vip3')">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
再测试:vip1就只能看见leve1的内容啦 😁
总结:如何使用SpringSecurity?
- 导入SpringSecurity的依赖
- 定义HttpSecurity策略
- 哪些路径可以访问,哪些路径不可以访问。
- 登录路径
- 注销
- 定义Authentication策略
- 用户所对应的权限
- 用户名和密码的设置,可以定义在内存,也可以从数据库获取(通常)
- SpringSecurity和thymeleaf的整合
- 页面进行权限的判断
参考:【狂神说Java】SpringBoot最新教程IDEA版通俗易懂
素材通过秦老师的学习群获得。