Spring Security基础使用

news2025/7/11 18:15:57

Spring Security基础使用

定义

官方文档:https://docs.spring.io/spring-security/reference/index.html

security结合servelt:https://docs.spring.io/spring-security/reference/5.6/servlet/getting-started.html

Security 官方示例项目:https://github.com/spring-projects/spring-security-samples/tree/5.6.x

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。

认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

SpringSecurity是一个提供身份验证、授权和防止常见攻击的框架。凭借对保护命令式和反应式应用程序的一流支持,它是保护基于Spring的应用程序的事实标准。

SpringSecurity 可以针对 servlet and reactive. ,显然我reactive我还不会,这里就针对serrvlet了解一下

然后本文大致讲一下基础使用,和部分源码流程(主要用于辅助理解)

Spring Security和Shiro比较

在 Java 生态中,目前有 Spring Security 和 Apache Shiro 两个安全框架,可以完成认证和授权的功能。

  • Spring Security
  • Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

相同点:

1:认证功能

2:授权功能

3:加密功能

4:会话管理

5:缓存支持

6:rememberMe功能…

不同点:

优点:

1:Spring Security基于Spring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发

2:Spring Security功能比Shiro更加丰富些,例如安全防护

3:Spring Security社区资源比Shiro丰富

缺点:

1:Shiro的配置和使用比较简单,Spring Security上手复杂

2:Shiro依赖性低,不需要任何框架和容器,可以独立运行,而Spring Security依赖于Spring容器

总之先随便了解一个

Spring Security 模块

官方文档:https://docs.spring.io/spring-security/reference/5.6.8/modules.html

以下仅做参考,具体查看那官方文旦,更全面

  • 核心模块 - spring-security-core.jar:包含核心验证和访问控制类和接口,远程支持的基本配置API,是基本模块
  • 远程调用 - spring-security-remoting.jar:提供与 Spring Remoting 集成
  • 网页 - spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL访问控制
  • 配置 - spring-security-config.jar:包含安全命令空间解析代码,若使用XML进行配置则需要
  • LDAP - spring-security-ldap.jar:LDAP 验证和配置,若需要LDAP验证和管理LDAP用户实体
  • ACL访问控制表 - spring-security-acl.jar:ACL(Access Control List)专门领域对象的实现
  • CAS - spring-security-cas.jar:CAS(Central Authentication Service)客户端继承,若想用CAS的SSO服务器网页验证
  • OpenID - spring-security-openid.jar:OpenID网页验证支持
  • Test - spring-security-test.jar:支持Spring Security的测试

Spring Security使用

多种方式设置用户名密码

1、快速开始

依赖
        <!-- 实现对 Spring Security 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
编写一个controller

…,随意啦,这里不贴代码了

测试

然后如果通过postman访问,服务端报错,响应401(表示没有认证)

org.springframework.security.access.AccessDeniedException: Access is denied

通过浏览器访问就会跳转到一个内置的登录界面

控制台日志在启动时会打印访问的密码,而默认的登录用户是user

Using generated security password: d5b4810b-7716-4fb3-8a85-537ee4181af0

This generated password is for development use only. Your security configuration must be updated before running your application in production.
测试原理1

1、日志中你可以看到密码是UserDetailsServiceAutoConfiguration这个自动配置类打印出来的,

2、这个配置类为我们创建了一个(基于内存的用户信息管理对象),

3、然后这个对象初始化的时候使用了SecurityProperties中的静态内部类user对象,这个User类的name属性默认是user,passwoed默认属性是通过uuid生成

4、InMemoryUserDetailsManager在配置类中初始化时会打印这个默认生成的密码

2、基于配置文件中设置的用户名密码访问

配置
spring:
  # Spring Security 配置项,对应 SecurityProperties 配置类
  security:
    # 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
    user:
      name: sry # 账号
      password: 123456 # 密码
测试

就是这么配置之后就可以通过我们设置的用户名和密码登录成功

测试原理2

请先查看本文测试原理1

1、SecurityProperties上面有注解,会读取我们的配置项,根据配置项来初始化我们的User

@ConfigurationProperties(prefix = "spring.security")

2、初始化自然会调用其setPassWord方法,这个方法中设置了User的属性passwordGenerated,这个属性默认是true,

3、UserDetailsServiceAutoConfiguration初始化InMemoryUserDetailsManager时,InMemoryUserDetailsManager会根据User的属性passwordGenerated来判断是否打印初始密码

3、实现UserDetailsService接口

代码
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ArrayList<String> roles = new ArrayList<>();
        UserDetails userDetails = User.withUsername("fox")
                .password("{noop}123456").roles(StringUtils.toStringArray(roles)).build();

        return userDetails;
    }
}
测试原理3

1、UserDetailsServiceAutoConfiguration创建了默认的 InMemoryUserDetailsManager(基于内存的用户管理对象),InMemoryUserDetailsManager继承自UserDetailsService,所以我们可以自行继承UserDetailsService来实现我们自己的获取用户信息的管理类

2、UserDetailsServiceAutoConfiguration上包含了注解,所以你懂得,如果我们自行实现了UserDetailsService,那么UserDetailsServiceAutoConfiguration配置类将不会再生效

@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },

3、所以我们这么搞了之后,配置文件中配置用户名密码就无效了

4.WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter是Sercurity的配置类,可以配置很多设置,这里演示配置用户密码

代码
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        String password = passwordEncoder().encode("123456");
        auth
            // 使用基于内存的 InMemoryUserDetailsManager
            .inMemoryAuthentication()
            //使用 PasswordEncoder 密码编码器
            //.passwordEncoder(passwordEncoder())
            // 配置用户
            .withUser("fox").password(password).roles("admin")
            // 配置其他用户
            .and()
            .withUser("fox2").password(password).roles("user");

        // 或者你也可以直接指定我们自己实现的userDetailsService
//        auth.userDetailsService(userDetailsService);

    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }
}
测试

配置类中设置的userDetailsService(用户信息获取类)的优先级更高,我们自己实现的userDetailsService实现已经不生效了

如果有多个userDetailsService实现,可以使用 auth.userDetailsService(userDetailsService);进行指定

WebSecurityConfigurerAdapter配置类生效原理4

1、@SpringBootApplication中包含了@EnableAutoConfiguration注解,这个注解中包含了Import(AutoConfigurationImportSelector.class),AutoConfigurationImportSelector会为我们加载依赖引入的包中/META-INF/spring.factories,

