Spring Security应用到源码分析
简单概述
Spring Security最最最重要的两个核心功能就是:"认证" 、"授权"
用户认证(Authentication)
按照业务场景来说,就是用户通过用户名&密码登录系统,系统对该用户的合法性进行验证
用户授权(Authorization)
用户授权是基于用户认证的,在用户认证之后,系统判断该用户是否有权限去操作某些资源
Spring Security的基本原理
-
SpringBoot遵从默认大于配置的原则,只需要开发人员引入SpringBoot与Sucurity整合的包即可实现自动化配置
-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
-
Spring Security 本质是一个过滤器链
-
我们启动一个带有Security的SpringBoot项目
-
可以从启动日志中得到以下信息
-
全是过滤器
-
Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter, org.springframework.security.web.context.SecurityContextPersistenceFilter, org.springframework.security.web.header.HeaderWriterFilter, org.springframework.security.web.csrf.CsrfFilter, org.springframework.security.web.authentication.logout.LogoutFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter, org.springframework.security.web.authentication.www.BasicAuthenticationFilter, org.springframework.security.web.savedrequest.RequestCacheAwareFilter, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter, org.springframework.security.web.authentication.AnonymousAuthenticationFilter, org.springframework.security.web.session.SessionManagementFilter, org.springframework.security.web.access.ExceptionTranslationFilter, org.springframework.security.web.access.intercept.FilterSecurityInterceptor]
-
重点看三个过滤器即可
FilterSecurityInterceptor
-
FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部
super.beforeInvocation(fi);
表示查看之前的Filter是否放行通过
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
表示真正的调用后台的服务
ExceptionTranslationFilter
-
ExceptionTranslationFilter:是一个异常过滤器,用来处理在认证过程中抛出的异常
UsernamePasswordAuthenticationFilter
-
UsernamePasswordAuthenticationFilter:对登录请求的表单做拦截,校验用户名和密码
-
这里使用的是默认的内存的中密码和默认的账户名user
-
后期我们会改造使用数据库做验证
-
可以发现,登录请求必须为post请求
过滤器链如何加载
-
过滤器链是通过 "DelegatingFilterProxy" 该类加载的,我们跟一下源码
-
这是SpringBoot自动装配的源码,如果我们使用SpringBoot,就得手写过滤器链的加载过程
targetBeanName:有一个默认的名字 "FilterChainProxy"
可以看到wac是Spring的应用上下文,从中获取 "FilterChainProxy" 的对象实例
然后执行 "FilterChainProxy"的init方法,会走 "FilterChainProxy" 的doFilter方法
可以看到无论如何都会执行该方法,我们进去其中看看
List<Filter> filters = getFilters(fwRequest);
可以跟进去,就是一个迭代器,返回一个过滤器集合
该集合中包含所有的Security过滤器
两大重要接口说明
-
在我们使用Spring Security的过程,我们自定义开发有两个非常重要的接口,我们详细来学习一下
UserDetailsService接口分析
-
我们上面的环境,什么也没有配置的情况下,认证的账户和密码都是security生成的,我们在实际项目中,这些隐私数据不可能寄托于内存噻,这时候我们就不能采用他的这种方式,而是要重写默认的认证方法
-
我们刚刚在上面说到了一个过滤器:"UsernamePasswordAuthenticationFilter" 的doFilter方法
-
对登录的POSt请求表单做拦截,校验用户名和密码.
-
-
UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter我们也去看看
-
security的认证流程大致就是
-
执行"UsernamePasswordAuthenticationFilter" 的doFilter方法
-
如果成功,则调用父类的 "successfulAuthentication"方法
-
如果失败,则调用父类的 "unsuccessfulAuthentication"方法
-
-
自定义开发的步奏为
-
创建一个类实现UserDetailsService接口,重写loadUserByUsername方法
-
连接数据库,查询用户信息,封装并返回Security提供的User对象
-
-
创建类继承UsernamePasswordAuthenticationFilter,并重写三个方法
-
attemptAuthentication():用户认证
-
successfulAuthentication():认证成功后调用
-
unsuccessfulAuthentication():认证失败后调用
-
-
PasswordEncoder接口分析
主要用于自定义开发中,查询用户数据封装User时,User属性密码的加密
-
PasswordEncoder接口一共有三个方法
-
String encode(CharSequence rawPassword);
-
表示吧参数按照默认的解析规则进行解析
-
-
boolean matches(CharSequence rawPassword, String encodedPassword);
-
第一个参数:表单提交的密码
-
第二个参数:数据库的密码
-
两个密码做匹配,返回匹配结果布尔值
-
-
default boolean upgradeEncoding(String encodedPassword) {return false;}
-
如果机械的密码能够在财经系解析且达到更安全的结果返回true
-
否则返回false,默认返回false
-
-
PasswordEncoder的接口实现类:BCryptPasswordEncoder
-
BCryptPasswordEncoder是Spring Security官方推荐使用的密码解析器
-
通过new 直接创建对象,调用父接口的方法完成密码加密与匹配
Spring Security Web权限方案
用户名和密码的自定义
默认Security的用户名为:"user",密码为项目启动时在日志中打印的密码,这在企业开发中肯定是不行的,下面我们由浅入深的来说说这个账户和密码的几种设置方式,当然最终的账户名和密码都是要落地到数据库中,只是拓宽一下大家的视野,知道有这么个东西
-
第一种方式:通过配置文件(测试接口时可用)
此时我们访问项目任一接口,都需要使用该用户信息登录后方可访问
-
第二种方式:通过配置类(测试接口时可用)
-
第三种方式:自定义编写实现类(企业开发)
-
首先改造配置文件类如下
-
-
然后编写UserDetailsService接口的实现类,重写方法
-
可见方法的返回值是:UserDetails,我们发现是个接口,查看他的所有实现类,只有User
-
查看User类的构造方法,创建User对象返回
-
认证请求相关设置
-
目前我们的登陆有点过于简陋,且不是自定义的,这肯定不行,那么就引申出一揽子的配置
我先说一下我们会做一个Demo,主要的目的是我们熟悉关于Security的一些配置
-
这个是我们的Controller
@RestController @RequestMapping("/test") public class TestController { @GetMapping("/hello") public String hello(){ return "Hello Security"; } @PostMapping("/login") public String login(){ return "Hello Login"; } @GetMapping("/test1") public String test1(){ return "good bye 欢迎下次光临"; } @GetMapping("/test2") public String test2(){ return "Test 2"; } }
-
这是我们自定义的接口实现类
-
给ninja2账户赋予A1权限 和role1角色
-
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //这里可进行数据库查询,封装User对象 //... //模拟数据库返回的数据进行User对象封装 //该用户的权限集合 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("A1,ROLE_role1"); User user = new User("ninja2", new BCryptPasswordEncoder().encode("ninja2"), auths); return user; } }
-
下面是我们Security的配置类
-
设置绑定自定义的认证接口
-
这是一些自定义的页面和接口
-
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Qualifier("userDetailsServiceImpl") @Autowired private UserDetailsService userDetailsService; @Bean PasswordEncoder initPasswordEncoder(){ return new BCryptPasswordEncoder(); } //重写这个方法,将自定义的用户名和密码以及角色set到内存中 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //密码解析器 // BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); // String password = bCryptPasswordEncoder.encode("ninja1"); //将配置文件配置的信息删了,在此手动将用户名、密码、角色全塞到内存中保存 // auth.inMemoryAuthentication().withUser("ninja1").password(password).roles("admin"); //指定用户认证业务接口和密码解析器 auth.userDetailsService(userDetailsService).passwordEncoder(initPasswordEncoder()); } //配置认证相关信息 @Override protected void configure(HttpSecurity http) throws Exception { //自定义自己编写的页面 http.formLogin() .loginPage("/login.html") //配置自定义的登陆页面 .loginProcessingUrl("/user/login") //设置登陆访问路径url .defaultSuccessUrl("/success.html").permitAll(); //登陆成功之后路径跳转 //认证请求路径相关配置 http.authorizeRequests() .antMatchers("/test/login").permitAll() //请求无责放行 //基于权限的控制 //[hasAuthority类型]:该接口只有拥有A1权限的用户才可以访问 // .antMatchers("/test/test1").hasAuthority("A1") //[hasAnyAuthority]:改接口只要拥有A1,B1中任一权限的用户可以访问 // .antMatchers("/test/test1").hasAnyAuthority("A1,B1") //基于角色的控制 //可以查查看源码,这个和权限不一样:return "hasRole('ROLE_" + role + "')"; //这儿我们设置该接口的访问角色为role1,实际上我们的用户的角色标识应该为:ROLE_role1 //[hasRole]:改接口只有拥有ROLE_role1角色的用户可以访问 // .antMatchers("/test/test1").hasRole("role1") //[hasAnyRole]:改接口只有拥有ROLE_role1、ROLE_role1中任一角色的用户可以访问 .antMatchers("/test/test1").hasAnyRole("role1,role2") .anyRequest().authenticated(); //其他请求需要认证才放行 //关闭csrf防护配置 http.csrf().disable(); //配置没有权限访问跳转自定义页面 http.exceptionHandling().accessDeniedPage("/unauth.html"); //退出 http.logout().logoutUrl("/logout") .logoutSuccessUrl("/test/test1").permitAll(); } }
-
大部分的代码,注释应该就能解释清楚,唯一需要注意的是
-
基于权限的两种控制方式的区别
-
hasAuthority
-
hasAnyAuthority
-
-
基于角色的两种控制方式的区别
-
hasRole
-
hasAnyRole
-
-
-
我们为ninja2用户配置了A1权限以及role1角色,想调试这些功能,可以更改给ninja2授予的权限和角色进行调试
-
Demo大致功能流程为:
-
访问任何接口,首页跳转到/login.html
-
输入自定义账号密码:ninja2 / ninja2 登陆
-
登陆状态下访问test2接口
-
登陆成功页,点击退出,再次访问test2接口,会跳转登录页
-
至于更多的权限校验,以及那四种模式我这里就不多说了,我最后会写一个比较完整的Demo,实现所有的功能
-
注解的使用
@Secured()
使用之前需要先开启,在启动类上使用注解开启
@EnableGlobalMethodSecurity(securedEnabled = true)
该注解用于Controller方法上,用于保护该接口只被具有某角色的用户访问
注意这里匹配的字符串需要添加前缀 "ROLE_"
@Secured({"ROLE_role1","ROLE_role2"})
@PreAuthorize()
使用之前需要先开启,在启动类上使用注解开启
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
该注解用于Controller方法上,用于方法访问前的权限验证
该注解有四个值值得注意一下,这也是之前我们的配置中有说明的部分
// @PreAuthorize("hasRole('Role_role1')") // @PreAuthorize("hasAnyRole('Role_role1','Role_role2')") // @PreAuthorize("hasAuthority('A1)") // @PreAuthorize("hasAnyAuthority('A1,A2')")
@PostAuthorize()
使用之前需要先开启,在启动类上使用注解开启
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
该注解用于Controller方法上,用于方法访问后的权限验证,使用不多
和@PreAuthorize()直接使用方法一致
@PostFilter()
只要做数据过滤,如下所示
以下代码的中的filterObject是方法返回值List中的遍历对象对象
我们使用 == 做过滤条件,最后返回为true的数据才会被留下来
当然也可以使用其他匹配符或者运算表达式,比如大于 小于 不等于等等...
我们用浏览器测试看看
@PostAuthorize("hasAnyAuthority('A1,A2')") @PostFilter("filterObject.username == 'zhangsan'") @GetMapping("/test2") public List test2() { List<Ninja> list = new ArrayList<>(); list.add(new Ninja(1, "zhangsan", "打球")); list.add(new Ninja(2, "lisi", "开摩托")); return list; }
@PreFilter()
进入控制器之前对数据进行过滤,和@PostFilter()注解使用方式一致
只会留下匹配结果为true的数据,然后进入到方法里
比如:@PreFilter(value = "filterObject.id % 2 == 0")
我们参数是一组对象集合,经过这个过滤器后
我们的集合中对象的id%2 == 0的对象才会被留下来,进入到方法里面
权限表达式
security + cookie 实现免登陆(源码分析)
第一次认证请求流程源码分析
-
首先我们根据上图查看UsernamePasswordAuthenticationFilter相关的源码
-
我要看的是它的父类 "AbstractAuthenticationProcessingFilter" 的一个"doFilter"方法
-
"doFilter"方法中对于认证结果分别都有自己的处理方法
-
unsuccessfulAuthentication(request, response, failed);
-
successfulAuthentication(request, response, chain, authResult);
-
-
successfulAuthentication就是认证通过后调用的方法,我们点进去看看
-
可以看到理由有一个操作方法是:
-
rememberMeServices.loginSuccess(request, response, authResult);
-
-
然后我们点击:"rememberMeServices",查看该Service的定义,发现如下
-
private RememberMeServices rememberMeServices = new NullRememberMeServices();
-
RememberMeServices 是一个接口
-
NullRememberMeServices是其实现,但是其实现并没有给出
-
public class NullRememberMeServices implements RememberMeServices { // ~ Methods // ======================================================================================================== public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { return null; } public void loginFail(HttpServletRequest request, HttpServletResponse response) { } public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { } }
-
-
然后我们在AbstractAuthenticationProcessingFilter中发现了对RememberMeServices的set方法
-
猜想,应该是这里替换了初始化的实现类
-
我们将关注点定位到RememberMeServices 的另一个实现:"AbstractRememberMeServices"
-
观察他的方法:"loginSuccess"
-
@Override public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!rememberMeRequested(request, parameter)) { logger.debug("Remember-me login not requested."); return; } onLoginSuccess(request, response, successfulAuthentication); }
-
-
发现调用 :"onLoginSuccess()"方法,发现该方法是该抽象类里面的一个接口,我们找其实现
-
分别是:PersistentTokenBasedRememberMeServices、TokenBasedRememberMeServices
-
我们首先查看一下:"PersistentTokenBasedRememberMeServices"的实现
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { //发现图中的tokenRepository在将Token写入数据库 tokenRepository.createNewToken(persistentToken); //然后在写入Cookie addCookie(persistentToken, request, response); } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }
-
-
然后我们查看该 "tokenRepository"的定义
-
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
-
PersistentTokenRepository是一个接口,默认使用的是基于内存的,我们看看他所有的实现
-
InMemoryTokenRepositoryImpl (默认基于内存)
-
JdbcTokenRepositoryImpl (基于数据库连接,这好像就是我们要找的玩意儿)
-
-
我们查看:"JdbcTokenRepositoryImpl ",发现很多默认的sql语句
-
/** Default SQL for creating the database table to store the tokens */ public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, " + "token varchar(64) not null, last_used timestamp not null)"; /** The default SQL used by the <tt>getTokenBySeries</tt> query */ public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?"; /** The default SQL used by <tt>createNewToken</tt> */ public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"; /** The default SQL used by <tt>updateToken</tt> */ public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?"; /** The default SQL used by <tt>removeUserTokens</tt> */ public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
-
这sql写的很明显,就是一个表的定义和增删改查,security都为我们配置好了
-
或许只需要一个开关,这些热插拔的组件应该就可以使用上
-
-
到了这里,上图中的这些步骤都完成了
-
第一步:请求认证
-
第二步:认证成功
-
第三步:向Cookie中写入Token
-
第三步:使用RemeberMeService 操作 TokenRepository写入Token到数据库
-
-
-
-
-
免登陆认证请求流程源码分析
security中有一个拦截器是专门为此而生的:"RememberMeAuthenticationFilter"
-
我们查看其"doFilter"方法,发现有这么一个代码片段
-
Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response);
-
-
我们查看:"autoLogin"的实现,也是有两个,根据上面的经验,我们直接走AbstractRememberMeServices类的实现
-
其他的一些判断我们浏览即可,我们只看核心代码
-
try { String[] cookieTokens = decodeCookie(rememberMeCookie); //检查cookie的有效性和操作tokenRepository查询数据库的Token是否一致 user = processAutoLoginCookie(cookieTokens, request, response); //该check只做判断,如果不符合会抛出异常 userDetailsChecker.check(user); logger.debug("Remember-me cookie accepted"); return createSuccessfulAuthentication(request, user);
-
操作Demo实现免登陆
-
第一步:操刀security配置类
-
注入数据源
-
容器注入PersistentTokenRepository 对象
-
配置基于数据库的免登陆认证
-
@Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 设置数据源 //自动建表,就是我们看到那写默认的sql中的建表语句 //如果不自动建表可以将那些sql拿出来,手动在数据库创建表 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } //配置认证相关信息 @Override protected void configure(HttpSecurity http) throws Exception { //配置基于数据库的免登陆认证 http.rememberMe().tokenRepository(persistentTokenRepository()) //组件热插拔 .tokenValiditySeconds(50) //设置有效期 .userDetailsService(userDetailsService); //组件热插拔 //自定义自己编写的页面 http.formLogin() .loginPage("/login.html") //配置自定义的登陆页面 .loginProcessingUrl("/user/login") //设置登陆访问路径url .defaultSuccessUrl("/success.html").permitAll(); //登陆成功之后跳转哪个路径 //认证请求路径相关配置 http.authorizeRequests() .antMatchers("/test/login").permitAll() //请求无责放行 .anyRequest().authenticated(); //其他请求需要认证才放行 //关闭csrf防护配置 http.csrf().disable(); //配置没有权限访问跳转自定义页面 http.exceptionHandling().accessDeniedPage("/unauth.html"); //退出 http.logout().logoutUrl("/logout") .logoutSuccessUrl("/test/test1").permitAll(); }
-
第二步:页面修改
页面新增一个checkbox的输入框
name 必须为remeber-me
CSRF & XSRF
CSRF利用的是网站对用户网页浏览器的信任,伪造权限认证数据,骗取服务器的放行。获取服务器资源的一种攻击手段
Spring Security自 4.0版本开始,默认情况下就开启CSRF保护
CsrfFilter过滤器就是为此而生
但是只针对 PATCH、POST、UPDATE、DELETE类型的请求
-
原理就是
在登陆的表单中新增一个隐藏的输入框
name必须为:"_csrf"
value,在登陆后的值为Security自动授予
服务器端无需任何处理,自动开启Csrf,你只要不去关闭即可
用户使用账号/密码登陆,Security会生成一个Token,并将其返回给页面,并在Sessin中记录
用户下次 访问时,那个隐藏的标签给我吧名为_csrf的标签的值带上
服务端CsrfFilter过滤器会对请求中的Token和服务端的Token做比较,判断请求是否合法