本文共 13397 字,大约阅读时间需要 44 分钟。
Spring Security是一种基于Spring AOP和Servlet规范中的FIlter实现的安全框架
是为给予Spring应用程序提供声明式安全保护的安全性框架,它能够在Web请求级别和方法调用级别处理身份认证和授权,并且因为基于Spring所以Spring Securitychongfenliyongle依赖注入和面向切面的技术。Spring Security 命名空间的引入可以简化我们的开发,它涵盖了大部分 Spring Security 常用的功能。它的设计是基于框架内大范围的依赖的,可以被划分为以下几块。
通过Spring Security使用Spring MVC Web应用程序集成,只是在web.xml声明 DelegatingFilterProxy 作为一个Servlet过滤器来拦截任何传入的请求。
DelegatingFilterProxy是一个特殊的Servlet Filter,他本身做的工作并不多,只是将工作委托给一个javax.servlet.Filter实现类,这个实现类作为一个<bean>注册在Spring的应用上下文中
传统配置DelegatingFilterProxy过滤器
springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy
@Configuration@EnableWebSecuritypublic class SecurityConfig extend WebSecurityConfigurerAdapter {}
顾名思义@EnableWebSecurity注解将会启用Web安全功能。但是它本身并没有什么用处,Spring Security必须配置在一个实现了WebSecurityConfigurerAdapter的bean中,或者拓展WebSecurityConfigurerAdapter。
项目分为5个Model分别为主模块,APP安全模块,浏览器安全模块,安全模块核心,安全模块的Demo
../SecurityApp ../SecurityBrowser ../SecurityCore ../SecurityDemo
我们可以看到在主模块的pom.xml的文件中,管理了剩余的4个Model并且将其作为自己的子Model
io.spring.platform platform-bom Cairo-SR2 pom import org.springframework.cloud spring-cloud-dependencies Finchley.RELEASE pom import
可以看到其中的两个依赖都是从Spring 官网的项目中引下来的管理整个项目版本号的两个依赖,分别为Spring IO和Spring Cloud
在导入Spring Cloud的时候需要注意每个版本的Spring Cloud管理的Spring Boot项目的版本不同,可能会因为与别的其他依赖产生版本冲突
接着我们举其中的一个例子来看
现在我们来看Demo的Model中的pom.xmlbsb-security com.bsb.security 1.0-SNAPSHOT ../Security/pom.xml
其中有这些结点,这些节点的意思就是该Model作为主模块的子Model进行管理,并且引用主模块的pom中的依赖
@Configurationpublic 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方法
@RestControllerpublic class BrowserSecurityController {private Logger logger = LoggerFactory.getLogger(getClass());private RequestCache requestCache = new HttpSessionRequestCache();private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@Autowiredprivate 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("访问服务需要身份认证,请引导用户到登录页面");}
}
如何读取.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=falsesession: store-type: noneoutput: ansi: enabled: alwaysserver: port: 8060bsb: 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();
现在有两个身份认证的表单
标准登录页signIn 标准登录页面
表单登录
现在我们希望一切对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支持的两个注解
这个时候我们再回来看一下我们的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方式注入进来并且成功读取配置类
@Autowiredprivate 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的默认安全认证之后我们顺利地访问到了我们的服务并且成功地返回了我们的响应
通过继承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的默认实现,那么我们如何通过自定义的认证逻辑实现用户的认证呢
//// 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 getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled();}
其中包括了一些我们对平时项目的一些数据的封装,包括用户名密码,用户是否被锁住,是否解冻是否可以使用
实现UserDetails接口
@Componentpublic 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 Setauthorities; 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 authorities) { this(username, password, true, true, true, true, authorities);}
这个构造器提供了三个参数,用户名密码以及该用户的授权,一旦返回该User实例,也就说明我们自己实现的自定义的用户认证逻辑成功,并且我们可以通过我们自己的安全配置来进行对用户授权的验证
转载地址:http://gleoa.baihongyu.com/