2、spring-boot-autoconfigure-2.3.5.RELEASE.jar!/META-INF/spring.factories 中包含了 SecurityAutoConfiguration,所以我们启动后会扫描这个配置类,一般spring-boot-starter-xx 都会有对应的配置类,命名一般都是XXAutoConfiguration

3、SecurityAutoConfiguration配置类中又导入了SpringBootWebSecurityConfiguration和WebSecurityEnablerConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
		SecurityDataConfiguration.class, ErrorPageSecurityFilterConfiguration.class })
public class SecurityAutoConfiguration {

4、关键来了,SpringBootWebSecurityConfiguration会生成一个默认的WebSecurityConfigurerAdapter,但上面有@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class),说明最终注入到容器中的还是我们自行实现的WebSecurityConfigurerAdapter

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

	@Bean
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
		return http.build();
	}

}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(DefaultWebSecurityCondition.class)
public @interface ConditionalOnDefaultWebSecurity {

}
class DefaultWebSecurityCondition extends AllNestedConditions {

	DefaultWebSecurityCondition() {
		super(ConfigurationPhase.REGISTER_BEAN);
	}

	@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
	static class Classes {

	}

	@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
	static class Beans {

	}

}

5、我们自行实现的WebSecurityConfigurerAdapter又是怎么生效的内WebSecurityEnablerConfiguration中@EnableWebSecurity

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {

6、@EnableWebSecurity中的@EnableGlobalAuthentication又包含了AuthenticationConfiguration

@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {

}

7、AuthenticationConfiguration这个类中包含了大量Security配置的初始化,但是我没找到哪里用了WebSecurityConfigurerAdapter的配置,后续再说吧

PasswordEncoder设置

官方文档

编码器使用示例

UserDetails userDetails = User.withUsername("fox")
        .password("{noop}123456").roles(StringUtils.toStringArray(roles)).build();
    @Autowired
    PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String pw = passwordEncoder.encode("123456");
UserDetails userDetails = User.withUsername("fox")
        .password(pw).roles(StringUtils.toStringArray(roles)).build();
    }

spring security支持多种密码编码器,使用方式同上,仅做演示,一般我们如果使用了某种加密方式,存在数据库的应该就是对应加密方式的密文了。

支持哪些编码器

我们可以在PasswordEncoderFactories中查看到具体支持哪些编码器

	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

配置默认的编码器

我们密码使用了那种编码器,那么我们secrity也要使用对应的编码器才能通过,在我们示例1中,security默认使用的是{noop}编码器,也就是不加密,而sercurity推荐使用BCryptPasswordEncoder

配置的方式如下

随意找个配置类注入编码器对象即可,为了统一管理,最好统一放在WebSecurityConfigurerAdapter下

@Configuration
public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder是一种单向加密机制,相同的密码经过加密可以得到不同的密文。

需要说明的,security设置编码器后输入的密码还是明文

实现数据库认证

表构建

总之就是在我们自行实现UserDetailsService或获取用户信息,角色,权限等数据

主要 是创建用户表,用户和角色关联表,角色表,角色和权限关联表,权限表

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(64) NOT NULL COMMENT '密码,加密存储',
  `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
  `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `phone` (`phone`) USING BTREE,
  UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
insert  into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values
(37,'sry','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','158xxxxxxx','xxxxxxx@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');

CREATE TABLE `tb_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
  `name` varchar(64) NOT NULL COMMENT '角色名称',
  `enname` varchar(64) NOT NULL COMMENT '角色英文名称',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
insert  into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values
(37,0,'超级管理员','fox',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05');


CREATE TABLE `tb_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户 ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
insert  into `tb_user_role`(`id`,`user_id`,`role_id`) values
(37,37,37);

CREATE TABLE `tb_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
  `name` varchar(64) NOT NULL COMMENT '权限名称',
  `enname` varchar(64) NOT NULL COMMENT '权限英文名称',
  `url` varchar(255) NOT NULL COMMENT '授权路径',
  `description` varchar(200) DEFAULT NULL COMMENT '备注',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
insert  into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,`updated`) values
(37,0,'系统管理','System','/',NULL,'2019-04-04 23:22:54','2019-04-04 23:22:56'),
(38,37,'用户管理','SystemUser','/users/',NULL,'2019-04-04 23:25:31','2019-04-04 23:25:33'),
(39,38,'查看用户','SystemUserView','',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'),
(40,38,'新增用户','SystemUserInsert','',NULL,'2019-04-04 15:30:31','2019-04-04 15:30:44'),
(41,38,'编辑用户','SystemUserUpdate','',NULL,'2019-04-04 15:30:32','2019-04-04 15:30:45'),
(42,38,'删除用户','SystemUserDelete','',NULL,'2019-04-04 15:30:48','2019-04-04 15:30:45'),
(44,37,'内容管理','SystemContent','/contents/',NULL,'2019-04-06 18:23:58','2019-04-06 18:24:00'),
(45,44,'查看内容','SystemContentView','/contents/view/**',NULL,'2019-04-06 23:49:39','2019-04-06 23:49:41'),
(46,44,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2019-04-06 23:51:00','2019-04-06 23:51:02'),
(47,44,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2019-04-06 23:51:04','2019-04-06 23:51:06'),
(48,44,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2019-04-06 23:51:08','2019-04-06 23:51:10');

CREATE TABLE `tb_role_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL COMMENT '角色 ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
insert  into `tb_role_permission`(`id`,`role_id`,`permission_id`) values
(37,37,37),
(38,37,38),
(39,37,39),
(40,37,40),
(41,37,41),
(42,37,42),
(43,37,44),
(44,37,45),
(45,37,46),
(46,37,47),
(47,37,48);

配置和依赖

省略。。

代码

总之就是在UserDetailsService实现里写从数据库获取用户信息的逻辑

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    public User getByUsername(String username) {
        return userMapper.getByUsername(username);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("自定义登录逻辑");
        //从mysql查询用户
        User user = getByUsername(username);
        List<GrantedAuthority> authorities = new ArrayList<>();
        if(user!=null){
            List<Permission> permissions = permissionMapper.selectByUserId(user.getId());
            //设置权限
            permissions.forEach(permission -> {
                if (permission!=null && !StringUtils.hasText(permission.getEnname())){
                    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getEnname());
                    authorities.add(grantedAuthority);
                }
            });
            // 封装成UserDetails的实现类
            UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
                    .password(user.getPassword()).authorities(authorities).build();
            return userDetails;
        }else {
            throw new UsernameNotFoundException("用户名不存在");
        }

    }
}

存在多个UserDetailsService实现,单独指定

