互联网分布式应用之SpringSecurity

SpringSecurity

Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台。
   

课程内容的介绍

1. SpringSecurity基本应用
2. SpringSecurity高级应用
  

一、SpringSecurity基本应用

1.初识Spring Security
1.1 Spring Security概念
Spring Security是Spring采用 AOP 思想,基于 servlet过滤器 实现的安全框架。它提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的事实上的标准。
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。
   
特征
对身份验证和授权的全面且可扩展的支持。
保护免受会话固定,点击劫持,跨站点请求伪造等攻击。
Servlet API集成。
与Spring Web MVC的可选集成。
    
1.2 快速入门案例
1.2.1 环境准备
我们准备一个SpringMVC+Spring+jsp的Web环境,然后在这个基础上整合SpringSecurity。
  
首先创建Web项目

    
添加相关的依赖
  <dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.1.RELEASE</version></dependency><dependency><groupId>javax.servlet</groupId><artifactId>servlet-api</artifactId><version>2.5</version><scope>provided</scope></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.25</version></dependency><dependencies>
   
添加相关的配置文件
Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.bobo.service" >
</context:component-scan></beans>
    
SpringMVC配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsd"><context:component-scan base-package="com.bobo.controller">
</context:component-scan><mvc:annotation-driven ></mvc:annotation-driven>
</beans>
  
log4j.properties文件
log4j.rootCategory=INFO, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[QC] %p [%t] %C.%M(%L) | %m%n
  
web.xml
<!DOCTYPE web-app PUBLIC"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN""http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app version="2.5" id="WebApp_ID" xmlns="http://java.sun.com/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"><display-name>Archetype Created Web Application</display-name><!-- 初始化spring容器 --><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:applicationContext.xml</param-value></context-param><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- post乱码过滤器 --><filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><init-param><param-name>encoding</param-name><param-value>utf-8</param-value></init-param></filter><filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- 前端控制器 --><servlet><servlet-name>dispatcherServletb</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" --><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-mvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcherServletb</servlet-name><!-- 拦截所有请求jsp除外 --><url-pattern>/</url-pattern></servlet-mapping></web-app>
   
添加Tomcat的插件 启动测试
<plugins><plugin><groupId>org.apache.tomcat.maven</groupId><artifactId>tomcat7-maven-plugin</artifactId><version>2.2</version><configuration><port>8082</port><path>/</path></configuration></plugin>
</plugins>
   

    
1.2.2 整合SpringSecurity
添加相关的依赖
spring-security-core.jar 核心包,任何SpringSecurity的功能都需要此包。
spring-security-web.jar:web工程必备,包含过滤器和相关的web安全的基础结构代码。
spring-security-config.jar:用于xml文件解析处理。
spring-security-tablibs.jar:动态标签库。
    <!-- 添加SpringSecurity的相关依赖 --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId><version>5.1.5.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-taglibs</artifactId><version>5.1.5.RELEASE</version></dependency>
    
web.xml文件中配置SpringSecurity。
  <!-- 配置过滤器链 springSecurityFilterChain 名称固定 --><filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern></filter-mapping>
   
添加SpringSecurity的配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:security="http://www.springframework.org/schema/security"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><!-- SpringSecurity配置文件 --><!--auto-config:表示自动加载SpringSecurity的配置文件use-expressions:表示使用Spring的EL表达式--><security:http auto-config="true" use-expressions="true"><!--拦截资源pattern="/**" 拦截所有的资源access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER 这个角色可以访问资源--><security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')" />
</security:intercept-url></security:http><!-- 认证用户信息 --><security:authentication-manager><security:authentication-provider><security:user-service ><!-- 设置一个账号 zhangsan 密码123 {noop} 表示不加密 具有的角色是  ROLE_USER--><security:user name="zhangsan" authorities="ROLE_USER" password="{noop}123" ></security:user><security:user name="lisi" authorities="ROLE_USER" password="{noop}123456" ></security:user></security:user-service></security:authentication-provider></security:authentication-manager>
</beans>
   
将SpringSecurity的配置文件引入到Spring中。

  
启动测试访问

  
2. 认证操作
2.1 自定义登录页面
如何使用我们自己写的登录页面呢?
<%--Created by IntelliJ IDEA.User: dpbDate: 2021/3/16Time: 16:57To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
<head><title>Title</title>
</head>
<body><h1>登录页面</h1><form action="/login" method="post">账号:<input type="text" name="username"><br>密码:<input type="password" name="password"><br><security:csrfInput/><input type="submit" value="登录"></form>
</body>
</html>
  
