SpringBoot 从入门到光头 —— 第十五章 SpringBoot 与安全
1. 安全
SpringSecurity 是针对 Spring 项目的安全框架,也是 SpringBoot 底层安全模块默认的技术选型。它可以实现强大的 Web 安全控制。对于安全控制,我们仅需要 引入 spring-boot-starter-security
模块 ,进行少量的配置,即可实现强大的安全管理
需要用到的几个类:
WebSecurityConfigAdapter
:自定义 Security 策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启 WebSecurity 模式
应用层序的的两个主要区域是“认证”和“授权”(或者访问控制)。这两个主要区域是 SpringSecurity 的两个目标
“认证”(Authentication),是建立在一个它声明的主体的过程(一个“主体”一般是指用户,设备或者是一些可以在你的应用程序中执行动作的其他系统)。
“授权”(Authorization)指定一个主体是否允许在你的应用程序执行一个动作的过程,为了抵达需要授权的点,主体的身份已经有认证过程建立。
这个概念是通用的而不只在于 SpringSecurity 中
2. 整合 SpringSecurity
引入 SpringSecurity
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency
编写 SpringSecurity 的配置类
com.yourname.security.config.MySecurityConfig
/** * @author gregPerlinLi * @since 2022-01-21 */ @EnableWebSecurity public class MySecurityConfig extends WebSecurityConfigurerAdapter { }
控制请求的访问权限
com.yourname.security.config.MySecurityConfig
/** * @author gregPerlinLi * @since 2022-01-21 */ @EnableWebSecurity public class MySecurityConfig extends WebSecurityConfigurerAdapter { /** * Define authorization rules * * @param http http security * @throws Exception exception */ @Override protected void configure(HttpSecurity http) throws Exception { // Authorization rules for custom requests http.authorizeRequests().antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("VIP1") .antMatchers("/level2/**").hasRole("VIP2") .antMatchers("/level3/**").hasRole("VIP3"); // Turn on the autoconfigured login function: If you don't have login or permission, you will come to the automatically generated login page // 1. /login to login page // 2. Redirecting to /login?error indicates a login failure // 3. There are more detailed rules http.formLogin(); // Turn on the autoconfigured logoff function // 1. Accessing /logout means that the user logs off and clear the session // 2. /login?logout page will be returned if logout is successful // 3. .logoutSuccessUrl("/"): Set the page to return after logout http.logout().logoutSuccessUrl("/"); // Enable remember me function // After successful login, the cookie will be sent to the browser cache, and the cookie will be brought to the later access page. As long as it passes the check, you can avoid login // if you click logout, the cookie will be deleted http.rememberMe(); } }
http.formLogin()
的作用:开启自动配置的登录功能,如果没有登录就会来到自动生成的登录界面/login
来到 SpringSecurity 自动生成的登录页面- 如果登录失败会重定向到
/login?error
http.formLogin().loginPage("/userlogin")
可以设置自定义的登录页面(可以加上.usernameParameter()
和.passwordParameter()
来修改默认的用户名/密码变量名,默认是username
/password
)- 默认 POST形式的
/login
代表处理登录 - 一旦定制
loginPage
以后,那么loginPage
的 POST 请求就是登录,当然,你也可以用.loginProcessUrl()
来设置处理登录请求的页面
http.logout()
的作用:开启自动配置的注销功能(只能使用 POST 请求)- 访问
/logout
表示用户注销,并清空 Session - 注销成功后默认会返回
/login?logout
页面 http.logout.logoutSuccessUrl("");
可以设置注销以后返回的页面
http.rememberMe()
的作用:开启“记住我”功能- 登录成功以后,将Cookie发送给浏览器缓存,以后访问页面带上这个Cookie,只要通过检查,就可以免登录(默认保留 14 天)
- 如果点击注销会删除这个 Cookie
- 如果使用定制登录页面的话可以加上
.rememberMeParameter()
来修改默认的“记住我”的变量名(默认是rememberMe
)
配置登录用户及其权限
com.yourname.security.config.MySecurityConfig
/** * @author gregPerlinLi * @since 2022-01-21 */ @EnableWebSecurity public class MySecurityConfig extends WebSecurityConfigurerAdapter { /** * Define authentication rules * * @param auth authentication * @throws Exception exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("XiaoMing").roles("VIP1", "VIP2", "VIP3").password(new BCryptPasswordEncoder().encode("123456")) .and() .withUser("XiaoHong").roles("VIP1", "VIP2").password(new BCryptPasswordEncoder().encode("123456")) .and() .withUser("XiaoQiang").roles("VIP1").password(new BCryptPasswordEncoder().encode("123456")); } }
注意⚠️:Spring 从 5.0 开始由于安全问题,设置用户密码的时候需要指定一种加密方式,否则会报错!
3. 整合 Thymeleaf 模板引擎实现更多功能
Thymeleaf 提供了许多针对 SpringSecurity 的安全属性,包括授权,获取登录用户信息等
在使用这些安全属性之前,我们需要引入 Thymeleaf 和 SpringSecurity 的整合模块
pom.xml
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
使用示例:
welcome.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8">
<title>Welcome</title>
<style type="text/css">
body {
font-family: Futura, "PingFang SC", "Helvetica", Arial, sans-serif;
}
</style>
</head>
<body>
<h1 align="center">Welcome to book library</h1>
<div sec:authorize="!isAuthenticated()">
<h2 align="center">Hello guest, if you want to surf the books <a th:href="@{/login}">please login</a></h2>
</div>
<div sec:authorize="isAuthenticated()">
<h2 align="center">Hello, <span sec:authentication="name"></span>, your roles are: <span sec:authentication="principal.authorities"></span></h2>
<form align="center" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
<hr/>
<div sec:authorize="hasRole('VIP1')">
<h3>Common Books</h3>
<ul>
<li><a th:href="@{/level1/1}">Commons book1</a></li>
<li><a th:href="@{/level1/2}">Commons book2</a></li>
<li><a th:href="@{/level1/3}">Commons book3</a></li>
</ul>
</div>
<div sec:authorize="hasRole('VIP2')">
<h3>Advanced Books</h3>
<ul>
<li><a th:href="@{/level2/1}">Advanced book1</a></li>
<li><a th:href="@{/level2/2}">Advanced book2</a></li>
<li><a th:href="@{/level2/3}">Advanced book3</a></li>
</ul>
</div>
<div sec:authorize="hasRole('VIP3')">
<h3>Ultra Books</h3>
<ul>
<li><a th:href="@{/level3/1}">Ultra book1</a></li>
<li><a th:href="@{/level3/2}">Ultra book2</a></li>
<li><a th:href="@{/level3/3}">Ultra book3</a></li>
</ul>
</div>
</body>
</html>
com.yourname.controller.BookController
/**
* @author gregPerlinLi
* @since 2022-01-20
*/
@Controller
public class BookController {
private final String PREFIX = "pages/";
/**
* Welcome page
*
* @return page
*/
@GetMapping(value = "/")
public String index() {
return "welcome";
}
/**
* Login page
*
* @return page
*/
@GetMapping(value = "/userlogin")
public String loginPage() {
return PREFIX + "login";
}
/**
* Level1 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level1/{path}")
public String level1(@PathVariable("path") String path) {
return PREFIX + "level1/" + path;
}
/**
* Level2 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level2/{path}")
public String level2(@PathVariable("path") String path) {
return PREFIX + "level2/" + path;
}
/**
* Level3 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level3/{path}")
public String level3(@PathVariable("path") String path) {
return PREFIX + "level3/" + path;
}
}
定制登录页面示例:
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<style type="text/css">
body {
font-family: Futura, "PingFang SC", "Helvetica", Arial, sans-serif;
}
</style>
</head>
<body>
<h1 align="center">Welcome to login book library system</h1>
<hr/>
<div align="center">
<form th:action="@{/login}" th:method="post">
<label>Username:<input type="text" name="username"></label><br/>
<label>Password:<input type="password" name="password"></label><br/>
<label><input type="checkbox" name="remember">Remember me in this computer for 14 days</label><br/>
<label><input type="submit" value="Login"></label><br/>
</form>
</div>
</body>
</html>
com.yourname.security.controller.Book.Controller
/**
* @author gregPerlinLi
* @since 2022-01-20
*/
@Controller
public class BookController {
private final String PREFIX = "pages/";
/**
* Welcome page
*
* @return page
*/
@GetMapping(value = "/")
public String index() {
return "welcome";
}
/**
* Login page
*
* @return page
*/
@GetMapping(value = "/userlogin")
public String loginPage() {
return PREFIX + "login";
}
/**
* Level1 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level1/{path}")
public String level1(@PathVariable("path") String path) {
return PREFIX + "level1/" + path;
}
/**
* Level2 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level2/{path}")
public String level2(@PathVariable("path") String path) {
return PREFIX + "level2/" + path;
}
/**
* Level3 page mapping
* @param path path
* @return page
*/
@GetMapping(value = "/level3/{path}")
public String level3(@PathVariable("path") String path) {
return PREFIX + "level3/" + path;
}
}
com.yourname.security.config.MySecurityConfig
/**
* @author gregPerlinLi
* @since 2022-01-21
*/
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Define authorization rules
*
* @param http http security
* @throws Exception exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// Authorization rules for custom requests
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("VIP1")
.antMatchers("/level2/**").hasRole("VIP2")
.antMatchers("/level3/**").hasRole("VIP3");
// Turn on the autoconfigured login function: If you don't have login or permission, you will come to the automatically generated login page
// 1. /login to login page
// 2. Redirecting to /login?error indicates a login failure
// 3. There are more detailed rules
// 4. The default form is POST, /login stands for processing login
// 5. Once loginPage is customized, the post request of loginPage is login
// 6. You can also use it .loginProcessUrl() to set the page for processing login requests
http.formLogin().usernameParameter("username").passwordParameter("password").loginPage("/userlogin").loginProcessingUrl("/login");
// Turn on the autoconfigured logoff function
// 1. Accessing /logout means that the user logs off and clear the session
// 2. /login?logout page will be returned if logout is successful
// 3. .logoutSuccessUrl("/"): Set the page to return after logout
http.logout().logoutSuccessUrl("/");
// Enable remember me function
// After successful login, the cookie will be sent to the browser cache, and the cookie will be brought to the later access page. As long as it passes the check, you can avoid login
// if you click logout, the cookie will be deleted
http.rememberMe().rememberMeParameter("remember");
}
/**
* Define authentication rules
*
* @param auth authentication
* @throws Exception exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("XiaoMing").roles("VIP1", "VIP2", "VIP3").password(new BCryptPasswordEncoder().encode("123456"))
.and()
.withUser("XiaoHong").roles("VIP1", "VIP2").password(new BCryptPasswordEncoder().encode("123456"))
.and()
.withUser("XiaoQiang").roles("VIP1").password(new BCryptPasswordEncoder().encode("123456"));
}
}