@Configuration
public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Resource
    private UserServiceImpl userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
}

自定义登录相关页面

默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成

编写登录页面

<!DOCTYPE html>
<html lang="en">
<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>

配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //认证相关页面和接口配置
        http.formLogin()  //表单提交
                .loginPage("/login.html") //自定义登录页面
                .loginProcessingUrl("/user/login")  //登录访问路径,必须和表单提交接口一样
                .defaultSuccessUrl("/admin/index");   //认证成功之后跳转的路径,默认是跳转项目根路径,没有这个路径就404,当然404之前登录还是成功的

        // 授权
        http.authorizeRequests()
                //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/user/login","/login.html").permitAll()
                .anyRequest().authenticated();  //需要认证
        //关闭csrf防护
         http.csrf().disable();

    }

认证成功和失败后转发地址,仅仅做记录

        http.formLogin()
            .successForwardUrl("/tomain");
            .failureForwardUrl("/toerrer");

配置说明

有些配置见名之意,就不细说了,注释很清楚

loginProcessingUrl

这个配置是指定登录接口的访问路径,如果是自定义登录界面,表单提交的地址需要和此地址匹配

然后就是这个并不需要你写登录接口,具体请看本文 认证流程 第一步,总之就是AbstractAuthenticationProcessingFilter#doFilter中会判断是不是这个路径,然后从这个路径的请求中获取认证参数进行接下来的处理

CSRF
什么是CSRF

​ CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。

跨域在浏览器端限制的,但是我们可以告诉浏览器端我们允许跨域,这里假设你打开了某个不安全的网站,同时你打开了支付宝的支付界面(假设哈),不安全的网站引导你点击某个图片或者链接,或者纯粹就是打开了后台就会跑脚本,然后这个链接或者脚本是访问支付宝的某个接口,由于在浏览器你已经登录,那么第三方网站访问支付宝接口就像你本人访问一样,然后你信息就暴露了

上述流程是基于你是通过cookie来保存你的sessionId (JSESSIONID),然后你服务端又是通过sessionId来确认请求者身份的

CSRF怎么防护
  • 验证 HTTP Referer 字段
  • 在请求地址中添加 token 并验证
  • 在 HTTP 头中自定义属性并验证

Referer是Http请请求头的一部分,从百度跳转其他网页,这个是浏览器为我们附加的,而不安全的网站不通过浏览器就没法利用浏览器的cookie机制

Referer: https://www.baidu.com/link?url=i2tk5htJZ-MW1xYpBULweJdYCihg588Jbs3fNNTm-U3PY5rasRwkR4P8y85WskjmOK-nJ6_H0ORsg-dsGH8vpa&wd=&eqid=a3938c58000593f600000002637b49cc

就是用户访问时请求头或者请求参数中附加一个令牌,要求浏览器端请求时附带这个令牌,没有这个令牌就拒绝访问

CSRF在Sercurity中实现

从 Spring Security4开始CSRF防护默认开启,默认会拦截请求,进行CSRF处理。CSRF为了保证不是其
他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生,在渲染请求页面时
埋入页面)的内容,如果token和服务端的token匹配成功,则正常访问。

security默认生成的表单中是隐含了_csrf 这个值的,security会在CsrfFilter这个过滤器中验证_csrf 的值

然后我发现登录后,我的请求不附带这个_csrf也可以访问,这个是因为CsrfFilter中对于特定类型的请求如Get请求是允许不附带_csrf这个值访问的,这块应该是可以进行配置的,比如配置_csrf为其他的提交参数,配置get也需要验证,具体怎么配,待定

不想关闭csrf处理

如果我们不想关闭csrf的防护,首先我们登录界面就需要和Security自动生成的登录界面一样,附加一个_Csrf的提交参数

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
    用户名:<input type="text" name="username"/><br/>
    密码: <input type="password" name="password"/><br/>
    <input type="submit" value="提交"/>
</form>
</body>
</html>

前后端分离csrf处理

首先我们的CsrfFilter中如果发现请求中没有_csrf,那么是会为我们生成一个放入请求中,

		request.setAttribute(CsrfToken.class.getName(), csrfToken);
		request.setAttribute(csrfToken.getParameterName(), csrfToken);

处理如下

我们配置一个接口不被csrf过滤器拦截,然后通过这个接口获取token,前端请求时将这个token放在请求头或请求参数中即可,注意放在请求头中请求头的key应该是X-CSRF-TOKEN

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
              // 网络防护忽略请求
                .csrf().ignoringAntMatchers("/user/login")
    }

// 在登录成功处理器中保存这个token
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());

测试

1、首先如果你访问的是其他接口,然后没有权限,跳转登录界面登录成功后是会再次访问原来的接口

2、如果是直接访问登录界面,登录成功后会进入默认的欢迎页

登录参数修改

默认的参数是username password,源码参考认证流程第2步

http.formLogin() //表单提交
                .usernameParameter("userName")  
                .passwordParameter("passWord")

自定义登录成功和失败处理器

配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //认证相关页面和接口配置
        http.formLogin()  //表单提交
            .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
            .failureHandler(new MyAuthenticationFailHandler("/error.html"));

        // 授权
        http.authorizeRequests()
                //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/login","/error.html").permitAll()
                .anyRequest().authenticated();  //需要认证
        //关闭csrf防护
         http.csrf().disable();

    }

成功处理器

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private String redirectUrl;

    public MyAuthenticationSuccessHandler(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(redirectUrl);
    }
}

失败处理器

public class MyAuthenticationFailHandler implements AuthenticationFailureHandler {

    private String redirectUrl;

    public MyAuthenticationFailHandler(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.sendRedirect(redirectUrl);
    }
}

认证对象信息

处理其中我们可以获取到认证对象

会话管理

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