修改相关的配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:security="http://www.springframework.org/schema/security"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><!-- SpringSecurity配置文件 --><!--auto-config:表示自动加载SpringSecurity的配置文件use-expressions:表示使用Spring的EL表达式--><security:http auto-config="true" use-expressions="true"><!-- 匿名访问登录页面--><security:intercept-url pattern="/login.jsp" access="permitAll()"/><!--拦截资源pattern="/**" 拦截所有的资源access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER 这个角色可以访问资源--><security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')" /><!--配置认证的信息<security:form-login login-page="/login.jsp"login-processing-url="/login"default-target-url="/home.jsp"authentication-failure-url="/error.jsp"/>--><!-- 注销 --><security:logout logout-url="/logout"logout-success-url="/login.jsp" /><!-- 关闭CSRF拦截<security:csrf disabled="true" />--></security:http><!-- 认证用户信息 --><security:authentication-manager><security:authentication-provider><security:user-service ><!-- 设置一个账号 zhangsan 密码123 {noop} 表示不加密 具有的角色是  ROLE_USER--><security:user name="zhangsan" authorities="ROLE_USER" password="{noop}123" ></security:user><security:user name="lisi" authorities="ROLE_USER" password="{noop}123456" ></security:user></security:user-service></security:authentication-provider></security:authentication-manager>
</beans>
   

  
访问home.jsp页面后会自动跳转到自定义的登录页面,说明这个需求是实现了。

  
但是当我们提交了请求后页面出现了如下的错误。
  

    
2.2 关闭CSRF拦截
为什么系统默认的登录页面提交没有CRSF拦截的问题呢?

 
我自定义的认证页面没有这个信息怎么办呢?两种方式:
关闭CSRF拦截

  
登录成功~
   
使用CSRF防护
在页面中添加对应taglib

  
我们访问登录页面

  
登录成功

  
2.3 数据库认证
前面的案例我们的账号信息是直接写在配置文件中的,这显然是不太好的,我们来介绍小如何实现和数据库中的信息进行认证。
  
添加相关的依赖
    <dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.4</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>2.0.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.8</version></dependency>
     
