1.什么是Security?
Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
how it works?
- Filter
拦截Http请求,获取用户名和秘密等认证信息 关键方法:
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException;
- AuthenticationManager
从filter中获取认证信息,然后查找合适的AuthenticationProvider来发起认证流程 关键方法:
Authentication authenticate(Authentication authentication) throws AuthenticationException;
- AuthenticationProvider
调用UserDetailsService来查询已经保存的用户信息并与从http请求中获取的认证信息比对。如果成功则返回,否则则抛出异常。 关键方法:
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;
- UserDetailsService
负责获取用户保存的认证信息,例如查询数据库。 关键方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
这些组件都是抽象的,每个都可以有不同的实现,换句话说都是可以定制,特别灵活,所以就特别复杂。具体到我们这个默认的例子中,使用的都是默认实现:
- Filter: UsernamePasswordAuthenticationFilter
- AuthenticationManager: ProviderManager
- AuthenticationProvider: DaoAuthenticationProvider
- UserDetailsService: InMemoryUserDetailsManager
业务流程是不是很清晰,之所以感觉复杂是因为经过框架的一顿设计,拉长了调用链。虽然设计上复杂了,但是如果理解了这套设计流程,终端用户使用就会简单很多,不理解的话,感觉特别复杂
2.环境搭建
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 COMMENT '用户ID',`username` varchar(100) CHARACTER SET utf8 NULL DEFAULT NULL COMMENT '登录账号',`password` varchar(255) CHARACTER SET utf8 NULL DEFAULT NULL COMMENT '密码'
)ENGINE = InnoDB CHARACTER SET = utf8 COMMENT = '用户表' 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', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', '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
3.代码工程
实验目的:实现基于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*/@Overridepublic 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 endsif (configAttribute.getAttribute().equals(authority.getAuthority())) {return;}}}throw new AccessDeniedException("no permission");}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic 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.@ResourceMenuMapper menuMapper;// Spring Security uses the getAttributes method in the FilterInvocationSecurityMetadataSource interface to determine which roles are required for a request. @Overridepublic 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.*/@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}/*** The supports method returns whether the class object supports verification.*/@Overridepublic 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 {@AutowiredUserService userService;@BeanRoleHierarchy roleHierarchy() {RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";roleHierarchy.setHierarchy(hierarchy);return roleHierarchy;}@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected 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@Overrideprotected 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>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O object) {object.setSecurityMetadataSource(cfisms());object.setAccessDecisionManager(cadm());return object;}}).and().formLogin().loginProcessingUrl("/login").permitAll().and().csrf().disable();}@BeanCustomFilterInvocationSecurityMetadataSource cfisms() {return new CustomFilterInvocationSecurityMetadataSource();}@BeanCustomAccessDecisionManager cadm() {return new CustomAccessDecisionManager();}
}
代码生成
配置文件在resource目录下,mybatis-generator.xml
如何生成代码?
可以参见这篇文章?Spring Boot集成Druid快速入门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.DruidDataSourceurl: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456mybatis:mapper-locations:- classpath:mapper/**/*.xml
以上只是一些关键代码,所有代码请参见下面代码仓库
代码仓库
- GitHub - Harries/springboot-demo: a simple springboot demo with some components for example: redis,solr,rockmq and so on.
4.测试
- 启动Spring Boot应用
- 访问地址http://127.0.0.1:8088/hello
- 此时会要求登陆,输入数据库不同角色的用户,验证不同的权限(密码都是加密的123)
5.引用
- Spring Boot集成Security快速入门Demo | Harries Blog™
- Spring Boot Security Auto-Configuration | Baeldung
- 先决条件 :: Spring Security Reference