用户信息获取

    /**
     * 登录成功后默认访问项目根路径
     * */
    @GetMapping("/")
    public String index() {
        // 获取当前登录的用户信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!authentication.isAuthenticated()){
            return null;
        }
        Object principal = authentication.getPrincipal();
        String username = null;
        if (principal instanceof UserDetails) {
            username =((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        log.info(username);
        return "index.html";
    }

用户信息获取获取原理5

参考认证流程第一步,登录成成功后,我们在这么一段代码中设置了sessionid和我们的认证对象的绑定关系

// 应该是这一段,进去了有一部分代码看不明白 
this.sessionStrategy.onAuthentication(authenticationResult, request, response);

登录成功后我们后续访问会进过SecurityContextPersistenceFilter,然后就会根据sessionId获取我们的认证对象

		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
			SecurityContextHolder.setContext(contextBeforeChainExecution);

SecurityContextHolder关联了会话策略,默认是ThreadLocalSecurityContextHolderStrategy,也就是说我们认证对象是存在一个ThreadLocal当中

会话控制

我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:

机制描述
always如果session不存在总是需要创建
ifRequired如果需要就创建一个session(默认)登录时
neverSpring Security 将不会创建session,但是如果应用中其他地方创建了session,那么Spring Security将会使用它
statelessSpring Security将绝对不会创建session,也不使用session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制。
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}

会话并发控制

仅做记录,未测试

用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频VIP账号一样,最多只能五个人同时登录,第六个人限制登录。

  • maximumSessions:最大会话数量,设置为1表示一个用户只能有一个会话
  • expiredSessionStrategy:会话过期策略
http.sessionManagement()
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(new MyExpiredSessionStrategy());
public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HttpServletResponse response = event.getResponse();
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("您已被挤兑下线!");
    }
}

测试

  1. 使用chrome浏览器,先登录,再访问http://localhost:8080/admin/index
  2. 使用ie浏览器,再登录,再访问http://localhost:8080/admin/index
  3. 使用chrome浏览器,重新访问http://localhost:8080/admin/index,会执行expiredSessionStrategy,页面上显示”您已被挤兑下线!“

阻止用户第二次登录

sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,当达到maximumSessions设置的最大会话个数时阻止登录。

http.sessionManagement()
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(new MyExpiredSessionStrategy())
                .maxSessionsPreventsLogin(true);

集群session

仅做记录,未测试

实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常了。解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库(redis、mongodb、mysql等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信息,如用户在服务器一上登录,将会话信息保存到库中,用户的下次请求被分配到服务器二,服务器二从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了。

引入spring session依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0</version>
</dependency>

修改application.yaml

spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379

server:
  port: 8080
  servlet:
    session:
      timeout: 600

测试

启动两个服务8080,8081 ,其中一个登录后访问http://localhost:8080/admin/index,另外一个不需要登录就可以访问

缺点:

Spring Session + Redis实现分布式Session共享 有个非常大的缺陷, 无法实现跨域名共享session , 只能在单台服务器上共享session , 因为是依赖cookie做的 , cookie 无法跨域。 Spring Session一般是用于多台服务器负载均衡时共享Session的,都是同一个域名,不会跨域。

安全会话cookie

我们可以使用httpOnly和secure标签来保护我们的会话cookie:

  • httpOnly:如果为true,那么浏览器脚本将无法访问cookie
  • secure:如果为true,则cookie将仅通过HTTPS连接发送

spring boot配置文件:

server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true

RememberMe实现

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。

RememberMe配置
@Autowired
public DataSource dataSource;

public PersistentTokenRepository persistentTokenRepository(){
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    //设置数据源
    jdbcTokenRepository.setDataSource(dataSource);
    return jdbcTokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    
//记住我
http.rememberMe()
    .tokenRepository(persistentTokenRepository())//设置持久化仓库
    .tokenValiditySeconds(3600) //超时时间,单位s 默认两周
    .userDetailsService(userService);  //设置自定义登录逻辑
}
创建表persistent_logins

官方文档

create table persistent_logins (
	username varchar(64) not null, 
	series varchar(64) primary key,
	token varchar(64) not null, 
	last_used timestamp not null
)
登录界面实现

如果自行实现,参考默认登录界面即可

测试

登录后关闭浏览器,再次打开浏览器访问http://localhost:8077/main.html,发现不用重新登录了

原理6

首先这个机制是通过cookie实现的,cookie设置了超时时间后,就不会在关闭浏览器后失效,如图,然后就是RememberMeAuthenticationFilter中会有一下从数据库获取用户信息,从而不用登录的逻辑,

退出登录

默认的退出登录路径
public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractHttpConfigurer<LogoutConfigurer<H>, H> {
	private List<LogoutHandler> logoutHandlers = new ArrayList<>();
	private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
	private String logoutSuccessUrl = "/login?logout";
	private LogoutSuccessHandler logoutSuccessHandler;
	private String logoutUrl = "/logout";
修改默认的登出配置
http.logout()
.logoutUrl("/logout") // 同样的,你不需要写这个接口,只是一个路径配置而已,security匹配到了这个路径就会进行登出的相关操作
.logoutSuccessUrl("/login.html");
退出登录原理7

LogoutFilter中

			// 清楚session相关操作
			this.handler.logout(request, response, auth);
			// 登出成功后跳转相关操作
			this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);

httpBasic

https://baijiahao.baidu.com/s?id=1650164072428116018&wfr=spider&for=pc

认证流程

官方登录认证流程:https://docs.spring.io/spring-security/reference/5.6/servlet/authentication/architecture.html#servlet-authentication-authentication

1、用户提交密码,然后进入Security过滤器,这里从UsernamePasswordAuthenticationFilter开始,请求先进入UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter#doFilter中,

		// (1)判断这个请求是不是登录请求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		......
        // 然后就会调用这个方法进行认证,而这个是在UsernamePasswordAuthenticationFilter中实现的
	   Authentication authenticationResult = attemptAuthentication(request, response);
......
    // 根据session策略,设置sessionId
    this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
    chain.doFilter(request, response);
}
// 成功后将用户信息存入SecurityContext(全局上下文是存在ThreadLocal<SecurityContext>)中,总之你之后可以获取到security上下文,然后获取用户信息
successfulAuthentication(request, response, chain, authenticationResult);

2、UsernamePasswordAuthenticationFilter#attemptAuthentication会判断是否post请求,将获取的用户名密码组装成UsernamePasswordAuthenticationToken对象,然后进行认证

//	public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
//	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
//	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
// 然后就会在这个方法总获取登录的用户名密码,否是明文。默认是获取username和password
String username = obtainUsername(request);
.....
// 组装用户信息
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

// 具体的认证方法
return this.getAuthenticationManager().authenticate(authRequest);

3、AuthenticationManager有许多实现,默认的情况写这里走的是ProviderManager#authenticate认证方法,然后遍历认证管理器中的认证提供者列表AuthenticationProvider,

		for (AuthenticationProvider provider : getProviders()) {
			......
            // 最终情况这里使用的是DaoAuthenticationProvider
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			.........
		}