添加配置文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/logistics?characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.bobo.service" ></context:component-scan><!-- SpringSecurity的配置文件 --><import resource="classpath:spring-security.xml" /><context:property-placeholder location="classpath:db.properties" /><bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource"><property name="url" value="${jdbc.url}" /><property name="driverClassName" value="${jdbc.driver}" /><property name="username" value="${jdbc.username}" /><property name="password" value="${jdbc.password}" /></bean><bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sessionFactoryBean" ><property name="dataSource" ref="dataSource" /><property name="configLocation" value="classpath:mybatis-config.xml" /><property name="mapperLocations" value="classpath:mapper/*.xml" /></bean><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="com.bobo.mapper" /></bean>
</beans>
  
需要完成认证的service中继承 UserDetailsService父接口。

  
实现类中实现验证方法。
package com.bobo.service.impl;import com.bobo.mapper.UserMapper;
import com.bobo.pojo.User;
import com.bobo.pojo.UserExample;
import com.bobo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class UserServiceImpl implements IUserService {@Autowiredprivate UserMapper mapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {// 根据账号查询用户信息UserExample example = new UserExample();example.createCriteria().andUserNameEqualTo(s);List<User> users = mapper.selectByExample(example);if(users != null && users.size() > 0){User user = users.get(0);if(user != null){List<SimpleGrantedAuthority> authorities = new ArrayList<>();// 设置登录账号的角色authorities.add(new SimpleGrantedAuthority("ROLE_USER"));UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUserName(),"{noop}"+user.getPassword(),authorities);return userDetails;}}return null;}}
  
最后修改配置文件关联我们自定义的service即可。

  
2.4 加密
在SpringSecurity中推荐我们是使用的加密算法是 BCryptPasswordEncoder。
  
首先生成秘闻

    
修改配置文件

   

   
去掉 {noop}

  
2.5 认证状态
用户的状态包括 是否可用,账号过期,凭证过期,账号锁定等等。

  
我们可以在用户的表结构中添加相关的字段来维护这种关系。
   
2.6 记住我
在表单页面添加一个记住我的按钮。

  
在SpringSecurity中默认是关闭 RememberMe功能的,我们需要放开。

  
这样就配置好了。
  
记住我的功能会方便大家的使用,但是安全性却是令人担忧的,因为Cookie信息存储在客户端很容易被盗取,这时我们可以将这些数据持久化到数据库中。
CREATE TABLE `persistent_logins` (`username` VARCHAR (64) NOT NULL,`series` VARCHAR (64) NOT NULL,`token` VARCHAR (64) NOT NULL,`last_used` TIMESTAMP NOT NULL,PRIMARY KEY (`series`)
) ENGINE = INNODB DEFAULT CHARSET = utf8
  

   

   
3. 授权
3.1 注解使用
开启注解的支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:security="http://www.springframework.org/schema/security"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><context:component-scan base-package="com.bobo.controller"></context:component-scan><mvc:annotation-driven ></mvc:annotation-driven><!--开启权限控制注解支持jsr250-annotations="enabled" 表示支持jsr250-api的注解支持,需要jsr250-api的jar包pre-post-annotations="enabled" 表示支持Spring的表达式注解secured-annotations="enabled" 这个才是SpringSecurity提供的注解--><security:global-method-securityjsr250-annotations="enabled"pre-post-annotations="enabled"secured-annotations="enabled"/>
</beans>
  
jsr250的使用
添加依赖
<dependency><groupId>javax.annotation</groupId><artifactId>jsr250-api</artifactId><version>1.0</version>
</dependency>
     
控制器中通过注解设置
package com.bobo.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import javax.annotation.security.RolesAllowed;@Controller
@RequestMapping("/user")
public class UserController {@RolesAllowed(value = {"ROLE_ADMIN"})@RequestMapping("/query")public String query(){System.out.println("用户查询....");return "/home.jsp";}@RolesAllowed(value = {"ROLE_USER"})@RequestMapping("/save")public String save(){System.out.println("用户添加....");return "/home.jsp";}@RequestMapping("/update")public String update(){System.out.println("用户更新....");return "/home.jsp";}
}
  

  

  
Spring表达式的使用
package com.bobo.controller;import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;import javax.annotation.security.RolesAllowed;@Controller
@RequestMapping("/order")
public class OrderController {@PreAuthorize(value = "hasAnyRole('ROLE_USER')")@RequestMapping("/query")public String query(){System.out.println("用户查询....");return "/home.jsp";}@PreAuthorize(value = "hasAnyRole('ROLE_ADMIN')")@RequestMapping("/save")public String save(){System.out.println("用户添加....");return "/home.jsp";}@RequestMapping("/update")public String update(){System.out.println("用户更新....");return "/home.jsp";}
}
  
SpringSecurity提供的注解
package com.bobo.controller;import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("/role")
public class RoleController {@Secured("ROLE_USER")@RequestMapping("/query")public String query(){System.out.println("用户查询....");return "/home.jsp";}@Secured("ROLE_ADMIN")@RequestMapping("/save")public String save(){System.out.println("用户添加....");return "/home.jsp";}@RequestMapping("/update")public String update(){System.out.println("用户更新....");return "/home.jsp";}
}
  
异常处理
新增一个错误页面,然后在SpringSecurity的配置文件中配置即可。

  

  
当然你也可以使用前面介绍的SpringMVC中的各种异常处理器处理。

  
3.2 标签使用
前面介绍的注解的权限管理可以控制用户是否具有这个操作的权限,但是当用户具有了这个权限后进入到具体的操作页面,这时我们还有进行更细粒度的控制,这时注解的方式就不太适用了,这时我们可以通过标签来处里。
 
添加SpringSecurity的标签库
<%--Created by IntelliJ IDEA.User: dpbDate: 2021/3/16Time: 17:02To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<html>
<head><title>Title</title>
</head>
<body><h1>欢迎光临...</h1><security:authentication property="principal.username" /><security:authorize access="hasAnyRole('ROLE_USER')" ><a href="#">用户查询</a><br></security:authorize><security:authorize access="hasAnyRole('ROLE_ADMIN')" ><a href="#">用户添加</a><br></security:authorize><security:authorize access="hasAnyRole('ROLE_USER')" ><a href="#">用户更新</a><br></security:authorize><security:authorize access="hasAnyRole('ROLE_ADMIN')" ><a href="#">用户删除</a><br></security:authorize>
</body>
</html>
  
页面效果

     

二、SpringSecurity高级应用

1. SpringSecurity核心源码分析
分析SpringSecurity的核心原理,那么我们从哪开始分析?以及我们要分析哪些内容?
1. 系统启动的时候SpringSecurity做了哪些事情?
2. 第一次请求执行的流程是什么?
3. SpringSecurity中的认证流程是怎么样的?
  
1.1 系统启动
当我们的Web服务启动的时候,SpringSecurity做了哪些事情?当系统启动的时候,肯定会加载我们配置的web.xml文件。
<!DOCTYPE web-app PUBLIC"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN""http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app version="2.5" id="WebApp_ID" xmlns="http://java.sun.com/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"><display-name>Archetype Created Web Application</display-name><!-- 初始化spring容器 --><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:applicationContext.xml</param-value></context-param><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- post乱码过滤器 --><filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><init-param><param-name>encoding</param-name><param-value>utf-8</param-value></init-param></filter><filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- 前端控制器 --><servlet><servlet-name>dispatcherServletb</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" --><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-mvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcherServletb</servlet-name><!-- 拦截所有请求jsp除外 --><url-pattern>/</url-pattern></servlet-mapping><!-- 配置过滤器链 springSecurityFilterChain 名称固定 --><filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern></filter-mapping></web-app>
    
web.xml中配置的信息:
1. Spring的初始化(会加载解析SpringSecurity的配置文件)。
2. SpringMVC的前端控制器初始化。
3. 加载DelegatingFilterProxy过滤器。
  
Spring的初始化操作和SpringSecurity有关系的操作是,会加载介绍SpringSecurity的配置文件,将相关的数据添加到Spring容器中。

  
SpringMVC的初始化和SpringSecurity其实是没有多大关系的。
   
DelegatingFilterProxy过滤器:拦截所有的请求。而且这个过滤器本身是和SpringSecurity没有关系的!!!在之前介绍Shiro的时候,和Spring整合的时候我们也是使用的这个过滤器。 其实就是完成从IoC容器中获取DelegatingFilterProxy这个过滤器配置的 FileterName 的对象。
  
系统启动的时候会执行DelegatingFilterProxy的init方法。
protected void initFilterBean() throws ServletException {synchronized(this.delegateMonitor) {// 如果委托对象为null 进入if (this.delegate == null) {// 如果targetBeanName==nullif (this.targetBeanName == null) {// targetBeanName = 'springSecurityFilterChain'this.targetBeanName = this.getFilterName();}// 获取Spring的容器对象WebApplicationContext wac = this.findWebApplicationContext();if (wac != null) {// 初始化代理对象this.delegate = this.initDelegate(wac);}}}
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException{// springSecurityFilterChainString targetBeanName = this.getTargetBeanName();Assert.state(targetBeanName != null, "No target bean name set");// 从IoC容器中获取 springSecurityFilterChain的类型为Filter的对象Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);if (this.isTargetFilterLifecycle()) {delegate.init(this.getFilterConfig());}return delegate;
}
     

   
init方法的作用是:从IoC容器中获取 FilterChainProxy的实例对象,并赋值给DelegatingFilterProxy的delegate属性。
   
1.2 第一次请求
客户发送请求会经过很多歌Web Filter拦截。

  
然后经过系统启动的分析,我们知道有一个我们定义的过滤器会拦截客户端的所有的请求。
  
DelegatingFilterProxy

  
当用户请求进来的时候会被doFilter方法拦截。
public void doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain) throws ServletException, IOException {Filter delegateToUse = this.delegate;if (delegateToUse == null) {// 如果 delegateToUse 为空 那么完成init中的初始化操作synchronized(this.delegateMonitor) {delegateToUse = this.delegate;if (delegateToUse == null) {WebApplicationContext wac = this.findWebApplicationContext();if (wac == null) {throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");}delegateToUse = this.initDelegate(wac);}this.delegate = delegateToUse;}}this.invokeDelegate(delegateToUse, request, response, filterChain);
}
  
invokeDelegate
protected void invokeDelegate(Filter delegate, ServletRequest request,ServletResponse response, FilterChain filterChain) throws ServletException,IOException {// delegate.doFilter() FilterChainProxydelegate.doFilter(request, response, filterChain);
}
   
所以在此处我们发现DelegatingFilterProxy最终是调用的委托代理对象的doFilter方法。

  
FilterChainProxy
过滤器链的代理对象:增强过滤器链(具体处理请求的过滤器还不是FilterChainProxy ) 根据客户端的请求匹配合适的过滤器链链来处理请求。
public class FilterChainProxy extends GenericFilterBean {private static final Log logger = LogFactory.getLog(FilterChainProxy.class);private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");// 过滤器链的集合 保存的有很多个过滤器链 一个过滤器链中包含的有多个过滤器private List<SecurityFilterChain> filterChains;private FilterChainProxy.FilterChainValidator filterChainValidator;private HttpFirewall firewall;// .....
}
  

  
// 处理用户请求
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;if (clearContext) {try {request.setAttribute(FILTER_APPLIED, Boolean.TRUE);this.doFilterInternal(request, response, chain);} finally {SecurityContextHolder.clearContext();request.removeAttribute(FILTER_APPLIED);}} else {this.doFilterInternal(request, response, chain);}
}
  
doFilterInternal
private void doFilterInternal(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);// 根据当前的请求获取对应的过滤器链List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);if (filters != null && filters.size() != 0) {FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);vfc.doFilter(fwRequest, fwResponse);} else {if (logger.isDebugEnabled()) {logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));}fwRequest.reset();chain.doFilter(fwRequest, fwResponse);}
}
   
获取到了对应处理请求的过滤器链。

  
SpringSecurity中处理请求的过滤器中具体处理请求的方法。
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {if (this.currentPosition == this.size) {if (FilterChainProxy.logger.isDebugEnabled()) {FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest)+ " reached end of additional filter chain; proceeding with original chain");}this.firewalledRequest.reset();this.originalChain.doFilter(request, response);} else {++this.currentPosition;Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);if (FilterChainProxy.logger.isDebugEnabled()) {FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest)+ " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");}nextFilter.doFilter(request, response, this);}
}
  

  
主要过滤器的介绍
https://www.processon.com/view/link/5f7b197ee0b34d0711f3e955
    
ExceptionTranslationFilter

ExceptionTranslationFilter是我们看的过滤器链中的倒数第二个,作用是捕获倒数第一个过滤器抛出来的异常信息。

  
FilterSecurityInterceptor
做权限相关的内容
public void invoke(FilterInvocation fi) throws IOException, ServletException{if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filter Applied") != null && this.observeOncePerRequest) {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} else {if (fi.getRequest() != null && this.observeOncePerRequest) {fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);}// 抛出异常 ExceptionTranslationFilter就会捕获异常InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} finally {super.finallyInvocation(token);}super.afterInvocation(token, (Object)null);}
}
  
ExceptionTranslationFilter 处理异常的代码。

  

  

    

  

  

  

  
当用第二次提交 http://localhost:8082/login时 我们要关注的是 DefaultLoginPageGeneratingFilter 这个过滤器。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest)req;HttpServletResponse response = (HttpServletResponse)res;boolean loginError = this.isErrorPage(request);boolean logoutSuccess = this.isLogoutSuccess(request);if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {// 正常的业务请求就直接放过chain.doFilter(request, response);} else {// 需要跳转到登录页面的请求String loginPageHtml = this.generateLoginPageHtml(request, loginError,logoutSuccess);// 直接响应登录页面response.setContentType("text/html;charset=UTF-8");response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);response.getWriter().write(loginPageHtml);}
}
   
generateLoginPageHtml
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {String errorMsg = "Invalid credentials";if (loginError) {HttpSession session = request.getSession(false);if (session != null) {AuthenticationException ex = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";}}StringBuilder sb = new StringBuilder();sb.append("<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <metacharset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1, shrink-to-fit=no\">\n <meta name=\"description\"
content=\"\">\n <meta name=\"author\" content=\"\">\n <title>Please signin</title>\n <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\"
integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n <link
href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\"
rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n </head>\n <body>\n <divclass=\"container\">\n");String contextPath = request.getContextPath();if (this.formLoginEnabled) {sb.append(" <form class=\"form-signin\" method=\"post\" action=\""+ contextPath + this.authenticationUrl + "\">\n <h2 class=\"form-signinheading\">Please sign in</h2>\n" + createError(loginError, errorMsg) +createLogoutSuccess(logoutSuccess) + " <p>\n <label
for=\"username\" class=\"sr-only\">Username</label>\n <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\"class=\"form-control\" placeholder=\"Username\" required autofocus>\n</p>\n <p>\n <label for=\"password\"class=\"sronly\">Password</label>\n <input type=\"password\"id=\"password\"name=\"" + this.passwordParameter + "\" class=\"form-control\"placeholder=\"Password\" required>\n </p>\n" + this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request) + "<button class=\"btn btn-lg btnprimary btn-block\" type=\"submit\">Sign in</button>\n</form>\n");}if (this.openIdEnabled) {sb.append(" <form name=\"oidf\" class=\"form-signin\"method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n<h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n" +createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "<p>\n <label for=\"username\" class=\"sr-only\">Identity</label>\n<input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter+ "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n</p>\n" + this.createRememberMe(this.openIDrememberMeParameter) +this.renderHiddenInputs(request) + " <button class=\"btn btn-lg btnprimary btn-block\"type=\"submit\">Sign in</button>\n </form>\n");}if (this.oauth2LoginEnabled) {sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");sb.append(createError(loginError, errorMsg));sb.append(createLogoutSuccess(logoutSuccess));sb.append("<table class=\"table table-striped\">\n");Iterator var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();while(var7.hasNext()) {Entry<String, String> clientAuthenticationUrlToClientName =(Entry)var7.next();sb.append(" <tr><td>");String url = (String)clientAuthenticationUrlToClientName.getKey();sb.append("<ahref=\"").append(contextPath).append(url).append("\">");String clientName = HtmlUtils.htmlEscape((String)clientAuthenticationUrlToClientName.getValue());sb.append(clientName);sb.append("</a>");sb.append("</td></tr>\n");}sb.append("</table>\n");}sb.append("</div>\n");sb.append("</body></html>");return sb.toString();
}
   
第一次请求的完整的流程

  
页面调试也可以验证我的推论。

    
1.3 认证流程
UsernamePasswordAuthenticationFilter:专门处理用户认证请求的。
   
在父类中AbstractAuthenticationProcessingFilter看doFilter的逻辑。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest)req;HttpServletResponse response = (HttpServletResponse)res;if (!this.requiresAuthentication(request, response)) {// 如果不是必须要认证的请求就直接放过chain.doFilter(request, response);} else {if (this.logger.isDebugEnabled()) {this.logger.debug("Request is to process authentication");}Authentication authResult;try {// 获取认证的信息 客户端提交的表单信息authResult = this.attemptAuthentication(request, response);if (authResult == null) {return;}this.sessionStrategy.onAuthentication(authResult, request, response);} catch (InternalAuthenticationServiceException var8) {this.logger.error("An internal error occurred while trying to authenticate the user.", var8);this.unsuccessfulAuthentication(request, response, var8);return;} catch (AuthenticationException var9) {this.unsuccessfulAuthentication(request, response, var9);return;}if (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}this.successfulAuthentication(request, response, chain, authResult);}
}
      
attemptAuthentication在UsernamePasswordAuthenticationFilter实现。
public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());} else {// 获取表单提交的账号密码String username = this.obtainUsername(request);String password = this.obtainPassword(request);if (username == null) {username = "";}if (password == null) {password = "";}// 空处理username = username.trim();// 账号密码封装为对应的对象UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);// 认证操作return this.getAuthenticationManager().authenticate(authRequest);}
}
   
authenticate

  
变量获取每个认证提供者,然后处理认证。

   
具体处理认证的实现。

  

  

  
此处就会进入到我们自定义的UserServiceImpl中。

  
到这儿就和我们讲的数据库认证连接起来了。
    
2.SpringBoot整合
2.1 整合实现
我们如何在SpringBoot项目里面使用SpringSecurity呢?
   
添加相关的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
  
启动访问

  
账号是:user 密码:控制台中有帮我们自动生成。

   

2.2 自定义登录页面
我们通过Thymeleaf来实现,先创建一个登录页面。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><h1>登录管理</h1><form th:action="@{/login}" method="post">账号:<input type="text" name="username"><br>密码:<input type="password" name="password"><br><input type="submit" value="登录"></form>
</body>
</html>
  
添加对应的控制器
@Controller
public class LoginController {@RequestMapping("/login.html")public String goLoginPage(){return "/login";}
}
    
添加SpringSecurity的配置类
package com.bobo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {/*** 先通过内存中的账号密码来处理* @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("zhang").password("{noop}123").roles("USER");}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().mvcMatchers("/login_page.html","/login.html","/css/**","/js/**").permitAll().antMatchers("/**").hasAnyRole("USER").anyRequest().authenticated().and().formLogin().loginPage("/login.html").loginProcessingUrl("/login").successForwardUrl("/home.html").and().csrf().disable();}
}
  
效果

  
2.3 数据库认证
关键配置

  
2.4 授权

     
3. SpringBoot中的源码分析
前面我们介绍了Spring中整合SpringSecurity的核心源码流程,那么中SpringBoot应该怎么看呢?

  
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, // 初始化
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,// 认证 创建默认的账号密码
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration // 和过滤器链有关
  
默认账号密码

  
DelegatingFilterProxy
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.boot.autoconfigure.security.servlet;import java.util.EnumSet;
import java.util.stream.Collectors;
import javax.servlet.DispatcherType;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import
org.springframework.boot.context.properties.EnableConfigurationProperties;
import
org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.http.SessionCreationPolicy;
import
org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;@Configuration(proxyBeanMethods = false
)
@ConditionalOnWebApplication(type = Type.SERVLET
)
@EnableConfigurationProperties({SecurityProperties.class})
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class,SessionCreationPolicy.class})
@AutoConfigureAfter({SecurityAutoConfiguration.class})
public class SecurityFilterAutoConfiguration {private static final String DEFAULT_FILTER_NAME ="springSecurityFilterChain";public SecurityFilterAutoConfiguration() {}@Bean@ConditionalOnBean(name = {"springSecurityFilterChain"})public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {// 获取DelegatingFilterProxy对象,并且设置 url-parttern=/*DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);registration.setOrder(securityProperties.getFilter().getOrder());registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));return registration;}private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) {return securityProperties.getFilter().getDispatcherTypes() == null ? null :(EnumSet)securityProperties.getFilter().getDispatcherTypes().stream().map((type)-> {return DispatcherType.valueOf(type.name());}).collect(Collectors.toCollection(() -> {return EnumSet.noneOf(DispatcherType.class);}));}
}
   

  

   
那么后面的处理又是一样的了,进入FilterChainProxy中。

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

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

相关文章

【算法每日一练]-图论(保姆级教程篇14 )#会议(模板题) #医院设置 #虫洞 #无序字母对 #旅行计划 #最优贸易

目录 今日知识点&#xff1a; 求数的重心先dfs出d[1]和cnt[i]&#xff0c;然后从1进行dp求解所有d[i] 两两点配对的建图方式&#xff0c;检查是否有环 无向图欧拉路径路径输出 topodp求以i为终点的游览城市数 建立分层图转化盈利问题成求最长路 会议&#xff08;模板题&a…

MidJourney笔记(10)-faq-fast-help-imagine-info-public-stealth

/faq 在官方 Midjourney Discord 服务器中使用可快速生成流行提示工艺频道常见问题解答的链接。 不过这个命令,我也是没有找到入口,之前还能在MidJourney的频道里使用,然后最近发现没有权限,有点奇怪。不知道系统又做了什么升级。 /fast 切换到快速模式。

Cannot resolve property ‘driverClassName‘

已解决 Cannot resolve property 错误 最近在学习spring时遇到了下面的问题&#xff1a; spring读取不到property的name属性&#xff0c;报红&#xff0c;编译不通过&#xff0c;上网查到了两种解决方案&#xff0c;如下&#xff1a; 1、重新加载spring文件就可以解决问题了&a…

TypeScript 从入门到进阶之基础篇(三) 元组类型篇

系列文章目录 TypeScript 从入门到进阶系列 TypeScript 从入门到进阶之基础篇(一) ts基础类型篇TypeScript 从入门到进阶之基础篇(二) ts进阶类型篇TypeScript 从入门到进阶之基础篇(三) 元组类型篇TypeScript 从入门到进阶之基础篇(四) symbol类型篇 持续更新中… 文章目录 …

VMware Workstation——修改虚拟机配置和设置网络

目录 一、修改配置 1、点击需要修改配置的虚拟机&#xff0c;然后点击编辑虚拟机配置 2、修改内存、CPU、硬盘配置 二、设置网络 1、从虚拟机配置中进入到网络适配器设置 2、选择网络连接模式 一、修改配置 1、点击需要修改配置的虚拟机&#xff0c;然后点击编辑虚拟机配…

Linux系统操作——tcping安装与使用

目录 .一、安装 1、安装依赖 tcptraceroute和bc 2、安装tcping 3、赋予tcping执行权限 4、测试 5、tcping返回结果说明 二、使用说明&#xff08;参数&#xff09; 一、安装 1、安装依赖 tcptraceroute和bc 【 CentOS 或 RHEL】 sudo yum install -y tcptraceroute bc…

听GPT 讲Rust源代码--compiler(25)

File: rust/compiler/rustc_target/src/spec/mod.rs 在Rust的源代码中&#xff0c;rust/compiler/rustc_target/src/spec/mod.rs文件的作用是定义和实现有关目标平台的规范。 SanitizerSet是一个结构体&#xff0c;用于表示目标平台上存在的sanitizer集合。 TargetWarnings是一…

了解统计分析中的岭回归

一、介绍 在统计建模和机器学习领域&#xff0c;回归分析是用于理解变量之间关系的基本工具。在各种类型的回归技术中&#xff0c;岭回归是一种特别有用的方法&#xff0c;尤其是在处理多重共线性和过拟合时。本文深入探讨了岭回归的概念、其数学基础、应用、优点和局限性。 在…

腾讯面试总结

腾讯 一面 mysql索引结构&#xff1f;redis持久化策略&#xff1f;zookeeper节点类型说一下&#xff1b;zookeeper选举机制&#xff1f;zookeeper主节点故障&#xff0c;如何重新选举&#xff1f;syn机制&#xff1f;线程池的核心参数&#xff1b;threadlocal的实现&#xff…

Git 实战指南:常用指令精要手册(持续更新)

&#x1f451;专栏内容&#xff1a;Git⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、Git 安装过程1、Windows 下安装2、Cent os 下安装3、Ubuntu 下安装 二、配置本地仓库1、 初始化 Git 仓库2、配置 name 和 e…

SpringBoot学习(六)-SpringBoot整合Shiro

12、Shiro 12.1概述 12.1.1简介 Apache Shiro是一个强大且易用的Java安全框架 可以完成身份验证、授权、密码和会话管理 Shiro 不仅可以用在 JavaSE 环境中&#xff0c;也可以用在 JavaEE 环境中 官网&#xff1a; http://shiro.apache.org/ 12.1.2 功能 Authentication…

DFS-体积

DFS-体积 题目&#xff1a; 给出n件物品&#xff0c;每件物品有一个体积Vi&#xff0c;求从中取若干件物品能够组成不同的体积和有多少种可能。例如&#xff0c;n3&#xff0c;Vi(1,3,4),那么输出6,6种不同的体积分别为1,3,4,5,7,8 输入格式&#xff1a; 第一行一个正整数&…

一文讲透Python数据分析可视化之直方图(柱状图)

直方图&#xff08;Histogram&#xff09;又称柱状图&#xff0c;是一种统计报告图&#xff0c;由一系列高度不等的纵向条纹或线段表示数据分布的情况。一般用横轴表示数据类型&#xff0c;纵轴表示分布情况。通过绘制直方图可以较为直观地传递有关数据的变化信息&#xff0c;使…

关于“Python”的核心知识点整理大全65

目录 20.2.19 设置 SECRET_KEY 20.2.20 将项目从 Heroku 删除 注意 20.3 小结 附录 A 安装Python A.1.1 确定已安装的版本 A.1.2 在 Linux 系统中安装 Python 3 A.2 在 OS X 系统中安装 Python A.2.1 确定已安装的版本 A.2.2 使用 Homebrew 来安装 Python 3 注意 …

ejs默认配置 原型链污染

文章目录 ejs默认配置 造成原型链污染漏洞背景漏洞分析漏洞利用 例题 [SEETF 2023]Express JavaScript Security ejs默认配置 造成原型链污染 参考文章 漏洞背景 EJS维护者对原型链污染的问题有着很好的理解&#xff0c;并使用非常安全的函数清理他们创建的每个对象 利用Re…

高校选课系统需求分析开发源码

高校学生选课报名系统包括学生、教师和管理员三方的功能需求&#xff0c;学生的需求是查询院系的课程、选课情况及个人信息修改&#xff1b;教师则需要查看和查询所有课程信息及自己的课程信息以及教师信息的修改&#xff1b;管理员则负责更为复杂的任务&#xff0c;包括对学生…

opencv007 图像运算——加减乘除

今天学习图像处理的基础——加减乘除&#xff0c;总体来说比较好理解&#xff0c;不过生成的图片千奇百怪哈哈哈哈 opencv中图像的运算本质是矩阵的运算 加法 做加法之前要求两张图片形状&#xff0c;长宽&#xff0c;通道数完全一致 cv2.add(img1, img2) add的规则是两个图…

HTML 使用 ruby 给汉字加拼音

使用 ruby 给汉字加拼音 兼容性 使用 ruby 给汉字加拼音 大家有没有遇到过要给汉字头顶上加拼音的需求? 如果有的话, 你是怎么解决的呢? 如果费尽心思, 那么你可能走了很多弯路, 因为 HTML 原生就有这样的标签来帮我们实现类似的需求. <ruby> ruby 本身是「红宝石」…

leetcode 每日一题 2023年12月30日 一周中的第几天

题目 给你一个日期&#xff0c;请你设计一个算法来判断它是对应一周中的哪一天。 输入为三个整数&#xff1a;day、month 和 year&#xff0c;分别表示日、月、年。 您返回的结果必须是这几个值中的一个 {"Sunday", "Monday", "Tuesday", &qu…

transforms图像增强(二)

一、图像变换 1、transforms.Pad transforms.Pad是一个用于对图像边缘进行填充的数据转换操作。 参数&#xff1a; padding&#xff1a;设置填充大小。可以是单个整数&#xff0c;表示在上下左右四个方向上均填充相同数量的像素&#xff1b;也可以是一个包含两个整数的元组…