Spring Security source code analysis from entry to mastery. Follow Xueshang Silicon Valley
- learning target
- 1. Introduction to Spring Security Framework
- 2. Spring Security entry case
-
- 2.1 Create a project
- 2.2 Running the project
- 2.3 Related concepts in rights management
- 2.4 Add a controller for access
- 2.5 Basic Principles of Spring Security
- 2.6 Explanation of UserDetailsService interface
- 2.7 PasswordEncoder Interface Explanation
- 2.8 SpringBoot's automatic configuration of Security
- 3. Spring Security Web permission scheme
-
- 3.1 Set the account number and password for logging in to the system
- 3.2 Implement database authentication to complete user login
- 3.3 Unauthenticated requests jump to the login page
- 3.4 Access control based on roles or permissions
- 3.5 Realize permission authentication based on database
- 3.6 Customize 403 page
- 3.7 Annotation use
- 3.8 Remember the account number and password when logging in (based on the database)
- 3.9 User Logout
- 3.10 CSRF
learning target
1. Introduction to Spring Security Framework
1.1 Overview
Spring is a very popular and successful Java application development framework, and Spring Security is a member of the Spring family. Based on the Spring framework, Spring Security provides a complete solution for web application security.
As you probably know the two main areas of security are " authentication " and " authorization " (or access control), in general, web application security consists of both user authentication and user authorization. Part, these two points are also important core functions of Spring Security.
(1) User authentication refers to: verifying whether a user is a legitimate subject in the system, that is to say, whether the user can access the system. User authentication generally requires the user to provide a user name and password. The system completes the authentication process by verifying the user name and password. In layman's terms, the system thinks whether the user can log in .
(2) User authorization refers to verifying whether a user has permission to perform an operation. In a system, different users have different permissions. For example, for a file, some users can only read it, while others can modify it. Generally speaking, the system assigns different roles to different users, and each role corresponds to a series of permissions. In layman's .
1.2 History
"Spring Security started late 2003," "spring's acegi security system". The cause was a question on the Spring developers mailing list, someone asked whether to consider providing a spring-based security implementation.
Spring Security started in late 2013 under the name "The Acegi Secutity System for Spring". A question was submitted to the Spring developers mailing list asking if there had been an opportunity for Spring's security community to consider an implementation. The Spring community was relatively small back then (compared to now). In fact Spring itself was only a project that existed on CourseForge in 2013, and the answer to this question is an area worthy of research, although the current lack of time prevents our exploration of it.
With this in mind, a simple security implementation was built but not released. A few weeks later, other members of the Spring community asked about security, and this time the code was sent to them. Several other requests followed. About 200,000 people had used the code by January 2014. Some of these entrepreneurs came up with a SourceForge project, and it was officially launched in March 2004.
In the early days, this project did not have any authentication module of its own, and the authentication process relied on container-managed security and Acegi security. Instead of focusing on authorization. This worked fine at first, but more and more users requested additional container support. The fundamental limitations of container-specific authentication domain interfaces become clear. There is also a related issue adding paths to new containers, which is a common source of end-user confusion and misconfiguration.
Introduction to Acegi security-specific authentication services. About a year later, Acegi Security officially became a sub-project of the Spring Framework. The 1.0.0 final version was published in 2006, after more than two and a half years of high-volume production software projects and hundreds of improvements and active use from community contributions.
Acegi Security officially became a Spring portfolio project at the end of 2007, renamed "Spring Security".
1.3 Comparison of the same product
1.3.1 Spring Security
Part of the Spring technology stack.
Secure your applications by providing full and scalable authentication and authorization support.
Spring Security official website documentation
Spring Security features:
- Integrate seamlessly with Spring.
- Comprehensive permission control.
- Specifically designed for web development.
- Older versions cannot be used outside of the web environment.
- The new version extracts the entire framework hierarchically and divides it into core modules and Web modules. Introducing the core module alone can break away from the Web environment.
- heavyweight.
1.3.2 Shiro
Apache's lightweight permission control framework.
Features:
- lightweight. The philosophy advocated by Shiro is to make complex things simple. Better performance for Internet applications with higher performance requirements.
- Versatility.
- Benefits: It is not limited to the Web environment, and can be used without the Web environment.
- Defect: Some specific requirements in the Web environment require manual code customization.
Spring Security is a security management framework in the Spring family. In fact, Spring Security has been developed for many years before Spring Boot appeared, but it is not used much. The field of security management has always been dominated by Shiro.
Compared with Shiro, it is more troublesome to integrate Spring Security in SSM. Therefore, although Spring Security is more powerful than Shiro, it is not used as much as Shiro (although Shiro does not have as many functions as Spring Security, but for most projects, Shiro will also suffice).
Since Spring Boot is available, Spring Boot provides an automatic configuration solution for Spring Security, and Spring Security can be used with less configuration.
Therefore, in general, the combination of common security management technology stacks is as follows:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
The above is just a recommended combination. From a purely technical point of view, no matter how the combination is, it can be run.
1.4 Module division
2. Spring Security entry case
2.1 Create a project
(1). Create a Spring Boot project
Group:com.atguigu
Artifact:securitydemo1
version choose one at random, follow-up adjustment
(2). Change project dependency files
change pom.xml, change version
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Change the configuration file as follows:
1. Add -web to the suffix
2. Add the following dependencies
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(3).Add Controller
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String add() {
return "hello security";
}
}
Change the port number:
server.port=8111
2.2 Running the project
access
http://localhost:8111/login
The interface is like this, indicating that the spring security access is successful
Default username: user
The password will be printed on the console when the project is started. Note that the password will change every time it is started!
After entering the account number and password, click sign in and find that this is the introductory case interface we want to see.
2.3 Related concepts in rights management
2.3.1 Subject
English words: principal
users or devices that use the system or users who log in remotely from other systems, etc. Simply put, whoever uses the system is the subject.
2.3.2 Authentication
English word: authentication
authority management system confirms the identity of a subject and allows the subject to enter the system. Simply put, the "subject" proves who he is.
Generally speaking, it is the login operation done before.
2.3.3 Authorization
English word: authorization "
grants" the "power" of the operating system to the "subject", so that the subject has the ability to perform specific functions in the operating system.
So in simple terms, authorization is to assign permissions to users.
2.4 Add a controller for access
New class IndexController.java
@Controller
public class IndexController {
@GetMapping("index")
@ResponseBody
public String index() {
return "success";
}
}
access:
http://localhost:8111/index
2.5 Basic Principles of Spring Security
Spring Security is essentially a filter chain:
the filter chain can be obtained from startup:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
2.5.1 Three basic filters
The underlying process of the code: focus on three filters:
FilterSecurityInterceptor : It is a method-level permission filter , basically located at the bottom of the filter chain.
super.beforeInvocation(fi) means to check whether the previous filter is passed.
fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); indicates the real call to the background service.
ExceptionTranslationFilter : It is an **exception filter,** used to handle exceptions thrown during the authentication and authorization process
UsernamePasswordAuthenticationFilter : Intercept the POST request of /login, and verify the username and password in the form.
2.5.2 Loading process of filter chain
Here we use Spring Boot integration, and the bottom layer helps us with automatic configuration. If it is not Spring Boot, we need to use the doFilter method of the DelegatingFilterProxy
doFilter method
initDelegate method
of the FilterChainProxy class.
The doFilterInternal method of the FilterChainProxy class
2.6 Explanation of UserDetailsService interface
When nothing is configured, the username and password are generated from Spring Security definitions. In the actual project, the account number and password are all queried from the database. So we need to control the authentication logic through custom logic.
In layman's terms, the user name and password for querying the database need to rewrite the interface
Query the user name and password of the database:
1. Create a class to inherit UsernamePasswordAuthenticationFilter, rewrite successfulAuthentication, unsuccessfulAuthentication, attemptAuthentication
2. Create a class to implement the UserDetailsService interface, write the process of querying data, and return the User object, which is an object provided by the security framework
If you need custom logic, you only need to implement the UserDetailsService interface. The interface is defined as follows:
- return valueUserDetails
This class is the system default user " principal "
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
The following is the UserDetails implementation class
org.springframework.security.core.userdetails.User
org.springframework.security.provisioning.MutableUser
In the future, we only need to use the User entity class!
- The method parameter username
represents the user name. This value is the data passed from the client form. By default, it must be called username, otherwise it cannot be received.
2.7 PasswordEncoder Interface Explanation
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
The interface implementation class
BCryptPasswordEncoder is the password parser officially recommended by Spring Security, and this parser is usually used more often.
BCryptPasswordEncoder is a concrete implementation of the bcrypt strong hashing method. It is a one-way encryption based on the Hash algorithm. The encryption strength can be controlled by strength, the default is 10.
- Demonstration of common methods
@Test
public void test01(){
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new
BCryptPasswordEncoder();
// 对密码进行加密
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+atguigu);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}
output result:
加密之后数据: $2a$10$HnfETFGYndTq05fxZDIK9uxm7tpPKPEZautjgDIseR0Suk6yeKQ2O
比较结果: true
2.8 SpringBoot's automatic configuration of Security
3. Spring Security Web permission scheme
3.1 Set the account number and password for logging in to the system
Method 1: in application.properties
spring.security.user.name=atguigu
spring.security.user.password=atguigu
At this time, the IDEA console will not generate an account and password, so you can log in directly with the account and password in the configuration file
Method 2: Write a configuration class
Add new class SecurityConfig.java
The code is as follows: SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 PasswordEncoder 类到 spring 容器中
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("hsp").password(password).roles("");
}
}
If you just write like this, you will report an error when you log in.
We need to add Bean
The complete code of SecurityConfig is as follows
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 PasswordEncoder 类到 spring 容器中
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("hsp").password(password).roles("");
}
}
Login is successful this time
Method 3: Custom writing implementation class
Custom implementation class setting:
the first step is to create a configuration class and set which userDetailsService implementation class to use.
The second step is to write the implementation class and return the User object. The User object has a username, password and operation authority
New class SecurityConfigCustomize.java
@Configuration
public class SecurityConfigCustomize extends WebSecurityConfigurerAdapter {
// 注入 PasswordEncoder 类到 spring 容器中
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
}
Create a new MyUserNameDetailsService class
@Service("UserDetailsService")
public class MyUserNameDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 设置密码
String password = bCryptPasswordEncoder.encode("123");
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 返回用户
return new User("heima", password, role);
}
}
Try it and find that the login is successful
3.2 Implement database authentication to complete user login
Complete custom login
3.2.1 Prepare SQL
CREATE DATABASE spring_security;
USE spring_security;
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR (20) UNIQUE NOT NULL,
PASSWORD VARCHAR (100)
) ;
INSERT INTO users VALUES(3,'lucy','123');
INSERT INTO users VALUES(4,'mary','456');
3.2.2 Add dependencies
add dependencies
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3.2.3 Making Entity Classes
Users
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}
3.2.4 Integrate MybatisPlus to make mapper
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<Users> {
}
3.2.5 Make login implementation class
Modify MyUserNameDetailsService.java
@Service("UserDetailsService")
public class MyUserNameDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用userMapper方法查询数据库的密码
QueryWrapper<Users> usersQueryWrapper = new QueryWrapper<>();
usersQueryWrapper.lambda().eq(Users::getUsername, username);
// 查询到users对象
Users users = userMapper.selectOne(usersQueryWrapper);
// 判断
if (null == users) {
// 表示数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 设置密码
String password = bCryptPasswordEncoder.encode(users.getPassword());
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 返回用户
return new User(username, password, role);
}
}
3.2.6 Add annotations to the startup class
Securitydemo1Application.java plus @MapperScan annotation
@SpringBootApplication
@MapperScan(basePackages = "com.atguigu.securitydemo1.mapper")
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}
3.2.7 Configure the database in the configuration file
application.properties
#mysql 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
3.2.8 Test access
Enter username: lucy, password: 123
3.3 Unauthenticated requests jump to the login page
3.3.1 Introducing front-end template dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3.3.2 Introducing the login page
Import the prepared login page into the project
and write a
login.html by hand
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名: <input type="text" name="username"/>
<br/>
密码: <input type="password" name="password"/>
<br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
Note: The page submission method must be a post request
Note: username and password must be username,password
Reason:
A filter UsernamePasswordAuthenticationFilter will be used when performing login.
3.3.3 Write the controller
Modify TestController.java to add
@GetMapping("index")
public String index() {
return "hello index";
}
3.3.4 Write configuration class release login page and static resources
Main idea: Rewrite the configure(HttpSecurity http) method SecurityConfigCustomize.java in WebSecurityConfigurerAdapter.java
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义自己登录的表单页面
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")// 登录访问的路径
.defaultSuccessUrl("/test/index") // 登录成功后要跳转的路径
.permitAll()
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll() // 访问以下路径后无需登录认证
.anyRequest().authenticated() // 所有请求都可以访问
.and().csrf().disable() // 关闭csrf的防护
;
}
3.3.5 Testing
access
http://localhost:8111/test/hello
can be accessed directly
http://localhost:8111/test/index
after login
3.3.6 Set the unauthorized request to jump to the login page
Above, if the parameters of the login interface are different, instead of username and password,
you can call the usernameParameter() and passwordParameter() methods to modify the configuration.
Modify
SecurityConfigCustomize.java
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义自己登录的表单页面
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")// 登录访问的路径
.defaultSuccessUrl("/test/index") // 登录成功后要跳转的路径
.permitAll()
.usernameParameter("loginAcct")
.passwordParameter("userPswd")
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll() // 访问以下路径后无需登录认证
.anyRequest().authenticated() // 所有请求都可以访问
.and().csrf().disable() // 关闭csrf的防护
;
}
Modify login.html
to change the username and password name
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名: <input type="text" name="loginAcct"/>
<br/>
密码: <input type="password" name="userPswd"/>
<br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
Found that you can log in successfully
http://localhost:8111/test/index
3.4 Access control based on roles or permissions
3.4.1 hasAuthority method
Returns true if the current principal has the specified permissions, otherwise returns false
Only for one role, or one permission assignment
- Modify the configuration class
SecurityConfigCustomize.jav to add
.antMatchers("/test/index").hasAuthority("admins") // 当前路径具有admins权限才可以访问
- Grant permissions to the user login subject
Modify MyUserNameDetailsService.java
MyUserNameDetailsService.java
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
- Test:
visit
http://localhost:8111/test/index
After the authentication is completed, the login success is returned
3.4.2 hasAnyAuthority method
Returns true if the current principal has any of the provided roles (given as a comma-separated list of strings)
Only for multiple roles, or multiple permission assignments
If we also add multiple roles according to hasAuthority
code show as below:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义自己登录的表单页面
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")// 登录访问的路径
.defaultSuccessUrl("/test/index") // 登录成功后要跳转的路径
.permitAll()
//.usernameParameter("loginAcct")
//.passwordParameter("userPswd")
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll() // 访问以下路径后无需登录认证
//.antMatchers("/test/index").hasAnyAuthority("admins") // 当前路径具有admins权限才可以访问
.antMatchers("/test/index").hasAuthority("admins,manager") // 当前路径具有admins、manage权限才可以访问
.anyRequest().authenticated() // 所有请求都可以访问
.and().csrf().disable() // 关闭csrf的防护
;
}
access
http://localhost:8111/test/index
Found an error 403
and we changed SecurityConfigCustomize.java
.antMatchers("/test/index").hasAnyAuthority("admins,manager") // 当前路径具有admins、manage权限才可以访问
visit again
http://localhost:8111/test/index
correct
3.4.3 hasRole method
Access is allowed if the user has the given role, otherwise a 403 occurs.
Returns true if the current principal has the specified role.
The underlying source code:
Modify SecurityConfigCustomize.java
.antMatchers("/test/index").hasRole("sale") // 当前路径只有sale该角色,才能访问
Add roles to users:
Modify MyUserNameDetailsService.java
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
Modify the configuration file:
Note that there is no need to add "ROLE_" in the configuration file, because the above-mentioned underlying code will be automatically added to match it.
access
http://localhost:8111/test/index
correct
3.4.4 hasAnyRole
Indicates that the user can access any one of the conditions.
Modify MyUserNameDetailsService.java
to add roles to users:
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale,ROLE_random");
Modify SecurityConfigCustomize.java
.antMatchers("/test/index").hasAnyRole("sale") // 当前路径中满足括号内任意角色,都能访问
access
http://localhost:8111/test/index
Access successful
3.5 Realize permission authentication based on database
First of all, we quoted the dependency of mybatis-plus, and there is no need to quote the dependency of mybatis here
3.5.1 Executing database scripts
CREATE TABLE menu(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
url VARCHAR(100),
parentid BIGINT,
permission VARCHAR(20)
);
INSERT INTO menu VALUES(1,'系统管理','',0,'menu:system');
INSERT INTO menu VALUES(2,'用户管理','',0,'menu:user');
CREATE TABLE role(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
);
INSERT INTO role VALUES(1,'admin');
INSERT INTO role VALUES(2,'common');
CREATE TABLE role_user(
uid BIGINT,
rid BIGINT
);
INSERT INTO role_user VALUES(1,1);
INSERT INTO role_user VALUES(2,2);
CREATE TABLE role_menu(
MID BIGINT,
rid BIGINT
);
INSERT INTO role_menu VALUES(1,1);
INSERT INTO role_menu VALUES(2,1);
INSERT INTO role_menu VALUES(2,2);
3.5.2 Add entity class
Menu.java
code is as follows:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu {
private Long id;
private String name;
private String url;
private Long parentId;
private String permission;
}
Add entity class Role
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private Long id;
private String name;
}
3.5.3 Write interface and implementation class
UsersMapper.java
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
/**
* 根据用户Id查询用户角色
*
* @param userId
* @return
*/
List<Role> selectRoleByUserid(Long userId);
/**
* 根据用户Id查询菜单
*
* @param userId
* @return
*/
List<Menu> selectMenuByUserid(Long userId);
}
The above interface needs to perform multi-table management query:
you need to customize UsersMapper.xml in the resource/mapper directory.
The two names must be consistent
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.securitydemo1.mapper.UsersMapper">
<!--
mapper接口和映射文件要保证两个一致
1.mapper接口的权和类名和映射文件的namespace一致
2.mapper接口中的方法的方法名要和映射文件中的sql的id保持一致
-->
<!-- List<Role> selectRoleByUserid(Long userId);-->
<select id="selectRoleByUserid" resultType="Role">
SELECT b.`id`,
b.`name`
FROM role_user a
LEFT JOIN role b
ON a.`rid` = b.`id`
WHERE a.`uid` = #{userId}
</select>
<!-- List<Menu> selectMenuByUserid(Long userId);-->
<select id="selectMenuByUserid" resultType="Menu">
SELECT m.id,
m.name,
m.url,
m.parentid,
m.permission
FROM menu m
INNER JOIN role_menu rm
ON m.id = rm.mid
INNER JOIN role r
ON r.id = rm.rid
INNER JOIN role_user ru
ON r.id = ru.rid
WHERE ru.uid = #{userId}
</select>
</mapper>
MyUserNameDetailsService.java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用userMapper方法查询数据库的密码
QueryWrapper<Users> usersQueryWrapper = new QueryWrapper<>();
usersQueryWrapper.lambda().eq(Users::getUsername, username);
// 查询到users对象
Users users = usersMapper.selectOne(usersQueryWrapper);
// 判断
if (null == users) {
// 表示数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
// 给该用户设置密码
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 设置密码
String password = bCryptPasswordEncoder.encode(users.getPassword());
// 获取到用户角色,菜单列表
List<Role> roleList = usersMapper.selectRoleByUserid((long) users.getId());
List<Menu> menuList = usersMapper.selectMenuByUserid((long) users.getId());
// 声明一个集合List
ArrayList<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
// 处理角色
for (Role role : roleList) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
grantedAuthoritiesList.add(simpleGrantedAuthority);
}
// 处理权限
for (Menu menu : menuList) {
grantedAuthoritiesList.add(new SimpleGrantedAuthority(menu.getPermission()));
}
// 返回用户
return new User(username, password, grantedAuthoritiesList);
}
3.5.4 Add mappings to the configuration file
In the configuration file application.properties add
mybatis-plus.type-aliases-package=com.atguigu.securitydemo1.pojo
mybatis-plus.mapper-locations=classpath:com/atguigu/securitydemo1/mapper/*.xml
3.5.5 Modify access configuration class
.antMatchers("/test/index").hasAnyRole("admin") // 当前路径中满足括号内任意角色,都能访问
3.5.6 Testing with administrators and non-administrators
Login as administrator
http://localhost:8111/test/index
Login as follows
If the non-administrator test will prompt 403 no permission
3.6 Customize 403 page
3.6.1 Modify access configuration class
Modify SecurityConfigCustomize.java
http.exceptionHandling().accessDeniedPage("/unauth.html");
3.6.2 Add corresponding controller
Here you jump directly to the static interface, so you don’t need to add a controller
3.6.3 Add corresponding static page
New static interface
unauth.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Unauth</title>
</head>
<body>
<h1>没有访问权限!</h1>
</body>
</html>
3.6.4 Testing
For testing, you can remove permissions
access
http://localhost:8111/test/index
Jump successfully
3.7 Annotation use
3.7.1 @Secured
Determine whether the user has a role and can access methods. Another thing to note is that the matched string here needs to be prefixed with "ROLE_". To use annotations, you must first enable the annotation function!
@EnableGlobalMethodSecurity(securedEnabled=true)
Securitydemo1Application.java
@SpringBootApplication
@MapperScan(basePackages = "com.atguigu.securitydemo1.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}
Annotate
TestController.java on the controller method
@GetMapping("update")
@Secured({
"ROLE_sale", "ROLE_manager"})
public String update() {
return "hello update";
}
Modify MyUserNameDetailsService.java, add ROLE_sale role
Direct access after login: Controller
http://localhost:8111/test/update
Change the above role to @Secured({"ROLE_normal", "ROLE_manager"}) and you will not be able to access it
3.7.2 @PreAuthorize
First enable the annotation function:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize: Annotation is suitable for permission verification before entering the method. @PreAuthorize can pass the roles/permissions parameters of the logged-in user to the method.
Securitydemo1Application.java
@SpringBootApplication
@MapperScan(basePackages = "com.atguigu.securitydemo1.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class Securitydemo1Application {
public static void main(String[] args) {
SpringApplication.run(Securitydemo1Application.class, args);
}
}
Modify TestController.java
@GetMapping("update")
@PreAuthorize("hasAnyAuthority('admin')")
public String update() {
return "hello update";
}
Make sure that MyUserNameDetailsService.java has the admin role
. Make sure
Use lucy to log in and test:
3.7.3 @PostAuthorize
First enable the annotation function:
@EnableGlobalMethodSecurity(prePostEnabled = true)
has been enabled before
The @PostAuthorize annotation is not used much, and the permission verification is performed after the method is executed, which is suitable for verifying the permission with the return value.
Modify TestController.java
@GetMapping("update")
@PostAuthorize("hasAnyAuthority('admin')")
public String update() {
System.out.println("update……");
return "hello update";
}
Modify MyUserNameDetailsService.java
and use lucy to log in to test:
it is found that there is no access right, but IDEA background output, indicating that it is verified after the method is executed.
3.7.4 @PostFilter
@PostFilter : Filter the data after authorization verification, leaving the data whose user name is admin1.
The filterObject in the expression refers to an element in the method return value List.
Modify TestController.java
@GetMapping("getAll")
@PreAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'lucy'")
@ResponseBody
public List<Users> getAllUser() {
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(1, "lucy", "123"));
list.add(new Users(2, "admin2", "888"));
System.out.println("list:" + list);
return list;
}
access
http://localhost:8111/test/getAll
username lucy
IDEA background printing
list:[Users(id=1, username=lucy, password=123),
Users(id=2, username=admin2, password=888)]
3.7.5 @PreFilter
@PreFilter: Filter data before entering the controller
Modify TestController.java
@GetMapping("getTestPreFilter")
@PreAuthorize("hasAnyAuthority('admin')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<Users> getTestPreFilter(@RequestBody List<Users> list) {
list.forEach(t -> {
System.out.println(t.getId() + "\t" + t.getUsername());
});
return list;
}
Login first, then use postman to test
http://localhost:8111/test/getTestPreFilter
Tested Json data:
[{
"id": "1",
"username": "lucy",
"password": "123"
},
{
"id": "2",
"username": "mary",
"password": "456"
},
{
"id": "3",
"username": "admins11",
"password": "11888"
},
{
"id": "4",
"username": "admins22",
"password": "22888"
}]
After clicking send on my side, it jumps to the login page. I am very confused. If you have tested the effect here, you can explain how to test
SecurityConfigCustomize.java on my side. I deliberately added no login verification and it has no effect.
3.7.6 Permission expressions
Mastering is not done here. You can view the official website:
permission expression
3.8 Remember the account number and password when logging in (based on the database)
The cookie-based method
is based on the principle of Spring Security
: onLoginSuccess in loginSuccess in
the parent class of UsernamePasswordAuthenticationFilter
successfulAuthentication method rememberMeServices class AbstractRememberMeServices
The onLoginSuccess in the PersistentTokenBasedRememberMeServices class
creates a table and writes the logic, and writes the Token into the Cookie.
This class is the process of creating a table and writing it. The doFilter method of the
JdbcTokenRepositoryImpl.java RememberMeAuthenticationFilter.
3.8.1 Create table
Don't create it here, it is created by Spring Security itself, the table named persistent_logins
The specific creation process is in JdbcTokenRepositoryImpl.
3.8.2 Add the configuration file of the database
This one was added before
3.8.3 Writing configuration classes
Create a configuration class
BrowserSecurityConfig
This code, open after the first startup, then comment out this code jdbcTokenRepository.setCreateTableOnStartup(true);
@Configuration
public class BrowserSecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new
JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
3.8.4 Modify security configuration class
Add SecurityConfigCustomize.java
@Autowired
private PersistentTokenRepository tokenRepository;
// 开启记住我功能
http.rememberMe()
.tokenRepository(tokenRepository)
.userDetailsService(userDetailsService);
3.8.5 Add a remember me checkbox to the page
login.html add
记住我:<input type="checkbox" name="remember-me" title="记住密码"/>
Here: the value of the name attribute must be remember-me. It cannot be changed to other values
3.8.6 Login Test
access:
http://localhost:8111/test/index
Press F12 for developer mode
Successful login
After successful login, close the browser and visit again, and find that it can still be used!
Note here, it is best to use the goole browser to see the effect
3.8.7 Set validity period
The default is 2 weeks. However, by setting the valid time of the status, you can log in normally even if the project is restarted next time. Set SecurityConfigCustomize.java
in the configuration file
// 开启记住我功能
http.rememberMe()
.tokenRepository(tokenRepository)
.userDetailsService(myUserNameDetailsService)
.tokenValiditySeconds(1800) // 单位是秒
;
3.9 User Logout
3.9.1 Add a logout link to the login page
success.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!
<a href="/logout">退出</a>
</body>
</html>
3.9.2 Add the exit mapping address in the configuration class
SecurityConfigCustomize.java
added
// 退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/quit").permitAll();
Revise
.defaultSuccessUrl("/success.html") // 登录成功后要跳转的路径
Modify TestController.java
to add exit method
@GetMapping("quit")
public String quit() {
return "quit";
}
3.9.3 Testing
access
http://localhost:8111/login.html
After logging in, jump to success.html
and click to exit
After logging out, it is impossible to access the controller that needs to be accessed when logging in!
For example, we visit:
http://localhost:8111/test/index
need to log in again
3.10 CSRF
3.10.1 CSRF understanding
Cross-site request forgery (English: Cross-site request forgery), also known as one-click attack or session riding, usually abbreviated as CSRF or XSRF, is a method that coerces users to execute unintentional The attack method of the operation. Compared with cross-site scripting (XSS), which uses the user's trust in the specified website, CSRF uses the website's trust in the user's web browser.
Cross-site request attack, simply put, is that the attacker uses some technical means to deceive the user's browser to visit a website that he has authenticated and perform some operations (such as sending emails, sending messages, and even property operations such as transferring money and purchasing goods) ). Since the browser has been authenticated, the visited website will be considered to be a real user operation and run. This exploits a vulnerability in user authentication on the web: simple authentication can only guarantee that the request is from a user's browser, but not that the request itself is voluntary.
Starting with Spring Security 4.0, CSRF protection is enabled by default to prevent CSRF from attacking applications, and Spring Security CSRF protects against PATCH, POST, PUT and DELETE methods.
3.10.2 Case
Add a hidden field to the login page:
login.html
<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
csrf SecurityConfigCustomize.java in the class that closes the security configuration
// http.csrf().disable();
Add dependencies in pom.xml
<!-- 对Thymeleaf添加Spring Security标签支持 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Here is a brief introduction to the principle and configuration of the implementation. For the specific implementation, this method does not work and needs to be skipped. We can just use the information provided by the teacher to have a look.
Import course case:
Modify SecurityConfig.java, turn off csrf protection
Modify csrfTest.html, comment CSRF logic
Modify csrf_token.html, comment CSRF logic
Start the service, access
http://localhost:8112/toupdate
After clicking to modify (actually log in), access correctly
Then, we annotate
SecurityConfig.java, which means to enable CSRF protection
// http.csrf().disable();
Visit again after restarting the service
http://localhost:8112/toupdate
After clicking Login, I found that I was redirected.
The reason is that the SecurityConfig.java class is configured with a failure jump.
Solution: We remove the comments of csrfTest.html and csrf_token.html and
visit again after restarting
http://localhost:8112/toupdate
After logging in, it is as follows:
We have selected the F12 developer and looked at the page elements, and the token has been brought here
3.10.3 The principle of Spring Security implementing CSRF:
Simple schematic:
-
Generate csrfToken and save it in HttpSession or Cookie.
The class diagram is as follows:
The SaveOnAccessCsrfToken class has an interface CsrfTokenRepository.
The current interface implementation class: HttpSessionCsrfTokenRepository, CookieCsrfTokenRepository
-
When the request comes, extract the csrfToken from the request, compare it with the saved csrfToken, and then judge whether the current request is legal. Mainly through the CsrfFilter filter to complete.
The doFilterInternal method of the CsrfFilter.java class
where the requireCsrfProtectionMatcher.matches method
View the DefaultRequiresCsrfMatcher method
where these requests "GET", "HEAD", "TRACE", "OPTIONS" CSRF are not protected