4、这里先回调用DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider#authenticate

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		......
                    // 这里会加载用户,具体实现在DaoAuthenticationProvider中
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) 
                                    
        ......
            // 认证前检查
			this.preAuthenticationChecks.check(user);
            // 通过配置的PasswordEncoder判断密码是否输入正确的逻辑,                       
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
         // 认证成功后,创建一个UsernamePasswordAuthenticationToken对象,其中存放了用户信息,还有是否认证成功的信息,这个是否认证成功的信息其他过滤器会用到                           
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

5、具体实现在DaoAuthenticationProvider的retrieveUser中通过UserDetailsService去获取用户,然后这就到了我们自行实现的UserDetailsService实现类中,

	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
......
    // 获取用户的逻辑
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
......
	}

用户授权(访问控制)

授权的方式包括 web授权和方法授权,web授权是通过 url拦截进行授权,方法授权是通过 方法拦截进行授权。他
们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor;若为方
法授权则拦截器为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方
法授权,最后决策通过,则允许访问资源,否则将禁止访问。

web授权

官方示例:https://docs.spring.io/spring-security/reference/5.8/servlet/authorization/authorize-http-requests.html

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护 ,Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

http.authorizeRequests()
            //设置哪些路径可以直接访问,不需要认证
            .antMatchers("/user/login","/login.html").permitAll()
            .anyRequest().authenticated();  //需要认证才能访问

访问控制的url匹配

在配置类中http.authorizeRequests() 主要是对url进行控制。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。

anyRequest()

表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证,会放在最后。

.anyRequest().authenticated()
antMatchers

方法定义如下:

public C antMatchers(String... antPatterns)

参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。

ANT通配符有三种:

通配符说明
?匹配任何单字符
*匹配0或者任意数量的字符
**匹配0或者更多的目录

在实际项目中经常需要放行所有静态资源:

// 放行 js和css 目录下所有的文件
.antMatchers("/js/**","/css/**").permitAll()
// 只要是.js 文件都放行
.antMatchers("/**/*.js").permitAll()    
regexMatchers()

使用正则表达式进行匹配。

//所有以.js 结尾的文件都被放行
.regexMatchers( ".+[.]js").permitAll()

无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是HttpMethod ,表示请求方式,当设置了 HttpMethod 后表示只有设定的特定的请求方式才执行对应的权限设置。

.antMatchers(HttpMethod.POST,"/admin/demo").permitAll()
.regexMatchers(HttpMethod.GET,".+[.]jpg").permitAll()
mvcMatchers()

适用于配置了 servletPath 的情况。 servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在application.properties 中添加下面内容设置 ServletPath。

spring.mvc.servlet.path=/web

在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers()返回值特有的方法,antMatchers()和 regexMatchers()没有这个方法。在 servletPath() 中配置了 servletPath 后,mvcMatchers()直接写 SpringMVC 中@RequestMapping()中设置的路径即可。

.mvcMatchers("/admin/demo").servletPath("/web").permitAll()

如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效的:

.antMatchers("/web/admin/demo").permitAll()

RequestMatcher接口

RequestMatcherSpring Security Web的一个概念模型接口,用于抽象建模对HttpServletRequest请求的匹配器这一概念。Spring Security内置提供了一些RequestMatcher实现类:

实现类介绍
AnyRequestMatcher匹配任何请求
AntPathRequestMatcher使用ant风格的路径匹配模板匹配请求
ELRequestMatcher使用EL表达式匹配请求
IpAddressMatcher基于IP地址匹配请求,支持IPv4IPv6
MediaTypeRequestMatcher基于MediaType匹配请求
RegexRequestMatcher基于正则表达式匹配请求
RequestHeaderRequestMatcher基于头部值比较匹配请求
AndRequestMatcherand组合多个RequestMatcher
OrRequestMatcheror组合多个RequestMatcher
NegatedRequestMatchernot操作一个RequestMatcher
MvcRequestMatcher使用Spring MVCHandlerMappingIntrospector匹配请求

内置的访问控制

  • 【常用】#permitAll() 方法,所有用户可访问。

  • 【常用】#denyAll() 方法,所有用户不可访问。

  • 【常用】#authenticated() 方法,登录用户可访问。

  • #anonymous() 方法,无需登录,即匿名用户可访问。

  • #rememberMe() 方法,通过 remember me登录的用户可访问。

  • #fullyAuthenticated() 方法,非 remember me 登录的用户可访问。

  • #hasIpAddress(String ipaddressExpression) 方法,来自指定 IP 表达式的用户可访问。

  • 【常用】#hasRole(String role) 方法, 拥有指定角色的用户可访问,角色将被增加 “ROLE_” 前缀。

  • 【常用】#hasAnyRole(String... roles) 方法,拥有指定任一角色的用户可访问。

  • 【常用】#hasAuthority(String authority) 方法,拥有指定权限(authority)的用户可访问。

  • 【常用】#hasAuthority(String... authorities) 方法,拥有指定任一权限(authority)的用户可访问。

  • 【最牛】#access(String attribute) 方法,当 Spring EL 表达式的执行结果为 true 时,可以访问。

基于权限的访问控制

除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。

hasAuthority(String)

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。权限名称大小写敏感

 return new User("fox", pw, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user"));//admin,user就是用户的权限

    @Override
    public UserDetails loadUserByUsername(String username) {
        ArrayList<String> roles = new ArrayList<>();

        UserDetails userDetails = User.withUsername("sry").password(passwordEncoder.encode("123456"))
                .authorities("admin","user").build();
        return userDetails;
    }

在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。

.antMatchers("/admin/demo").hasAuthority("admin")

否则报403错误

hasAnyAuthority(String …)

如果用户具备给定权限中某一个,就允许访问。

.antMatchers("/admin/demo").hasAnyAuthority("admin","System")
基于角色的访问控制
hasRole(String)

如果用户具备给定角色就允许访问,否则出现 403。参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_admin 其中 admin是角
色名,ROLE_是固定的字符开头。

return new User("fox", pw, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,user"));//给用户赋予admin角色

这样不用加ROLE_

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ArrayList<String> roles = new ArrayList<>();
        String[] rolesStr = StringUtils.toStringArray(roles);
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        UserDetails userDetails = User.withUsername("sry").password(passwordEncoder.encode("123456"))
                .roles("admin","user").build();
        return userDetails;
    }

使用 hasRole()时参数也只写admin 即可,否则启动报错。

.antMatchers("/admin/demo").hasRole("admin")
hasAnyRole(String …)

如果用户具备给定角色的任意一个,就允许被访问 。

基于IP限制访问
hasIpAddress(String)

如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr() 获取 ip 地址。需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。

//  localhost --> getRemoteAddr:  0:0:0:0:0:0:0:1
.antMatchers("/admin/demo").hasIpAddress("127.0.0.1")
自定义403处理方案

使用 Spring Security 时经常会看见 403(无权限)。Spring Security 支持自定义权限受限处理,需要实现 AccessDeniedHandler接口

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        out.flush();
        out.close();
    }
}

