Spring Security是一个强有力并且高度定制化的认证和访问控制框架,致力于为Java应用程序提供认证和授权。
特性:
1、为认证和授权提供综合性和扩展性支持。
2、免受session定位、点击劫持、跨站点请求伪装等攻击。
3、Servelt API集成。
4、与Spring MVC集成
一、Spring Security架构
1、认证
AuthenticationManager是主要的认证策略接口,该接口只包含一个方法。
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
认证管理器通过authenticate方法可做下面3处理:
(1)如果能证明输入是一个合理的用户标识,则返回Authentication对象。
(2)如果输入不是一个合理的用户标识,抛出AuthenticationExcepton。
(3)如果无法决策则返回null。
备注:AuthenticationException为运行时异常,根据需要进行处理。
AuthenticationManager常用的实现为ProviderManager,ProviderManager会委托给AuthenticationProvider实例链。
AuthenticationProvider和AuthenticataionManager相似,但提供了额外的方法允许调用者查询是否支持给定的Authentication类型。
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
2、授权(访问控制)
认证成功后就是授权,核心策略是AccessDecisionManager,框架提供了访问决策管理器3种实现,它们都会委托给AccessDecisionVoter,就像
ProviderManager委托给AuthenticationProvider。
public interface AccessDecisionVoter { boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); }
object参数代表用户想要访问的任何事物(如:web或者java类中的方法),ConfigAttributes代表访问资源所需的许可证等级,
ConfigAttribute是一个接口,只有一个getAttribute()方法,返回值为谁允许访问资源的表达式规则,例如用户角色的名字:ROLE_ADMIN或者ROLE_AUDIT,一般以ROLE_前缀开头。
二、Web Security
1、Spring security在web层是基于Filters的,下面是单个http请求的处理层级:
Client <-> Filter <-> Filter <-> Filter <-> Servlet
客户单发送请求到app,由容器来决定使用哪些filters和servelt,一个请求最多一个servlet来处理,但能有多个filters,多个filters有排序,一个filter能否决其它剩余filter,并且能修改下游filter
和servlet的请求与响应。
Spring security在过滤器链中注册为单个过滤器,具体类型为FilterChainProxy,默认被注册为@Bean来处理所有请求。下面是有过滤器链代理后单个http请求的处理层级:
Client <-> Filter <-> FilterChainProxy(到多个filters) <-> Filter <-> Servlet
备注:事实上在安全过滤器中还有隐含的一层,DelegatingFilterProxy,该代理会委托给FilterChainProxy(在容器中为一个@Bean,并且bean的名字固定为springSecurityFilterChain,该bean包含了所有
安全相关的逻辑)
2、FilterChainProxy下面可以有多个由spring security管理的过滤器链,并且会把请求分发到第一个匹配到的过滤器链,有且只有一个过滤器链来处理。
FilterChainProxy
/foo/** /bar/** /**
Filter Filter Filter
Filter Filter Filter
Filter Filter Filter
注意:/foo/**会比/**先匹配到,匹配规则:精确匹配 > 路径匹配 > 通配符匹配 > 缺省匹配
3、一般没有其它自定义的FilterChainProxy有5个或过滤器链,第一个用来忽略静态资源,像/css/**,/images/**以及错误视图/error。最后一个过滤器链匹配/**,
包含认证,授权,异常处理,会话处理,头部写入等等。
三、Java配置
1、表单登录认证
(1)配置类继承自WebSecurityConfigurerAdapter,并带@EnableWebSecurity注解。
@EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests()// 限制基于HttpServletReqeuest的请求访问 .antMatchers("/", "/home").permitAll()// /和/home路径能被任何人访问 .anyRequest().authenticated()// 其它请求需要身份认证 .anyRequest().hasRole("USER")// 其它请求必须是USER角色,该方法默认会加上ROLE_前缀 .and() .formLogin()// 支持基于表单的身份认证 .loginPage("/login")// 指定跳转到登录页的url,若不指定则会生成默认登录页面,默认为/login .loginProcessingUrl("/loginProcess")// 指定认证处理的url,表单action指定地址必须为该地址,默认为/login .defaultSuccessUrl("/success")// 认证成功后默认跳转的地址,默认为/home .failureUrl("/loginProcess?error")// 认证失败后跳转的地址,默认为/login?error .permitAll()// 给用户所有与表单登录相关的url访问授权 .and() .rememberMe()// 开启记住我的功能 .rememberMeCookieName("remember-me")// 传给浏览器的cookie名,默认为remember-me .rememberMeParameter("remember-me")// 前端复选框传入的字段名,默认为remember-me .tokenValiditySeconds(30 * 1000)// cookie有效时间 .key(UUID.randomUUID().toString())// 防止名为remember-me的token被修改的key,默认为随机数,生成随机数需要时间,最好指定固定值 .and() .logout()// 开启退出登录支持 .logoutUrl("/logout")// 触发退出登录的url,前端页面地址必须为改地址,默认为/logout .logoutSuccessUrl("/loginProcess?logout")// 退出登录跳转的地址 .deleteCookies("remember-me")// 退出登录后删除名为remember-me的cookie,默认会删除remember-me功能对应的cookie .permitAll()// 授权所有与退出登录相关的url .and() .csrf().disable();// 禁用csrf,不禁用则要在表单里加上隐藏域或csrf标签 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 创建DelegatingPasswordEncoder,该PasswordEncoder会使用BCryptPasswordEncoder对密码进行编码 PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); String encodedPwd = encoder.encode("123"); // 前端传入的密码编码后与加密后的密码比对,编码后的格式为{编码器Id}+编码后的密码 auth.inMemoryAuthentication().withUser("lyl").password(encodedPwd).roles("USER"); } /** * 不重写configure(AuthenticationManagerBuilder auth)方法可以在容器中注册UserDetailService实例 */ // @Bean // public UserDetailsService userDetailsService() { // String encodedPwd = PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123"); // UserDetails userDetails = User.withUsername("lyl").password(encodedPwd).roles("USER").build(); // return new InMemoryUserDetailsManager(userDetails); // } }
(2)Web MVC相关配置
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/success").setViewName("success"); registry.addViewController("login").setViewName("login"); } @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp("/WEB-INF/jsp/", ".jsp"); } } @Controller public class AuthController { @RequestMapping("/loginProcess") public String handelRequest(String error, String logout) { // 如果是退出登录,则跳转到退出登录后的页面 if (logout != null) { return "logout"; } // 认证、授权失败或者直接访问该地址跳回到登录页面 return "login"; } }
(3)前端jsp表单
<!-- 注意:请求方式必须为post,且action地址必须为loginProcessingUrl配置的地址 --> <form action="loginProcess" method="post"> <!-- 如果没禁用csrf则要加上隐藏域,用于传csrf token给后台--> <!-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> --> <table> <tr> <td>用户名:</td> <td><input name="username" type="text" /></td> </tr> <tr> <td>密码:</td> <td><input name="password" type="password" /></td> </tr> <tr> <!-- 上传字段名必须和后端Java配置一致,默认为remember-me,用户名和密码也必须与后端配置保持一致,默认为username和password--> <td>记住我<input name="remember-me" type="checkbox" checked="checked" /></td> </tr> <tr> <td><input type="submit" value="登录"></td> </tr> </table> </form>
2、JDBC Authentication(JDBC认证)
前面的用户名、密码等认证信息是保存在内存中的,实际上用户名、密码都保存在数据库中,Spring Security中提供了JDBC认证,指定数据源和编码器即可,步骤如下:
(1)重写WebSecurityConfigurerAdapter中的configure方法
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 创建DelegatingPasswordEncoder,该PasswordEncoder会使用BCryptPasswordEncoder对密码进行编码 PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); // 默认数据库模式为HSQL的,因此不要加上withDefaultSchema() // 保存在数据库中的密码应该是通过相应密码编码器进行加密的,这里需指定编码器 auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(encoder); }
(2)在数据库中创建表,Spring Security默认会从下面表中读取用户身份以及权限信息。
mysql数据库表结构:
create table users( username varchar(50) not null primary key, password varchar(100) not null, enabled boolean not null ); create table authorities ( username varchar(50) not null, authority varchar(50) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority);
oracle数据库表结构:
create table users ( username varchar2(128) primary key, password varchar2(128) not null, enabled char(1) check (enabled in ('y','n') ) not null ); create table authorities ( username varchar2(128) not null, authority varchar2(128) not null ); alter table authorities add constraint authorities_unique unique(username, authority); alter table authorities add constraint authorities_fk1 foreign key(username) references users(username) enable;
users表数据如下所示:
authorities表数据如下所示:
注意:
(1)users表中的密码必须与配置AuthenticationManager时指定的PasswordEncoder编码后的密码一致。
(2)authorities表中的authority列的值必须以"ROLE_"前缀开头。
3、密码编码器
DelegatingPasswordEncoder(委托密码编码器)
Spring Security5.0以前默认的密码编码器是NoOpPasswordEncoder(要求明文密码),现在默认的密码编码器是BCryptPasswordEncoder。
密码存储格式为:{encoderId}encodedPassword
encoderId为编码器的标识符,用来查找应该使用哪个密码编码器,encoderId必须在编码后的密码之前,并且以"{"开头,以"}"结尾。
下面是不同编码器经过DelegatingPasswordEncoder委托后对原密码"password"进行编码后的密码:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
下面是Spring Security提供的一些PasswordEncoder的实例,与上一 一对应。
BCryptPasswordEncoder NoOpPasswordEncoder Pbkdf2PasswordEncoder SCryptPasswordEncoder StandardPasswordEncoder