Spring Security is a Java framework for securing the security of applications. It provides a comprehensive set of security solutions, including authentication, authorization, attack prevention, and more. Spring Security is based on the concept of filter chains and can be easily integrated into any Spring-based application. It supports a variety of authentication options and authorization policies, allowing developers to choose the one that suits them. In addition, Spring Security offers additional features such as integration with third-party authentication providers and single sign-on, as well as session management and password encoding. In conclusion, Spring Security is a powerful and easy-to-use framework that can help developers improve the security and reliability of their applications.
Intercept HTTP requests and obtain authentication information such as usernames and secrets Key Methods:
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException;
- AuthenticationManager
Obtain the authentication information from the filter and find the appropriate AuthenticationProvider to initiate the authentication process Key Methods:
Authentication authenticate(Authentication authentication) throws AuthenticationException;
- AuthenticationProvider
Call the UserDetailsService operation to query the saved user information and compare it with the authentication information obtained from the http request. If successful, it returns, otherwise an exception is thrown. Key Methods:
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
- UserDetailsService
It is responsible for obtaining the authentication information saved by the user, such as querying the database. Key Methods:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
These components are abstract, and each can have a different implementation, in other words, they can be customized, they are very flexible, and therefore they are very complex. In our default example, the default implementation is used:
- Filter: UsernamePasswordAuthenticationFilter
- AuthenticationManager: ProviderManager
- AuthenticationProvider: DaoAuthenticationProvider
- UserDetailsService: InMemoryUserDetailsManager
The business process is not very clear, and the reason why it feels complex is that the design of the framework has elongated the call chain. Although the design is complicated, if you understand the design process, it will be much easier for the end user to use, and if you don’t understand it, it will feel particularly complicated
mysql database
docker run --name docker-mysql-5.7 -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7
init data
create database demo;
DROP TABLE IF EXISTS `jwt_user`;
CREATE TABLE `jwt_user`(
`id` varchar(32) CHARACTER SET utf8 NOT NULL ,
`username` varchar(100) CHARACTER SET utf8 NULL DEFAULT NULL ,
`password` varchar(255) CHARACTER SET utf8 NULL DEFAULT NULL
)ENGINE = InnoDB CHARACTER SET = utf8 ROW_FORMAT = Compact;
INSERT INTO jwt_user VALUES('1','admin','123');-- ----------------------------
-- Table structure for menu
-- ----------------------------
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '/db/**');
INSERT INTO `menu` VALUES ('2', '/admin/**');
INSERT INTO `menu` VALUES ('3', '/user/**');
-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES ('1', '1', '1');
INSERT INTO `menu_role` VALUES ('2', '2', '2');
INSERT INTO `menu_role` VALUES ('3', '3', '3');
-- ----------------------------
-- Table structure for role
-- ----------------------------
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', 'dba');
INSERT INTO `role` VALUES ('2', 'admin', 'admin');
INSERT INTO `role` VALUES ('3', 'user', 'user');
-- ----------------------------
-- Table structure for user
-- ----------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- password is 123
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
INSERT INTO `user_role` VALUES ('4', '3', '3');
remark
msyql account:root
mysql password:123456
Purpose: To control permissions based on MySQL
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springboot-demo</artifactId>
<groupId>com.et</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>security</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<!-- <build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<phase>package</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<verbose>true</verbose>
<overwrite>true</overwrite>
<configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
</configuration>
</plugin>
</plugins>
</build>-->
</project>
config
package com.et.security.config;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* Comparing role information in the AccessDecisionManager class
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {/**
* Override the decide method, in which it is judged whether the currently logged in user has the role information required for the current request URL. If not, an AccessDeniedException is thrown, otherwise nothing is done.
*
* @param auth Information about the currently logged in user
* @param object FilterInvocationObject, you can get the current request object
* @param ca FilterInvocationSecurityMetadataSource The return value of the getAttributes method in is the role required by the current request URL
*/
@Override
public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) {
Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
for (ConfigAttribute configAttribute : ca) {
/*
* If the required role is ROLE_LOGIN, it means that the currently requested URL can be accessed after the user logs in.
* If auth is an instance of UsernamePasswordAuthenticationToken, it means that the current user has logged in and the method ends here, otherwise it enters the normal judgment process.
*/
if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken) {
return;
}
for (GrantedAuthority authority : auths) {
// If the current user has the role required by the current request, the method ends
if (configAttribute.getAttribute().equals(authority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no permission");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
package com.et.security.config;
import com.et.security.entity.Menu;
import com.et.security.entity.Role;
import com.et.security.mapper.MenuMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {AntPathMatcher antPathMatcher = new AntPathMatcher(); // AntPathMatcher is mainly used to implement ant-style URL matching.
@Resource
MenuMapper menuMapper;
// Spring Security uses the getAttributes method in the FilterInvocationSecurityMetadataSource interface to determine which roles are required for a request. @Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
/*
* The parameter of this method is a FilterInvocation. Developers can extract the currently requested URL from the FilterInvocation.
* The return value is Collection<ConfigAttribute>, indicating the role required by the current request URL.
*/
String requestUrl = ((FilterInvocation) object).getRequestUrl();
/*
* Obtain all resource information from the database, that is, the menu table in this case and the role corresponding to the menu,
* In a real project environment, developers can cache resource information in Redis or other cache databases.
*/
List<Menu> allMenus = menuMapper.getAllMenus();
// Traverse the resource information. During the traversal process, obtain the role information required for the currently requested URL and return it.
for (Menu menu : allMenus) {
if (antPathMatcher.match(menu.getPattern(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++) {
roleArr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);
}
}
// If the currently requested URL does not have a corresponding pattern in the resource table, it is assumed that the request can be accessed after logging in, that is, ROLE_LOGIN is returned directly.
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* The getAllConfigAttributes method is used to return all defined permission resources. SpringSecurity will verify whether the relevant configuration is correct when starting.
* If verification is not required, this method can directly return null.
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* The supports method returns whether the class object supports verification.
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
package com.et.security.config;
import com.et.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowired
UserService userService;
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// The memory user is not configured, but the UserService just created is configured into the AuthenticationManagerBuilder.
auth.userDetailsService(userService);
}
@Order
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
/*.antMatchers("/admin/**").hasRole("ADMIN")//Indicates that the user accessing the URL in the "/admin/**" mode must have the role of ADMIN
.antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().authenticated()//Indicates that in addition to the URL pattern defined previously, users must access other URLs after authentication (access after logging in) */
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(cfisms());
object.setAccessDecisionManager(cadm());
return object;
}
})
.and()
.formLogin()
.loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
@Bean
CustomFilterInvocationSecurityMetadataSource cfisms() {
return new CustomFilterInvocationSecurityMetadataSource();
}
@Bean
CustomAccessDecisionManager cadm() {
return new CustomAccessDecisionManager();
}
}
Code generation
The configuration file is in the resource directory and is mybatis-generator.xml
How do I generate code?
you can read this article
Spring Boot Integration with Druid Quick Start Demo
DemoApplication.java
package com.et.security;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(value = "com.et.security.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.yaml
server:
port: 8088
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis:
mapper-locations:
- classpath:mapper/**/*.xml
The above are just some of the key codes, all of which can be found in the repositories below
Code repositories
- Start the Spring Boot application
- Access address http://127.0.0.1:8088/hello
- At this time, you will be asked to log in, enter users with different roles in the database, and verify different permissions (passwords are encrypted 123)