在配置类中设置访问受限后交个MyAccessDeniedHandler处理

http.exceptionHandling()
     .accessDeniedHandler(new MyAccessDeniedHandler());
基于表达式的访问控制
access(表达式)

之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)

https://docs.spring.io/spring-security/site/docs/5.2.7.RELEASE/reference/htmlsingle/#tech-intro-access-control

表达式根对象的基类是SecurityExpressionRoot,提供了一些在web和方法安全性中都可用的通用表达式。

可以通过 access() 实现和之前学习的权限控制完成相同的功能。

.antMatchers("/user/login","/login.html").access("permitAll")
.antMatchers("/admin/demo").access("hasAuthority('System')") 
自定义方法

判断登录用户是否具有访问当前 URL 的权限。

@Component
public class MySecurityExpression implements MySecurityExpressionOperations{
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        // 获取主体
        Object obj = authentication.getPrincipal();
        if (obj instanceof UserDetails){
            UserDetails userDetails = (UserDetails) obj;
            //
            String name = request.getParameter("name");
            //获取权限
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            //判断name值是否在权限中
            return authorities.contains(new SimpleGrantedAuthority(name));
        }
        return false;
    }
}

在 access 中通过bean的beanName.方法(参数)的形式进行调用:

.anyRequest().access("@mySecurityExpression.hasPermission(request,authentication)")

方法授权

基于注解的访问控制

Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。

这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

JSR-250注解
@RolesAllowed

表示访问对应方法时所应该具有的角色。其可以标注在类上,也可以标注在方法上,当标注在类上时表示其中所有方法的执行都需要对应的角色,当标注在方法上表示执行该方法时所需要的角色,当方法和类上都使用了@RolesAllowed进行标注,则方法上的@RolesAllowed将覆盖类上的@RolesAllowed,即方法上@RolesAllowed将对当前方法起作用。@RolesAllowed的值是由角色名称组成的数组。

@PermitAll

表示允许所有的角色进行访问,也就是说不进行权限控制。@PermitAll可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法不进行权限控制,而标注在类上时表示对类里面所有的方法都不进行权限控制。(1)当@PermitAll标注在类上,而@RolesAllowed标注在方法上时则按照@RolesAllowed将覆盖@PermitAll,即需要@RolesAllowed对应的角色才能访问。

(2)当@RolesAllowed标注在类上,而@PermitAll标注在方法上时则对应的方法也是不进行权限控制的。

(3)当在类和方法上同时使用了@PermitAll和@RolesAllowed时先定义的将发生作用(这个没多大的实际意义,实际应用中不会有这样的定义)。

@DenyAll

是和PermitAll相反的,表示无论什么角色都不能访问。@DenyAll只能定义在方法上。你可能会有疑问使用@DenyAll标注的方法无论拥有什么权限都不能访问,那还定义它干啥呢?使用@DenyAll定义的方法只是在我们的权限控制中不能访问,脱离了权限控制还是可以访问的。

开启注解
在启动类或者在配置类上添加 @EnableGlobalMethodSecurity(jsr250Enabled = true)

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

在controller方法上添加@RolesAllowed注解

@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"})
//@PermitAll
@GetMapping("/demo")
public String demo() {
    return "spring security demo";
}

由于url的访问控制是优先于方法的访问控制,测试时注意配置类中配置

@Secured注解

@Secured是由Spring Security定义的用来支持方法权限控制的注解。它的使用也是需要启用对应的支持才会生效的。@Secured 是专门用于判断是否具有角色的,能写在方法或类上。参数要以 ROLE_开头。

开启注解
在启动类或者在配置类上添加 @EnableGlobalMethodSecurity(securedEnabled = true)

@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

在controller方法上添加@Secured 注解

@Secured("ROLE_ADMIN")
@GetMapping("/demo")
public String demo() {
    return "spring security demo";
}
支持表达式的注解

Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
使用@PreAuthorize和@PostAuthorize进行访问控制

@PreAuthorize可以用来控制一个方法是否能够被调用,执行之前先判断权限,大多情况下都是使用这个注解。

//@PreAuthorize("hasRole('ROLE_ADMIN')")
//@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
//限制只能查询Id小于10的用户
@PreAuthorize("#id<10")
@RequestMapping("/findById")
public User findById(long id) {
    User user = new User();
    user.setId(id);
    return user;
}


// 限制只能查询自己的信息
@PreAuthorize("principal.username.equals(#username)")
@RequestMapping("/findByName")
public User findByName(String username) {
    User user = new User();
    user.setUsername(username);
    return user;
}

//限制只能新增用户名称为abc的用户
@PreAuthorize("#user.username.equals('abc')")
@RequestMapping("/add")
public User add(User user) {
    return user;
}

@PostAuthorize可以在方法调用完之后进行权限检查

// 在方法find()调用完成后进行权限检查,如果返回值的id是偶数则表示校验通过,否则表示校验失败,将抛出AccessDeniedException
@PostAuthorize("returnObject.id%2==0")
public User find(int id) {
    User user = new User();
    user.setId(id);
    return user;
}
使用@PreFilter和@PostFilter进行过滤

使用@PreFilter和@PostFilter可以对集合类型的参数或返回值进行过滤。使用@PreFilter和@PostFilter时,Spring Security将移除使对应表达式的结果为false的元素。

@PostFilter("filterObject.id%2==0")
public List<User> findAll() {
    List<User> userList = new ArrayList<User>();
    User user;
    for (int i=0; i<10; i++) {
        user = new User();
        user.setId(i);
        userList.add(user);
    }
    return userList;
}

@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {

}

授权流程

基于Filter

  1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子
    类拦截。

  2. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类
    DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection 。SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则

  3. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资
    源,否则将禁止访问。

基于AOP

//MethodSecurityInterceptor#invoke
public Object invoke(MethodInvocation mi) throws Throwable {
    InterceptorStatusToken token = super.beforeInvocation(mi);

    Object result;
    try {
        result = mi.proceed();
    }
    finally {
        super.finallyInvocation(token);
    }
    return super.afterInvocation(token, result);
}

实现原理分析

核心配置

配置生效原理

请查看本文原理4部分,当然我就分析了一部分

