Spring Security使用浅析
Spring Security使用浅析
常见类说明
认证过程中常见的类
类 | 说明 |
---|---|
Authentication |
登录用户的角色信息及权限信息等,还包含IP 、SESSION 等信息,不同Authentication 实现不尽相同。 |
AuthenticationProvider |
认证的提供者,真正起到真正作用的类。 |
AuthenticationManager |
认证处理器,内置一组AuthenticationProvider 用于认证。当有一个Provider 判定认证成功时,即判定认证成功。 |
AuthenticationEventPublisher |
用于发布认证成功/失败事件 |
SecurityFilterChain |
Spring security 自带的过滤器 |
鉴权过程中常见的类
类 | 说明 |
---|---|
ConfigAttribute |
访问规则,包含需要访问权限的信息 |
SecurityMetadataSource |
存储并返回用户权限信息 |
FilterSecurityInterceptor |
鉴权过滤器,一般位于springsercurity过滤器链中最后一个 |
AccessDecisionManager |
决策管理器,用于保存一组投票器 默认为 AffirmativeBased 类型 一票赞成ConsensusBased 少数服从多数UnanimousBased 一票否决 |
总体说明
什么是认证
认证就是认为前端传过来的username
就是本人。
通过认证后:
- 如果当前已经有角色信息(
Authentication
),可直接将其角色信息用于授权判断,或在应用业务代码中进行使用。 - 如果当前没有角色信息(
Authentication
),仅根据username
就可以直接去查询角色信息,并可直接将其角色信息用于授权判断,或在应用业务代码中进行使用。
什么是授权
授权是已认证的用户中的权限(GrantedAuthority
)能否访问某个资源。
两种授权方式:
web授权
,通过指定url
指定权限方法授权
,基于aop
,通过在spring
代理类方法上加入如@PreFilter
的注解来增加权限
重点流程说明
认证初始化阶段三大核心类
AuthenticationManagerBuilder
用于:- 会生产AuthenticationManager,
UsernamePasswordAuthenticationFilter
就是将用于授权逻辑委派给AuthenticationManager
进行处理 - 创建全局
AuthenticationManager
,用于子处理器无法处理认证(Authentication
)请求时进行处理。 - 保存认证所需全部的
AuthenticationProvider
。AuthenticationProvider
用于AuthenticationManager
(认证处理器)进行认证。 - 初始化
defaultUserDetailsService
,即默认如何根据用户名查询角色权限信息。 - 初始化
AuthenticationEventPublisher
- 会生产AuthenticationManager,
WebSecurity用于:
- 生产
FilterChainProxy
,springsecurity
封装的一组**SecurityFilterChain
**的代理,用来控制FilterChain
的执行逻辑 ignoredRequests
,不进行访问控制(认证、授权)的请求securityFilterChainBuilders
FilterSecurityInterceptor
授权HttpFirewall
拒绝策略,在进入过滤器链前先判断RequestRejectedHandler
,请求拒绝处理器,过滤器报异常时调用,无默认DefaultWebSecurityExpressionHandler
- 生产
HttpSecurity用于:
- 生产
SecurityFilterChain
requestMatcher
请求匹配,用于判断该请求是否走了过滤器OrderedFilter
,经过排序的WebSecurity中securityFilterChainBuilders建造的过滤器AuthenticationManager
,内置一组AuthenticationProvider用于认证。
- 生产
FilterChainProxy与SecurityFilterChain与FilterSecurityInterceptor
**FilterChainProxy
中组合了DefaultSecurityFilterChain
**。
FilterChainProxy
相当于一组过滤器的代理。
我们通过@Bean
注入一个SecurityFilterChain
对象相当于将自定义的SecurityFilterChain
注入到FilterChainProxy
持有的一组过滤器的代理中。
FilterSecurityInterceptor也是一个Filter
SecurityFilterChain创建
FilterOrderRegistration
构造方法将springSecurity
自定义过滤器及顺序注册进来- 通过HttpSecurityConfiguration创建
HttpSecurity
名为httpSecurity
的spring bean
- 创建
HttpSecurity
的过程中调用performBuild()
中http.
方法将springSecurity
自定义过滤器放入HttpSecurity
的filters
中 HttpSecurity
调用performBuild
创建DefaultSecurityFilterChain
,并将HttpSecurity
的filters
中所有的springSecurity
自定义过滤器放入DefaultSecurityFilterChain
的filters
中
FilterChainProxy。
初始化
@EnableWebSecurity
注解导入WebSecurityConfiguration.class
,HttpSecurityConfiguration.class
WebSecurityConfiguration
中,setter
方法setFilterChainProxySecurityConfigurer
过程中创建WebSecurity
的实例WebSecurityConfiguration
中,创建了名为springSecurityFilterChain
的FilterChainProxy
的bean
,该bean
是由WebSecurity
调用build
方法创建的- build的过程中会创建
FilterChainProxy
对象,并将全量的securityFilterChains
组合到对象中 DelegatingFilterProxy
会代理名为springSecurityFilterChain
的FilterChainProxy
的bean
认证
- 调用
DelegatingFilterProxy
的doFilter
会调用FilterChainProxy
中的doFilter
- 调用
FilterChainProxy
中的doFilterInternal
创建VirtualFilterChain
,将FilterChain
(系统自带过滤器)和SecurityFilterChain
(spring Security
自带过滤器链和自定义过滤器链)组合,依次调用 SecurityFilterChain
中有一个关键的Filter
即UsernamePasswordAuthenticationFilter
,控制登录行为
- 调用
FilterSecurityInterceptor授权
FilterSecurityInterceptor
- 内置成员变量
AccessDecisionManager
,有以下实现AffirmativeBased
ConsensusBased
UnanimousBased
FilterSecurityInterceptor
调用AccessDecisionManager.decide(authenticated, object, attributes)
后- 获取全部参与投票的
AccessDecisionVoter
,进行判断
- 内置成员变量
MethodSecurityInterceptor
核心流程代码
实现自定义功能的UsernamePasswordAuthenticationFilter
写一个子类
MyFilter
继承UsernamePasswordAuthenticationFilter
,重写attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
方法,增加想要的功能,调用父类attemptAuthentication
方法public class MyFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(request.getContentType())) { return super.attemptAuthentication(request, response); } Map map = null; try { map = new ObjectMapper().readValue(request.getInputStream(), Map.class); } catch (IOException e) { throw new BusinessException(ResponseCode.FAIL); } String username = (String) map.get(getUsernameParameter()); String password = (String) map.get(getPasswordParameter()); UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
注册将
MyFilter
@Bean public LoginFilter loginFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) { LoginFilter loginFilter = new LoginFilter(); loginFilter.setFilterProcessesUrl("/login"); loginFilter.setUsernameParameter("username"); loginFilter.setPasswordParameter("password"); loginFilter.setAuthenticationManager(authenticationManager); loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(authentication.getPrincipal()))); }); loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.LOGIN))); }); return loginFilter; }
替换
SecurityFilterChain
中UsernamePasswordAuthenticationFilter
@Bean public SecurityFilterChain filterChain(HttpSecurity http, MyFilter myFilter) throws Exception { http.addFilterAt(myFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }
用户登录信息存储方式
一旦认证成功后将Authentication ``authResult
保存在了SecurityContextHolder
中。
将Authentication
中authenticated
属性设置为true ,安全拦截器都会对他放行
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
}
认证的核心代码
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
this.getAuthenticationManager().authenticate(authRequest)
中调用UserDetailsService
中loadUserByUsername(String username)
查询用户的密码及权限信息,并封装成UserDetails
对象,在调用additionalAuthenticationChecks(UserDetails userDetails)
方法中, 调用DaoAuthenticationProvider
中UsernamePasswordAuthenticationToken authentication)
进行密码校验,并将AbstractAuthenticationToken
中authenticated
(已登录状态)设置为true
密码自动升级配置方式及说明
通过注入
UserDetailsPasswordService
实现类通过前缀名识别密码,通过前缀名修改加密方式,再进行加密保存
@Bean public UserDetailsPasswordService userDetailsPasswordService() { return new UserDetailsPasswordService() { @Override public UserDetails updatePassword(UserDetails user, String newPassword) { userService.updateUserPassword(user.getUsername(), newPassword); return user; } }; }
由于检测到加密方式改变会自动改变密码,当
springsecurity
升级时,修改了默认加密方式也不用修改代码
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
PasswordEncoderFactories
中,会将BCryptPasswordEncoder
作为默认的Encoder
因此没有必要手动注入
PasswordEncoder
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
RememberMe配置方式及说明
服务端session
30分钟就过期了,此时客户端携带cookie
请求服务端受保护资源需重新登录
rememberMe
开启会将认证信息放到cookie
中,如果session
过期,服务端会解密cookie
,而不用重新登录。
LoginFilter
中增加/** * @author sakura */ public class LoginFilter extends UsernamePasswordAuthenticationFilter { @Autowired private RememberMeServices rememberMeServices; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //其他代码略 if (rememberMeServices instanceof PersistentTokenBasedRememberMeServices) { String rememberMeParameterName = ((PersistentTokenBasedRememberMeServices) rememberMeServices).getParameter(); String rememberMe = (String) map.get(rememberMeParameterName); request.setAttribute(rememberMeParameterName, rememberMe); } //其他代码略 } }
SecurityConfig
中增加/** * 持久化series * @param dataSource * @return */ @Bean public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) { return new JdbcTokenRepositoryImpl() { { setDataSource(dataSource); // 第一次创建表才需要 // setCreateTableOnStartup(true); } }; } /** * 记住我,重写了判断【前端传递记住我参数】是否开启。 * @param userDetailsService * @param persistentTokenRepository * @return */ @Bean public RememberMeServices rememberMeServices(UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) { return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService, persistentTokenRepository) { protected boolean rememberMeRequested(HttpServletRequest request, String parameter) { Object paramValue = request.getAttribute(parameter); if (paramValue != null && paramValue instanceof String) { String attribute = (String) paramValue; if (attribute.equalsIgnoreCase("true") || attribute.equalsIgnoreCase("on") || attribute.equalsIgnoreCase("yes") || attribute.equals("1")) { return true; } } return false; } }; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public LoginFilter loginFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) { LoginFilter loginFilter = new LoginFilter(); loginFilter.setFilterProcessesUrl("/login"); loginFilter.setUsernameParameter("username"); loginFilter.setPasswordParameter("password"); //第一次登录,勾选记住我则触发PersistentTokenRepository持久化 loginFilter.setAuthenticationManager(authenticationManager); loginFilter.setRememberMeServices(rememberMeServices); loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(authentication.getPrincipal()))); }); loginFilter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.LOGIN))); }); return loginFilter; } @Bean public SecurityFilterChain filterChain(HttpSecurity http, LoginFilter loginFilter, RememberMeServices rememberMeServices) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and().formLogin() .and().exceptionHandling().authenticationEntryPoint((request, response, exception) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.UN_LOGIN))); }) .and().logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>())); }) .and().rememberMe().rememberMeServices(rememberMeServices) .and().csrf().disable(); http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }
@Bean public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) { return new JdbcTokenRepositoryImpl() { { setDataSource(dataSource); // 第一次创建表才需要 // setCreateTableOnStartup(true); } }; }
Remember-me token
中存在用:
分隔的两段,第一段为series
,第二段为token
。spring secrity
会根据series
去persistent_logins
表中查询token
,如果表中token
和Remember-me token
中第二段token
一致校验通过。Remember-me token
的过期时间为两周,根据去persistent_logins
表中所存储的日期判断token
是否过期根据去
persistent_logins
表中所存储的用户名,通过loadUserByUsername(String username)
查询出用户信息及权限
Csrf配置方式及说明
当携带header
中携带XSRF-TOKEN
参数时,springsecurity
不会返回一个新的csrf token
。
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = (csrfToken == null);
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
Oauth2 授权码模式说明
- 应用客户端
- 资源服务器
- 授权服务器
- 用户在应用客户端(浏览器、移动设备或者服务器)上,请求资源服务器受保护资源,由于未携带资源token令牌无法登陆,故点击使用第三方登录
- 应用客户端通过用户代理(浏览器)向授权服务器发送获取授权码的请求,携带客户端client id,授权方式,重定向url。
- 授权服务器通过用户代理(浏览器)展示出是否授权应用客户端某些权限。如:读、写、可读可写。
- 用户代理(浏览器)点击提交后,向授权服务器提交权限
- 如果拒绝,则拒绝授权
- 如果允许,通过(1)中重定向url,url中携带授权码(此授权码是与登录用户关联的)返回给客户端
- 客户端拿到授权码后,携带客户端client id、客户端密钥、重定向url(与请求授权码时设置的重定向URL一样)、授权码向授权服务器发送请求
- 授权服务器返回客户端查询资源token令牌
- 客户端携带资源token令牌访问资源服务器
- 授权服务器使用资源token令牌在资源服务器中进行操作
没搞懂ObjectPostProcessor是干嘛的
http.apply(new UrlAuthorizationConfigurer(http.getSharedObject(ApplicationContext.class)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(mySecurityMetadataSource);
AccessDecisionManager accessDecisionManager = object.getAccessDecisionManager();
if (accessDecisionManager instanceof AbstractAccessDecisionManager) {
((AbstractAccessDecisionManager) accessDecisionManager).getDecisionVoters().add(new MyDecisionVoter());
}
object.setRejectPublicInvocations(true);
return object;
}
}));