SpringBoot 集成 Spring Security
官方文档地址:
https://spring.io/projects/spring-security-kerberos
-
从应用开发的第一天,就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
-
市面上存在比较有名的:Shiro,Spring Security !
-
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
-
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。
-
Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
-
一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。
- 用户认证:指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
- 用户认证一般要求用户提供用户名和密码。
- 系统通过校验用户名和密码来完成认证过程。
- 用户授权:指的是验证某个用户是否有权限执行某个操作。
- 在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
- 一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
- 用户认证:指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
1、环境搭建
-
环境搭建
新建一个 Springboot 项目 web 模块,thymeleaf 模块
-
导入静态资源(静态资源结构,内容略)
- static
- cheng
- css
- qinstyle.css
- js
- jquery-3.1.1.min.js
- semantic.min.js
- css
- cheng
- templates
- views
- level1
- 1.html
- 2.html
- 3.html
- level2
- 1.html
- 2.html
- 3.html
- level3
- 1.html
- 2.html
- 3.html
- login.html
- level1
- index.html
- views
- static
-
编写 controller 跳转
package com.cheng.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class RouterController { @RequestMapping({"/", "/index"}) public String index() { return "index"; } @RequestMapping("/toLogin") public String toLogin() { return "views/login"; } @RequestMapping("/level1/{id}") public String level1(@PathVariable("id") int id) { return "views/level1/" + id; } @RequestMapping("/level2/{id}") public String level2(@PathVariable("id") int id) { return "views/level2/" + id; } @RequestMapping("/level3/{id}") public String level3(@PathVariable("id") int id) { return "views/level3/" + id; } }
-
测试环境搭建是否成功?
测试成功!
2、认识 Spring Security
-
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制。
-
对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
-
我们需要记住的类:
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
-
Spring Security的两个主要目标: “认证” 和 “授权”(访问控制)。
-
“认证”(Authentication)
- 身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
- 身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
-
“授权” (Authorization)
-
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
-
这个概念是通用的,而不是只在Spring Security 中存在。
-
3、认证和授权代码测试
-
引入 Spring Security 模块
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
编写基础配置类,定制请求的授权规则
package com.cheng.config; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity // 开启 WebSecurity 模式 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 实现效果:首页所有人都能访问,功能页需要有对应权限才能访问 http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/level1/**").hasAnyRole("vip1") .antMatchers("/level2/**").hasAnyRole("vip2") .antMatchers("/level3/**").hasAnyRole("vip3"); } }
-
测试
测试结果发现只有首页能进入,其它页面点击会报 403 错误,没有权限。
-
开启自动配置的登录功能
// 开启自动配置的登录功能 // /login:请求来到登录页 // /login?error:重定向到这里表示登录失败 http.formLogin();
-
测试
测试发现点击首页之外的页面,会直接跳转到登录页
-
定义认证规则
// 定义认证规则 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 在内存中定义数据,正常情况下,这些数据需要从数据库中读取 auth.inMemoryAuthentication() .withUser("root").password("111111").roles("vip1", "vip2", "vip3") .and() .withUser("admin").password("222222").roles("vip1", "vip2") .and() .withUser("visitor").password("333333").roles("vip1"); }
-
使用账号登录进行测试
测试结果:报错 500
There was an unexpected error (type=Internal Server Error, status=500). There is no PasswordEncoder mapped for the id "null"
错误原因:必须要对密码进行加密,否则无法登录
-
添加加密
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 模拟数据,正常情况下,这些数据需要从数据库中读取 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("root").password(new BCryptPasswordEncoder().encode("111111")).roles("vip1", "vip2", "vip3") .and() .withUser("admin").password(new BCryptPasswordEncoder().encode("222222")).roles("vip1", "vip2") .and() .withUser("visitor").password(new BCryptPasswordEncoder().encode("333333")).roles("vip1"); }
-
登录测试
测试结果:root 可以访问所有页面,admin 无法访问 level3 下的页面,visitor 只能访问 level1 下的页面。
测试成功!
4、权限控制及注销
-
开启制动配置的注销功能
// 开启自动配置的注销功能 // /logout 注销请求 http.logout();
-
在前端 index.html 导航栏中添加一个 注销 按钮
<!--注销--> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a>
-
测试
登陆后点击注销,会跳到确认注销页面,确认后跳转到登录页面。
-
设置注销成功后跳转到首页
// 开启自动配置的注销功能 // /logout 注销请求 // logoutSuccessUrl("/") 注销成功后跳转到首页 http.logout().logoutSuccessUrl("/");
-
测试
添加需求
- 未登录时,导航栏只显示登录按钮
- 登录之后,导航栏显示登录信息及注销按钮
- 不同用户,只展示对应权限能够操作的内容
- 例如:root 用户页面都显示,admin 用户只显示 VIP1, VIP2 对应的内容
-
此需求需要结合 thymeleaf 中的一些功能实现
导入 maven 依赖
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
-
修改前端页面
导入命名空间
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5
修改导航栏,添加认证判断
<!--登录注销--> <div class="right menu"> <div sec:authorize="!isAuthenticated()"> <!--未登录--> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div> <!--注销--> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注销 </a> </div> <!--已登录--> <div sec:authorize="isAuthenticated()"> <a class="item"> <i class="address card icon"></i> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> </div>
-
重启测试
注:这里需要通过 /login 页面登录,如果直接点击导航栏(/toLogin)会报错,因为还未进行设置
测试结果:未登录时,只显示登录图标,登录后,显示用户名和角色信息。
-
它默认防止 csrf 跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭 csrf 功能;
//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求 http.csrf().disable();
-
设置不同用户登陆时,只能看到对应权限的内容
<div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <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> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <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> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <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>
-
测试
测试效果:
- 未登陆时,除导航栏外内容都不显示
- root 用户登录时,显示所有内容
- admin 用户登录时,显示 VIP1, VIP2 对应的内容
- visitor 用户登录时,只显示 VIP1 对应的内容
5、记住我功能
需要实现的效果:登录之后,关闭浏览器,再进入时不需要重新登录
-
开启记住我功能
// 记住我 http.rememberMe();
-
测试
登录时会有一个记住我功能,勾选之后登录,然后关闭浏览器,再重新打开浏览器访问,用户依然存在,无需再登录。
-
实现原理
登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie。
6、定制登录页
-
指定登录页为自己写的 Login 界面
在登录页配置后指定 loginPage
// 开启自动配置的登录功能 // /login:请求来到登录页 // /login?error:重定向到这里表示登录失败 http.formLogin().loginPage("/toLogin");
-
修改前端,指向自己定义的 Login 请求
<div sec:authorize="!isAuthenticated()"> <!--未登录--> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div>
-
配置登录时需要发送的信息,login.html 配置提交请求及方式,方式必须为post。
<form th:action="@{/login}" 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>
-
验证处理,配置接收登录的用户名和密码的参数
// 开启自动配置的登录功能 // /login:请求来到登录页 // /login?error:重定向到这里表示登录失败 http.formLogin() .loginPage("/toLogin") .usernameParameter("username") .passwordParameter("password") .loginProcessingUrl("/login");// 登录表单提交请求
-
在登录页增加记住如的多选框
<div class="field"> <input type="checkbox" name="remember"> 记住我 </div>
-
后端验证处理
// 记住我 http.rememberMe().rememberMeParameter("remember");// 定制记住我参数
-
测试
测试成功!
附:完整配置代码
package com.cheng.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity // 开启 WebSecurity 模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 实现效果:首页所有人都能访问,功能页需要有对应权限才能访问
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasAnyRole("vip1")
.antMatchers("/level2/**").hasAnyRole("vip2")
.antMatchers("/level3/**").hasAnyRole("vip3");
// 开启自动配置的登录功能
// /login:请求来到登录页
// /login?error:重定向到这里表示登录失败
http.formLogin()
.loginPage("/toLogin")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login");// 登录表单提交请求
//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.csrf().disable();
// 记住我
http.rememberMe().rememberMeParameter("remember");// 定制记住我参数
// 开启自动配置的注销功能
// /logout 注销请求
// logoutSuccessUrl("/") 注销成功后跳转到首页
http.logout().logoutSuccessUrl("/");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 模拟数据,正常情况下,这些数据需要从数据库中读取
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("root").password(new BCryptPasswordEncoder().encode("111111")).roles("vip1", "vip2", "vip3")
.and()
.withUser("admin").password(new BCryptPasswordEncoder().encode("222222")).roles("vip1", "vip2")
.and()
.withUser("visitor").password(new BCryptPasswordEncoder().encode("333333")).roles("vip1");
}
}