除了 SecurityAutoConfiguration 之外,还有UserDetailsServiceAutoConfiguration,SecurityFilterAutoConfiguration ,你应该可以在SecurityFilterAutoConfiguration 中找到过滤器链执行生效的代码

配置项

WebSecurityConfigurerAdapter中我们可以选择自己想要修改的内容,来进行重写

HttpSecurity

安全过滤器链配置

@Configuration
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .failureForwardUrl("/login?error")
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll()
                .and()
            .httpBasic()
                .disable();
    }
}
  • authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
  • formLogin()对应表单认证相关的配置
  • logout()对应了注销相关的配置
  • httpBasic()可以配置basic登录

所有的http相关配置可以通过查看HttpSecurity的主要方法获取。

方法说明
openidLogin()用于基于 OpenId 的验证
headers()将安全标头添加到响应,比如说简单的 XSS 保护
cors()配置跨域资源共享( CORS )
sessionManagement()允许配置会话管理
portMapper()允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee()配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509()配置基于x509的认证
rememberMe允许配置“记住我”的验证
authorizeRequests()允许基于使用HttpServletRequest限制访问
requestCache()允许配置请求缓存
exceptionHandling()允许配置错误处理
securityContext()在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi()将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf()添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout()添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous()允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin()指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login()根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel()配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic()配置 Http Basic 验证
addFilterBefore()在指定的Filter类之前添加过滤器
addFilterAt()在指定的Filter类的位置添加过滤器
addFilterAfter()在指定的Filter类的之后添加过滤器
and()连接以上策略的连接器,用来组合安全策略。实际上就是"而且"的意思
WebSecurity

核心过滤器配置

WebSecurity 基于Servlet Filter 用来配置 springSecurityFilterChain,而springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器DelegatingFilterProxy 。我们一般不会过多来自定义 WebSecurity , 使用较多的使其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。

public void configure(WebSecurity web) throws Exception {
    web
        .ignoring()
        .antMatchers("/resources/**");
}
AuthenticationManagerBuilder

认证管理器配置

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .inMemoryAuthentication()
        .withUser("admin").password("admin").roles("USER");
}

全局配置, 使用@Autowired注入的AuthenticationManagerBuilder是全局的身份认证器,作用域可以跨越多个WebSecurityConfigurerAdapter。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .inMemoryAuthentication()
        .withUser("admin").password("admin").roles("USER");
}

FilterChain

官方文档:https://docs.spring.io/spring-security/reference/5.6/servlet/architecture.html

上面记录的认证和授权流程已经涉及到了许多的过滤器,比如处理认证的UsernamePasswordAuthenticationFilter和LogoutFilter等等,总之Security将我们认证和授权的相关操作都集中在过滤器中,进入过滤器的流程如下

1、首先DelegatingFilterProxy是继承GenericFilterBean(这个应该是spring扩展的),而GenericFilterBean又javax.servlet.Filter,多以tomcat在接收到请求后会执行我们的DelegatingFilterProxy这个过滤器,然后 DelegatingFilterProxy就是一个对于servlet filter的代理,用这个类的好处主要是通过Spring容器来管理servlet filter的生命周期

DelegatingFilterProxy extends GenericFilterBean

2、FilterChainProxy extends GenericFilterBean,总之DelegatingFilterProxy执行以下代码后会进入我们的FilterChainProxy 的doFilter方法中,而FilterChainProxy 就security实现的过滤器

delegate.doFilter(request, response, filterChain);

3、然后FilterChainProxy 的doFilterInternal中就会具体进入我们security的过滤器中

		// 获取的应该是security的过滤器
		List<Filter> filters = getFilters(firewallRequest);

		VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
		virtualFilterChain.doFilter(firewallRequest, firewallResponse);

过滤器链的执行顺序

Below is a comprehensive list of Spring Security Filter ordering:

  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter 保存更新清除SecurityContext,如果认证是通过session存储,这里就可以拿到已经认证过的认证对象
  • HeaderWriterFilter
  • CorsFilter:允许跨域策略的验证
  • CsrfFilter :跨站请求伪造防护,默认是验证请求头或请求参数_csrf
  • LogoutFilter : 拦截对登出请求的访问,然后进行登出,默认是拦截/logout
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter : 用户名密码登录
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter : 登录界面生成
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter:如果是session控制的登录,这里可以控制同一个用户最多允许几个会话
  • DigestAuthenticationFilter: 对登录用户名密码的一种签名处理,
  • BearerTokenAuthenticationFilter : 默认是请求头Authorization 请求头中以Bearer 开头的token,占时我只知道这个oauth2 场景汇中客户端访问 资源服务器时,资源服务器会在这里进行认证
  • BasicAuthenticationFilter : 配置开启后,通过这个处理base64加密的用户名密码
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter:异常转换筛选器允许将拒绝访问的异常和身份验证异常转换为HTTP响应。
  • FilterSecurityInterceptor:是否经过认证,是否有权限访问
  • SwitchUserFilter

其他

不管是认证流程,还是配置生效的流程,都没有了解的很清楚,特别是配置生效逻辑,不过SpringSecurity大致流程明白了,其他的以后再说

关联信息

  • 关联的主题:
  • 上一篇:
  • 下一篇:
  • image: 20221021/1
  • 转载自:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/74680.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度对比 Footprint 和 Dune 数据模型

在产品建设方面&#xff0c;数据所有权转移已迅速成为需要解决的关键问题之一。社交媒体平台滥用数据的现象频发&#xff0c;我们亟需建立方案来解决这一问题。鉴于此&#xff0c;区块链提供了一种功能&#xff0c;可以永恒地存储、转移数据&#xff0c;并创建市场&#xff0c;…

ChatGPT帮你写代码?人工智能ChatGPT之于Web3的几点思考

人工智能AI有人热情膜拜&#xff0c;有人畏惧如虎&#xff0c;AI的发展已经进入高层面的快车道&#xff0c;技术更新迭代日新夜异&#xff0c;技术无罪也无善恶&#xff0c;曾经科幻未来的人机交流&#xff0c;共生共存如今在我们的脑海中已经有了轮廓的意识感想&#xff0c;科…

Spring底层事务原理

Spring事务底层原理一、EnableTransactionManagement工作原理二、Spring事务基本执行原理三、Spring事务的过程四、Spring事务传播机制五、Spring事务传播机制分类&#xff08;1&#xff09;案例分析、情况1&#xff08;2&#xff09;案例分析、情况2&#xff08;3&#xff09;…

