Spring Security 学习
Spring Security是一种基于Spring AOP和Servlet规范中的FIlter实现的安全框架
是为给予Spring应用程序提供声明式安全保护的安全性框架,它能够在Web请求级别和方法调用级别处理身份认证和授权,并且因为基于Spring所以Spring Securitychongfenliyongle依赖注入和面向切面的技术。
Spring Security从两个方面解决问题
- 它使用servlet规范中的Filter保护Web请求并限制URL级别的访问。
- Spring Security还能够使用Spring AOP保护方法的调用——借助于对象代理和适用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法
Spring Security 命名空间的引入可以简化我们的开发,它涵盖了大部分 Spring Security 常用的功能。它的设计是基于框架内大范围的依赖的,可以被划分为以下几块。
- Web/Http 安全:这是最复杂的部分。通过建立 filter 和相关的 service bean 来实现框架的认证机制。当访问受保护的 URL 时会将用户引入登录界面或者是错误提示界面。
业务对象或者方法的安全:控制方法访问权限的。 - AuthenticationManager:处理来自于框架其他部分的认证请求。
- AccessDecisionManager:为 Web 或方法的安全提供访问决策。会注册一个默认的,但是我们也可以通过普通 bean 注册的方式使用自定义的 AccessDecisionManager。
- AuthenticationProvider:AuthenticationManager 是通过它来认证用户的。
- UserDetailsService:跟 AuthenticationProvider 关系密切,用来获取用户信息的。
通过Spring Security使用Spring MVC Web应用程序集成,只是在web.xml声明 DelegatingFilterProxy 作为一个Servlet过滤器来拦截任何传入的请求。
DelegatingFilterProxy是一个特殊的Servlet Filter,他本身做的工作并不多,只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring的应用上下文中
传统配置DelegatingFilterProxy过滤器
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
编写简单的安全性配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extend WebSecurityConfigurerAdapter {
}
顾名思义@EnableWebSecurity注解将会启用Web安全功能。但是它本身并没有什么用处,Spring Security必须配置在一个实现了WebSecurityConfigurerAdapter的bean中,或者拓展WebSecurityConfigurerAdapter。
All-Security项目——Maven管理
项目分为5个Model分别为主模块,APP安全模块,浏览器安全模块,安全模块核心,安全模块的Demo
<modules>
<module>../SecurityApp</module>
<module>../SecurityBrowser</module>
<module>../SecurityCore</module>
<module>../SecurityDemo</module>
</modules>
我们可以看到在主模块的pom.xml的文件中,管理了剩余的4个Model并且将其作为自己的子Model
使用Maven的dependencyManagement管理统一版本号
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
可以看到其中的两个依赖都是从Spring 官网的项目中引下来的管理整个项目版本号的两个依赖,分别为Spring IO和Spring Cloud
在导入Spring Cloud的时候需要注意每个版本的Spring Cloud管理的Spring Boot项目的版本不同,可能会因为与别的其他依赖产生版本冲突
接着我们举其中的一个例子来看
现在我们来看Demo的Model中的pom.xml
<parent>
<artifactId>bsb-security</artifactId>
<groupId>com.bsb.security</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../Security/pom.xml</relativePath>
</parent>
其中有这些结点,这些节点的意思就是该Model作为主模块的子Model进行管理,并且引用主模块的pom中的依赖
项目的逐步搭建
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
上面这个SpringSecurity的配置类首先继承WebSecurityConfigurerAdapter并且重写参数为HttpSecurity的方法,可以看到这一整个方法都是由一系列的链式调用来重写的这个configure方法,下面我们来浅浅地解读一下这个configure方法
- 使用formLogin方法使得整个配置了SpringScurity的Rest服务开启表单登录认证
- 接着调用loginPage指定登录页,这里使用一个url来表示,并且通过一个Controller去接收这个登录认证请求
@RestController
public class BrowserSecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
/**
* 当需要身份认证时跳转到这里
* @param request
* @param response
* @return
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是 " + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
logger.info(securityProperties.getBrowser().getLoginPage());
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问服务需要身份认证,请引导用户到登录页面");
}
}
- 通过ResquestMapping映射到这个请求的url上,这个Controller的作用是,通过判断是对数据的请求还是对html的静态页面的请求,对应使用不用的登陆页
读取.yml文件中的属性
如何读取.yml这种配置文件中的属性呢,Spring为我们提供了一个解决策略
因为我在项目中使用的是.yml作为项目的配置文件,这种配置文件在我看来,有几个好处,层次比较清晰,并且结构清晰,配置使用的是K-V形式的配置,看一下我的SpringBoot项目中的.yml配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
name: root
password: xxxxxx
url: jdbc:mysql://localhost:3306/securityDemo?useSSL=false
session:
store-type: none
output:
ansi:
enabled: always
server:
port: 8060
bsb:
security:
browser:
loginPage: /demo-signIn.html
可以看到.yml这种配置文件有天然的树状结构,并且通过类似父子结点能够更好地去寻找配置的结点进行修改或者查找
现在,我们就要来为上面安全配置类通过不同的条件,分配不同的认证页面,我们来回顾一下上面的安全配置类
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
现在有两个身份认证的表单
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>signIn</title>
</head>
<body>
<h2>标准登录页面</h2>
<h2>表单登录</h2>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button> </td>
</tr>
</table>
</form>
</body>
</html>
标准登录页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo-signIn</title>
</head>
<body>
<h1>demo-signIn</h1>
</body>
</html>
demo登录页(为了简单简写一下)
现在我们希望一切对html静态页面请求的身份验证页面都展示为demo登录页,一切对数据请求的认证页面都展示为标准登录页
现在我们希望由.yml来配置不同的登录页,首先我们来封装几个类
public class BrowserProperties {
private String loginPage = "/signIn.html";
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
}
首先封装BrowserProperties类,其中只有一个成员变量就是loginPage并且添加getter/setter方法,并且为loginPage指定默认的值为/signIn.html 这个就是我们的标准登录页
@ConfigurationProperties(prefix = "bsb.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browserProperties) {
this.browser = browserProperties;
}
}
其次我们封装的这个类是SecurityProperties类,其中引用一个BrowserProperties对象,并且这个对象的名称为browser,并且在这个类上我们使用Spring的注解 @ConfigurationProperties指定它是一个Spring的配置文件的读取类,并且前缀为bsb.security看到这里大家或许能理解为什么要这么写了,当然如果只是封装这两个类,那么这个相当于工具类的配置文件读取的工作是完成不了的
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
最后一个类,这个类使用Spring支持的两个注解
- @Configuration 告诉Spring这个类是一个Java配置类,其中可能会配置一些Bean进行注入
- @EnableConfigurationProperties(SecurityProperties.class) 开启Spring的配置读取并指定配置读取类也就是我们刚才配置的SecurityProperties类
这个时候我们再回来看一下我们的Controller
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是 " + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
logger.info(securityProperties.getBrowser().getLoginPage());
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问服务需要身份认证,请引导用户到登录页面");
}
这个Controller中的一个映射请求url的方法通过判断请求的url后缀是否为.html,重定向到不同的登录页,因为我们在SecurityCoreConfig类上指定了读取配置的类并且指定其为Java配置类,所以我们可以通过使用@AutoWired方式注入进来并且成功读取配置类
@Autowired
private SecurityProperties securityProperties;
securityProperties.getBrowser().getLoginPage();
bsb:
security:
browser:
loginPage: /demo-signIn.html
可以看出来这其实就是按照yml这种树状结构一级一级进行读取,并且获取到我们在配置类中设定的loginPage并且通过Controller的判断成功重定向到不同的身份认证页
认证步骤
看到了上面的一些简单配置,我们现在来分析一下Spring Security的认证步骤
如果我们不像上面那样为SpringBoot创建的服务配置一个我们需要的安全配置类的话,就是说当Spring Boot只是存在于我们的依赖中,这个时候访问我们的服务会有什么效果呢
这个时候我们能够看到在url上Spring Security 为我们重定向到了localhost:8060/login页面,并且这个页面很丑,没错这就是Spring Security默认的认证页面,如果需要进一步地去访问我们的服务,就必须通过这一关默认的身份验证
接下来我们还可以看到,开启服务之后在idea的控制台打印了这样一句之前没有过的话
Using generated security password: 135610b7-f01a-49c9-b11f-1e987da36f0c
这句话就是告诉我们本次服务开启的时候,需要通过认证的密码是这一串密码,接下来我们试一下(默认的认证用户为user)
我们可以看到在通过了Spring Security的默认安全认证之后我们顺利地访问到了我们的服务并且成功地返回了我们的响应
如何通过自己的配置让Spring Security使用我们自己的安全配置
通过继承WebSecurityConfigurerAdapter 类重写其中的configure方法,并且在其中通过链式调用进行身份认证,经过上面模块的说明,我们可以看到使用自己的配置类进行配置之后的安全模块的启用
Spring Security的工作原理(过滤器链)
我们可以看到前面的两个过滤器
-
UsernamePasswordAuthticationFilter 表单登录
这个过滤器使用用户表单登录提交的username/password进行校验,如果提交了用户名密码,这个过滤器就会尝试着用过滤到的username/password进行校验,如果这个过滤器拦截到的请求没有携带username/password参数,那么这个过滤器就会将请求移交给下一个过滤器进行处理
BasicAuthenticationFilter 默认的basic登录
FilterSecurityInterceptor 这个拦截器作为Spring Security安全认证的最后一环守门人,他会进行最终的身份验证去判断是否能够访问Rest的服务
ExceptionTranslationFilter 用来捕获FilterSecurityInterceptor根据认证结果抛出的异常,并且做出相应处理
如上图,其中绿色的过滤器我们可以通过代码的控制来控制其是否启用,但是蓝色,橙色这种拦截去和过滤器我们没有办法进行控制,这些拦截器和过滤器会一直存在于过滤器链上进行他们的工作
我们可以通过在每个过滤器源码打断点debug来观察一次完整的安全认证是怎么被处理的
如果我们直接通过浏览器去访问Rest服务的话,这个时候会直接进入到最后的橙色FilterSecurityInterceptor 拦截器,因为在这个过程中我们没有携带任何关于username以及password的数据,所以自然前面的绿色拦截器就没有了作用
并且这个时候抛出一个异常,异常抛出之后由ExceptionTranslationFilter 过滤器,并且对这个异常进行处理,实际上就是一个重定向到Spring Security默认的认证页上进行身份认证
这个时候可以看到调试的断点到了UsernamePasswordAuthticationFilter 中,因为这个时候已经使用了默认的登录认证页,并且通过UsernamePasswordAuthticationFilter 来认证用户的登录请求
-
最终还是到了FilterSecurityInterceptor 拦截器,这个时候,这个拦截器拦截到的已经不是对认证的请求了,已经是对Rest服务的请求了,这个时候我们可以看一下FilterSecurityInterceptor 的源码
InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); }
这个时候如果身份认证通过的话,就会这个拦截器就会调用下一个链进行真正的对Rest服务的访问
自定义的用户认证逻辑实现
上面我们说到的所有的认证逻辑都是基于Spring Security的默认实现,那么我们如何通过自定义的认证逻辑实现用户的认证呢
UserDetailsService接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
我们可以看到在这个Spring官方提供的接口中只有一个方法,接收一个var1的String变量作为参数(并且作为用户名),并且可能会抛出UsernameNotFoundException异常
我们看一下UserDetails接口
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
其中包括了一些我们对平时项目的一些数据的封装,包括用户名密码,用户是否被锁住,是否解冻是否可以使用
实现UserDetails接口
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查找用户信息
logger.info(username);
String passwordEn = passwordEncoder.encode("123456");
logger.info("密码为 " + passwordEn);
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
我们可以通过使用@AutoWired注解注入一些Mybaits或者JPA的DAO对象来实现根据数据库中已有的记录实现我们自己逻辑的功能
我们可以看到上述代码的最后返回了一个User对象,这个User对象不是我们自己封装的pojo对象,而是Spring官方提供的一个User类,大概看一下
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 500L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
…………
}
其中也有很多的用户信息的封装,并且重要的是实现了UserDetails接口
我们可以看一下这个User类的其中一个构造器
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
这个构造器提供了三个参数,用户名密码以及该用户的授权,一旦返回该User实例,也就说明我们自己实现的自定义的用户认证逻辑成功,并且我们可以通过我们自己的安全配置来进行对用户授权的验证