SEVNLDAEFR, 186142-28-9

This peptide substrate corresponds to the Swedish Lys-Met/Asn-Leu (K670N/M671L) mutation of the amyloid precursor protein (APP) β-secretase cleavage site. It has been used for assaying β-secretase activity.该肽底物对应于淀粉样前体蛋白(APP) β-分泌酶切割位…

政企办公「分水岭」以至,融云百幄数智化破局

中国政企数智办公平台行业研究报告 246 年前&#xff0c;亚当斯密便在《国富论》中提出&#xff0c;劳动分工是提高社会生产效率的主要原因。从工厂定岗&#xff0c;到国际分工&#xff0c;分工的价值已经深入社会生活和工作的方方面面。关注【融云RongCloud】&#xff0c;了解…

六、使用注解开发

文章目录一、CRUD 注解(舍弃 mapper.xml): Select、 Insert、 Update、 Delete、 Param1、Select2、Insert3、Delete4、Update5、Param二、Lombok 插件&#xff0c;减少重复代码1、Data一、CRUD 注解(舍弃 mapper.xml): Select、 Insert、 Update、 Delete、 Param 通过注解去处…

【Netty】Netty高性能原理剖析

Netty高性能原理剖析1、前言2、Netty高性能2.1 多路复用通讯方式2.2 异步通讯 NIO2.3 零拷贝(DIRECT BUFFERS 使用堆外直接内存&#xff09;2.4 内存池(基于内存池的缓冲区重用机制&#xff09;2.5 高效的 Reactor 线程模型2.5.1 Reactor 单线程模型2.5.2 Reactor 多线程模型2.…

低代码有多爽?1个月的活,只需3天干完

仅看到“低代码”三个字&#xff0c;不少程序员就会吐槽“只适合简单业务&#xff0c;普通查改还行&#xff0c;复杂业务简直是灾难”&#xff0c;甚至认为是“儿童玩具”。 不少人自以为是程序员的二把刀&#xff0c;以为自己懂完了&#xff0c;在没理解低代码的应用场景&…

怎么压缩动态图片?手机怎么压缩gif动图?

在平时的聊天当中为了增加聊天的趣味性我们经常会保存一些有趣的gif动图表情包&#xff0c;但是由于gif图一般是由多帧组成&#xff0c;因此有的gif动图就会非常大&#xff0c;无法添加到表情当中&#xff0c;这时候就需要将gif压缩变小&#xff0c;那么我们用手机怎么压缩gif动…

华硕编程竞赛11月JAVA专场 C题太空遨游 题解

作者主页&#xff1a;Designer 小郑 作者简介&#xff1a;Java全栈软件工程师一枚&#xff0c;来自浙江宁波&#xff0c;负责开发管理公司OA项目&#xff0c;专注软件前后端开发&#xff08;Vue、SpringBoot和微信小程序&#xff09;、系统定制、远程技术指导。CSDN学院、蓝桥云…

chatGPT辣么火,你却不会注册

chatGPT 是什么&#xff1f; 一款目前超级火的 AI 对话聊天工具&#xff0c;只是不同于其他的智能聊天机器人那样&#xff0c;他非常的智能。 可以回答你的技术问题、帮你写代码、还能帮你写小说等等&#xff0c;发挥你的想象力&#xff0c;让他干点啥都行。 比如让他帮你用…

[附源码]Python计算机毕业设计SSM基于旅游服务平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

VMware使用和Linux安装Docker

一、VMware安装和配置 二、Linux安装Docker PS:记得每次配置安装新东西前先拍摄拍照&#xff0c;这样即使安装坏了&#xff0c;不需要重装虚拟机。&#xff08;吃过这个亏&#xff09; 1.Docker支持64位版本的CentOS 7和CentOS 8及更高版本&#xff0c;它要求Linux内核版本不…

Web前端大作业—— 饮食餐饮网站 咖啡网站pc端带轮播(5个页面)HTML+CSS+JavaScript 学生美食网页设计作品 学生餐饮文化网页模板

&#x1f468;‍&#x1f393;静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计&#x1f469;‍&#x1f393;,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等&#xff0c;用的最多的还是DW&#xff0c;当然不同软件写出的…

数据技术篇之日志采集

第2章 日志采集 1.日志采集有哪些 页面浏览日志 页面浏览日志是指当一个页面被浏览器加载呈现时采集的日志。此类日志 也是最基础的互联网日志&#xff0c;也是目前所有互联网产品的两大基本指标&#xff1a;页面浏览量&#xff08;Page View&#xff0c;PV&#xff09;和访客…

谈一谈 IPA 上传到 App Store Connect 的几种方法

谈一谈​ 1、前言​ 关于上传​ 2、Xcode​ 利用​ 3、Application Loader​ 当然&#xff0c;Xcode 这种方式&#xff0c;是需要有源代码情况下&#xff0c;才能上传。所以&#xff0c;就会有没有源代码的情况&#xff0c;怎么上传的情况啦&#xff01;​ Application L…

ET框架解读其一

ECS&#xff1f; 真正的ECS属于是entity-component-system组件里面只有数据没有方法&#xff0c;system里面是针对组件的方法&#xff0c;system通过查找只需要关注自己想关注的组件集合就可以。但是ET框架的代码在组件里面写满了方法&#xff0c;有数据又有方法的组件&#x…

Spire.Doc for Java 10.12.2 update Word to PDF/HTML to Word

谷歌找破解版Spire.Doc for Java is a professional Word API that empowers Java applications to create, convert, manipulate and print Word documents without dependency on Microsoft Word. By using this multifunctional library, developers are able to process co…

并发编程概述 和 并行编程(Parallel Framework)

任务&#xff08;task&#xff09; 异步编程&#xff08;async&await&#xff09; 并发编程概述 前言 说实话&#xff0c;在我软件开发的头两年几乎不考虑并发编程&#xff0c;请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天&#xff08;剩下时间各种…

HPPH-SiO2 NPs/PEG/DSPE光克洛修饰介孔二氧化硅纳米粒子/聚乙二醇/磷脂的研究

小编这里分享的科研知识是HPPH-SiO2 NPs/PEG/DSPE光克洛修饰介孔二氧化硅纳米粒子/聚乙二醇/磷脂的研究&#xff0c;来看&#xff01; 光克洛修饰介孔二氧化硅纳米粒子的研究&#xff1a; 光敏剂的研发历经以卟吩姆钠为代表的第一代卟啉类光敏剂到以维替泊芬,他拉泊芬和替莫泊芬…