Spring security 入门学习笔记
整个框架的核心是一个过滤器,这个过滤器名字叫springSecurityFilterChain类型是FilterChainProxy核心过滤器里面是过滤器链(列表),过滤器链的每个元素都是一组URL对应一组过滤器WebSecurity用来创建FilterChainProxy过滤器,HttpSecurity用来创建过滤器链的每个元素。关注两个东西:和框架的用法就是通过对进行配置框架用法是写一个自定义
框架原理简述
整个框架的核心是一个过滤器,这个过滤器名字叫springSecurityFilterChain类型是FilterChainProxy
核心过滤器里面是过滤器链(列表),过滤器链的每个元素都是一组URL对应一组过滤器
WebSecurity用来创建FilterChainProxy过滤器,
HttpSecurity用来创建过滤器链的每个元素。
框架接口设计
关注两个东西:建造者和配置器
框架的用法就是通过配置器对建造者进行配置
框架用法是写一个自定义配置类,继承WebSecurityConfigurerAdapter,重写几个configure()方法
WebSecurityConfigurerAdapter就是Web安全配置器的适配器对象
@EnableWebSecurity`注解导入了三个类,重点关注`WebSecurityConfiguration
1、SpringSecurity主要配置详解
涉及到需要掌握和了解的相关类
WebSecurityConfigurerAdapter
WebSecurity
SpringSecurityFilterChain
HttpSecurity
FormLoginConfigurer
UsernamePasswordAuthenticationFilter
CsrfConfigurer
1.1、WebSecurityConfigurerAdapter 配置适配器类
security过滤器链由WebSecurityConfigurerAdapter类来配置加载的,可以继承重写该类相关方法,来对每个过滤器进行个性配置
相关的重写的方法有
public void configure(WebSecurity web) throws Exception {
// 设置不需要授权就可以访问的URL
web.ignoring().antMatchers("/public/dic")
.antMatchers("/static/**")
.antMatchers("/favicon.ico", "/error");
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(securityAuthenticationProvider);
}
protected void configure(HttpSecurity http) throws Exception {
}
– WebSecurity
它是用来创建 SecurityFilterChain的
WebSecurity是Spring Security的一个SecurityBuilder。它的任务是基于一组WebSecurityConfigurer构建出一个Servlet Filter,具体来讲就是构建一个Spring Security的FilterChainProxy实例。这个FilterChainProxy实现了Filter接口,也是通常我们所说的Spring Security Filter Chain,所构建的FilterChainProxy实例缺省会使用名称springSecurityFilterChain作为bean注册到容器,运行时处理web请求过程中会使用该bean进行安全控制。
每个FilterChainProxy包装了一个HttpFirewall和若干个SecurityFilterChain, 这里 每个 SecurityFilterChain要么对应于一个要忽略安全控制的URL通配符(RequestMatcher);要么对应于一个要进行安全控制的URL通配符(HttpSecurity)。
WebSecurity构建目标FilterChainProxy所使用的WebSecurityConfigurer实现类通常会继承自WebSecurityConfigurerAdapter(当然也可以完全实现接口WebSecurityConfigurer)。每个WebSecurityConfigurerAdapter可以配置若干个要忽略安全控制的URL通配符(RequestMatcher)和一个要进行安全控制的URL通配符(HttpSecurity)。
WebSecurity = 1 HttpFirewall + x HttpSecurity (securityFilterChainBuilders) + y RequestMatcher (ignoredRequests)
- 这里
1 HttpSecurity对应1个URL pattern,用于匹配一组需要进行安全配置的请求; - 这里
1 RequestMatcher对应1个URL pattern,用于匹配一组需要忽略安全控制的请求,比如静态公开资源或者其他动态公开资源;
通过WebSecurity 设置让过滤器忽略掉哪些路径 (相当于设置可匿名访问的路径), 以下是参考代码:
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/public/dic")
.antMatchers("/static/**")
.antMatchers("/favicon.ico", "/error");
}
– HttpSecurity
重写的 configure(HttpSecurity http) 的方法就是用来配置
HttpSecurity的,configure(HttpSecurity http)方法内的配置最终内容主要是Filter的创建。http.authorizeRequests()、http.formLogin()、http.httpBasic()分别创建了ExpressionUrlAuthorizationConfigurer,FormLoginConfigurer,HttpBasicConfigurer。在三个类从父级一直往上找,会发现它们都是SecurityConfigurer建造器的子类
。
HttpSecurity作为建造者会根据api把这些配置器添加到实例中。这些
配置器中大都是创建了相应的过滤器,并进行配置,最终在HttpSecurity建造SecurityFilterChain实例时放入过滤器链
开发人员可以在应用中提供多个WebSecurityConfigurerAdapter用于配置Spring Web Security,但要注意它们的优先级必须不同,这一点可以通过@Order注解来设置。
HttpSecurity 提供了以下相关配置
// URL路径授权的配置 http.authorizeRequests()// 登录表单配置 http.formLogin()// 用户退出注销页面配置 http.logout()// Basic认证模式,不同与表单模式,spring security 5.x默认的验证模式已经是表单模式,由于安全性问题,这种模式基本不会使用了 http.httpBasic();// 用于处理csrf跨站请求伪造,disable()表示不处理 http.csrf();// 处理用户的session,例如设置每个用户的最大并发会话数量,统计在线人数等 http.sessionManagement();// 异常处理配置,可配置 匿名用户访问无权限资源时异常 .authenticationEntryPoint() 和 认证过的用户访问无权限资源时异常 .accessDeniedHandler() http.exceptionHandling()
下面,我们来分析下 HttpSecurity 具体配置方法
1.2、http.authorizeRequests() 配置URL路径授权
http.authorizeRequests() 返回的是 ExpressionUrlAuthorizationConfigurer 配置类实例,所以 ExpressionUrlAuthorizationConfigurer 就是URL路径授权配置类
- 可匿名访问的URL路径,我们通常在WebSecurity中配置,所以我们这里只要配置所有URL都需要授权访问就可以。
http
.authorizeRequests()
.anyRequest().authenticated()
- 当然,你也可以在此配置让某些URL路径忽略授权,如下是配置了前匹配 /dic 和 /public 的所有URL都不需要授权,没匹配上的都需要授权
http
.authorizeRequests()
.antMatchers("/dic/**","/public/**").permitAll()
.anyRequest().authenticated()
- 上面的授权,我们并没有指定对什么角色,我们现在需要指定路径 /admin 只能是角色为 ROLE_ADMIN 的才能访问
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
.anyRequest().authenticated()
-
在实际应用中,我们对路径的授权控制大多数都是配置在数据库中,所以我们想要从数据库中读取授权的控制数据;
通过Debug我们发现我们的资源地址和权限信息保存在 FilterSecurityInterceptor 这个类的securityMetadataSource属性中;
当访问资源,访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,其中FilterInvocationSecurityMetadataSource的常用的实现类为DefaultFilterInvocationSecurityMetadataSource,这个类中有个很关键的东西就是requestMap,也就是我们提供的所得到的权限配置数据,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略,如果权限足够,则返回,权限不够则报错并调用权限不足页面
所以,我们要自定义两个类继承下面的两个类去实现授权
FilterInvocationSecurityMetadataSource
– 在get用来从数据库中拿到
AccessDecisionManager
分三部走
1、自定义类 UrlFilterInvocationSecurityMetadataSource
@Component public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { //用于实现ant风格的URL匹配 AntPathMatcher antPathMatcher = new AntPathMatcher(); // 实际中可从数据库中获取 // @Autowired // MenuMapper menuMapper; public final class Memu { private String pattern; public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } public String getRoles() { return roles; } public void setRoles(String roles) { this.roles = roles; } private String roles; } // 创建用于测试的所有权限配置列表数据,实际应用中应该从数据库中获取 public List<Memu> getAllMenus(){ List<Memu> allMenus = new ArrayList<Memu>(); Memu myMap = new Memu(); myMap.setPattern("/admin"); myMap.setRoles("role_admin"); allMenus.add(myMap); Memu myMap1 = new Memu(); myMap1.setPattern( "/user/**"); myMap1.setRoles("role_admin,role_user"); allMenus.add(myMap1); return allMenus; } //Collection<ConfigAttribute>表示当前请求URL所需的角色 @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { //获取当前请求的URL String requestUrl = ((FilterInvocation) o).getRequestUrl(); // 如果返回null,表示直接通行 // return null; //获取数据库中的资源信息,一般放在Redis缓存中 List<Memu> allMenus = this.getAllMenus(); //遍历信息,遍历过程中获取当前请求的URL所需要的角色信息并返回 for (Memu menu : allMenus){ if(antPathMatcher.match(menu.getPattern(), requestUrl)){ String roles = menu.getRoles(); String[] roleArr = menu.getRoles().split(","); return SecurityConfig.createList(roleArr); } } //如果当前请求的URL在资源表中不存在响应的模式,就假设该请求登录后即可访问,直接返回ROLE_LOGIN return SecurityConfig.createList("ROLE_LOGIN"); } //getAllConfigAttributes()方法用来返回所有定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确 //如果不需要校验,直接返回null即可 @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } //supports方法返回类对象是否支持校验 @Override public boolean supports(Class<?> aClass) { return FilterInvocation.class.isAssignableFrom(aClass); } }2、自定义类 AccessDecisionManager
@Component public class UrlAccessDecisionManager implements AccessDecisionManager { //在该方法中判断当前登录的用户是否具有当前请求URL所需要的角色信息,如果不具备,就抛出AccessDeniedException异常,否则不做任何事情 /* 参数: 1. Authentication: 包含当前登录用户的信息 2. Object: 是一个FilterInvocation对象,可以获取当前请求对象 3. Collection<ConfigAttribute>: FilterInvocationSecurityMetadataSource中的getAttributes方法的返回值,即当前请求URL所需要的角色 */ @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> auths = authentication.getAuthorities(); for (ConfigAttribute configAttribute : collection){ if("ROLE_LOGIN".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){ //如果角色是"ROLE_LOGIN",说明当前请求的URL用户登录后即可访问 //如果auth是UsernamePasswordAuthenticationToken的实例,那么说明当前用户已登录,该方法到此结束,否则进入正常判断流程 return; } for (GrantedAuthority authority : auths){ //如果当前用户具备当前请求需的角色,方法结束。 if (configAttribute.getAttribute().equals(authority.getAuthority())){ return; } } } throw new AccessDeniedException("权限不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }3、在我们定义的WebSecurityConfigurer配置类中,进行装配,代码如下
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .... .... // 装配urlFilterInvocationSecurityMetadataSource @Autowired @Qualifier("urlFilterInvocationSecurityMetadataSource") UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource; // 装配urlAccessDecisionManager @Autowired @Qualifier("urlAccessDecisionManager") AccessDecisionManager urlAccessDecisionManager; /** * 认证和授权配置 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 授权的配置 http.authorizeRequests() .antMatchers("/admin").hasRole("ROLE_ADMIN") .anyRequest() //任何请求 .authenticated() //访问任何资源都需要身份认证 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource); o.setAccessDecisionManager(urlAccessDecisionManager); return o; } }); ... ... } }
这样,我们就完成了权限验证的动态配置。
1.3、http.formLogin() 登录表单配置
http.formLogin() 返回的是 FormLoginConfigurer 配置类实例
FormLoginConfigurer 配置的是用户名密码认证过滤器 UsernamePasswordAuthenticationFilter 的相关属性
http.formLogin()
/*
需要认证时,重定向的登录页,当我们配置为/authenticate 时,会同时设置
/authenticate GET-提供登录页面
/authenticate POST-提供用户名和密码验证的URL
/authenticate?error GET - 认证失败时重定向的地址
/authenticate?logout GET - 成功退出后重定向的地址
*/
.loginPage("/static/login.html")
// 指定验证用户名和密码的URL
.loginProcessingUrl("/login")
// 执行身份验证时用于查找用户名的HTTP请求参数,默认是 username
.usernameParameter("username")
// 执行身份验证时用于查找密码的HTTP请求参数,默认是 password
.passwordParameter("password")
// 设置登录成功后,重定向的URL,不要和 successHandler 一起使用
.defaultSuccessUrl("/#/notice/index",true)
// 设置配置的登录表单URL对所有人放行
.permitAll()
// 设置认证失败时的 AuthenticationFailureHandler 类,如果不设置,默认是重定向到 loginPage()设置的地址 + ?error
.failureHandler(securityAuthenticationFailureHandler)
// 设置验证成功后的 AuthenticationSuccessHandler 类,用于登录成功后的相关处理, 默认设置的是 SavedRequestAwareAuthenticationSuccessHandler
.successHandler(userLoginSuccessHandler)
当我们没有配置 http.formLogin() 时,security 启用时默认的设置其实相当于是这样:
http.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.failureUrl("/login?error")
.successHandler(SavedRequestAwareAuthenticationSuccessHandler)
// 并且退出成功后重定向的页面时默认是 /login?logout
1.4、http.logout() 退出机制配置
http.logout() 返回的是 LogoutConfigurer 配置类实例
LogoutConfigurer 配置的是用户名密码认证过滤器 LogoutFilter 的相关属性
http.logout()
// 设置退出动作URL路径,如果CSRF设置enabled,则只接受POST请求,否则所有请求方式都接受
.logoutUrl("/logout")
// 设置退出成功后,重定向的URL路径
.logoutSuccessUrl("/aftersignout.html")
// 删除指定的cookie,参数为cookie的名称,多个cookie以逗号分隔
.deleteCookies("S_AUTH","S_IFS")
// 指定退出成功后的处理类,参数是一个LogoutSuccessHandler的子类
.logoutSuccessHandler(LogoutSuccessHandler)
// 清除当前用户认证信息
.clearAuthentication(true)
// 使当前用户SESSION失效
.invalidateHttpSession(true)
.permitAll()
注意: logoutSuccessUrl 不要与 logoutSuccessHandler 一起使用,否则logoutSuccessHandler将失效。
1.5、http.csrf() 跨站请求伪造的配置管理
Spring Security为防止CSRF(Cross-site requetst forgery跨站请求伪造)的发生,限制了除了get以外的大多数方法
启用 http.csrf() 会拦截 PATCH, POST, PUT, 或者DELETE 请求,所以我们最好使用POST替代GET传递敏感信息,防止隐私信息泄漏。
-
屏蔽
CSRF控制,即Spring Security不再限制CSRFhttp.csrf().disable(); // 关闭打开的csrf保护 -
开启
CSRF控制
springsecurity会在后端生成一个动态csrf token存入cookie,前端在后续的请求中附加该token,如果token不存在或不正确说明不是正常请求,予以屏蔽,从而达到解决CSRF问题的目的。
开启该配置时,将对所有的 PATCH, POST, PUT, 或者DELETE 请求进行拦截
// 将 cookieHttpOnly属性设为false,表示前端可以通过JS获取cookie的值,否则生成的 XSRF-TOKEN 的cookie会有个HttpOnly属性=true (js无法获取HttpOnly属性=true的cookie参数 )
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
启用CSRF机制必须要做到如下几点:
-
首先命名CSRF Cookie;
默认的cookie名称是 XSRF-TOKEN
默认作为提交参数接收的参数名是 _csrf
默认作为header参数接收的参数名是 X-XSRF-TOKEN
-
从Cookie中获取CSRF Token;
$.cookie(“XSRF-TOKEN”)
-
在PATCH, POST, PUT, 或者DELETE 访问请求API必须添加CSRF Token;
在参数中加一个 _csrf,值为$.cookie(“XSRF-TOKEN”)
或者在ajax请求中header里加入 X-XSRF-TOKEN 参数,值为$.cookie(“XSRF-TOKEN”)
-
注销系统时必须采用POST方式(必然要添加CSRF Token);
上面四件事少做一件,都会提示如下错误:
There was an unexpected error (type=Forbidden, status=403).
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.
1.6、http.sessionManagement() SESSION管理配置
-
设置session过期时跳转的地址
Spring Security可以在用户使用已经超时的sessionId进行请求时将用户引导到指定的页面。这个可以通过如下配置来实现
http.sessionManagement() .invalidSessionUrl("/session_timeout") // 控制session的存储实现,默认是用 SessionRegistryImpl去实现 .sessionRegistry(sessionRegistry);SessionRegistryImpl 实现代码,需要在WebSecurityConfigurerAdapter注入
@Autowired SessionRegistry sessionRegistry; @Bean public SessionRegistry getSessionRegistry(){ SessionRegistry sessionRegistry=new SessionRegistryImpl(); return sessionRegistry; }需要注意的是session超时的重定向页面应当是不需要认证的,否则再重定向到session超时页面时会直接转到用户登录页面。
此外如果你使用这种方式来检测session超时,当你退出了登录,然后在没有关闭浏览器的情况下又重新进行了登录,Spring Security可能会错误的报告session已经超时。这是因为即使你已经退出登录了,但当你设置session无效时,对应保存session信息的cookie并没有被清除,等下次请求时还是会使用之前的sessionId进行请求。解决办法是显示的定义用户在退出登录时删除对应的保存session信息的cookie :
http.logout().deleteCookies("S_AUTH") -
设置同一用户的最大会话数
通常情况下,在你的应用中你可能只希望同一用户在同时登录多次时只能有一个是成功登入你的系统的,通常对应的行为是后一次登录将使前一次登录失效,或者直接限制后一次登录。Spring Security的session-management为我们提供了这种限制。
// 最大允许用户的最大会话数,即同一用户账号最多能同时被几个人使用 http.sessionManagement() .maximumSessions(10)当同一用户同时存在的已经通过认证的session数量超过了maximumSessions所指定的值时,Spring Security的默认策略是将先前的设为无效。如果要限制用户再次登录可以设置maxSessionsPreventsLogin(true)
http.sessionManagement() .maximumSessions(10) .maxSessionsPreventsLogin(true) // 同一账号重复登录,使同一账号的session超过maximumSessions,导致之前登录的账号session过期,这时之前的用户再次访问时,会被重定向到这个URL, // 需要注意设置该URL为不需要进行认证。 .expiredUrl("/session_expired") -
设置无效的会话处理策略
// InvalidSessionStrategyImpl 是自定义实现接口 InvalidSessionStrategy 的类 http.sessionManagement().invalidSessionStrategy(new InvalidSessionStrategyImpl()) -
设置会话过期处理策略
// SessionInformationExpiredStrategyImpl 是自定义实现接口 SessionInformationExpiredStrategy 的类 http.sessionManagement().expiredSessionStrategy(new SessionInformationExpiredStrategyImpl());
1.7、http.exceptionHandling() 异常处理配置
在实际项目中,我们有时需要对认证成功与失败、授权成功与失败进行特定的处理,这时,我们会创建对应的处理实现类,并且将这些处理类配置到异常过滤器中
http.exceptionHandling()
// 指定已认证用户访问需要授权的路径被拒绝时的处理类
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
}
})
// 指定已认证用户访问需要授权的路径被拒绝时的重定向页面,不要和accessDeniedHandler一起使用
.accessDeniedPage("/error.html")
// 指定未认证匿名用户访问需要授权的路径时的处理类
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
}
});
2、用户认证
2.1、认证流程
身份验证接口Authentication包含三个属性:
- principal:用户身份,如果是用户/密码认证,这个属性就是UserDetails实例
- credentials:通常就是密码,在大多数情况下,在用户验证通过后就会被清除,以防密码泄露。
- details:更多详细信息
- authorities:用户权限
UsernamePasswordAuthenticationToken、RememberMeAuthenticationToken、OAuth2Authentication等都是间接实现了Authentication接口的类
- AuthenticationManager是用来实现管理认证的接口,入参是Authentication,实现类是ProviderManager
- AuthenticationProvider是为身份认证提供认证方式的接口,例如DaoAuthenticationProvider用来实现用户/密码认证,JwtAuthenticationProvider实现JWT Token认证
- 支持多种类型AuthenticationProvider会注入到ProviderManager中,ProviderManager会根据Authentication的类型调用相应类型的AuthenticationProvider
流程及代码解读
当未认证匿名用户访问需要授权的URL时,执行的流程:
首先,后端会进入 AuthenticationEntryPoint 接口实现类执行类方法 commence ,在方法中会执行重定向到登录页面
用户浏览器跳转到登录页面
用户输入用户名和密码,以 POST方式提交到 /login
/login 路径POST 匹配的过滤器是 UsernamePasswordAuthenticationFilter ,在过滤器的方法 attemptAuthentication 中执行认证流程
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 必须是POST提交的登录请求才会执行认证流程,否则报异常 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 拿到请求参数中的用户名和密码 String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 取出左右空格 // 将用户名和密码包装成一个 用户名密码身份认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property // 将 HttpServletRequest 的其他详细信息,放入用户身份认证的详细信息中,在ProviderManager.authenticate方法中当认证比对成功后,会通过copyDetails(authentication, result),取出这边放置的Details,赋值给认证通过的身份信息中 setDetails(request, authRequest); // 将用户提交的身份认证交给认证管理器去认证 return this.getAuthenticationManager().authenticate(authRequest); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); }this.getAuthenticationManager() 返回一个AuthenticationManager的子类ProviderManager,
由 ProviderManager.authenticate() 去执行认证
public Authentication authenticate(Authentication authentication) throws AuthenticationException { // authentication 就是前面AuthenticationFilter.attemptAuthentication()中传入的UsernamePasswordAuthenticationToken, // authentication.getClass()得到类名为 "UsernamePasswordAuthenticationToken" Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); // 获取所有的认证信息提供类 AuthenticationProvider ,并循环遍历 for (AuthenticationProvider provider : getProviders()) { // 判断这个认证信息提供类是否支持filter给的 Authentication , if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 把用户提交的认证信息传给provider类authenticate方法去认证,在authenticate方法中会根据用户提交的用户名从数据库去取出用户密码,然后跟用户提交的密码比较,密码一致则返回一个 Authentication 的子类 result = provider.authenticate(authentication); // 身份认证完成 if (result != null) { // 复制用户提交的身份认证中的Details给认证成功结果中的Details copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (AuthenticationException e) { lastException = e; } } /****************************认证信息匹配不正确*************************/ // ProviderManager.parent 是一个 AuthenticationManager ,这里再使用另一个AuthenticationManager来管理认证 if (result == null && parent != null) { // Allow the parent to try. try { result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // 忽略,因为如果在调用父级之前没有发生其他异常,我们将在下面抛出,并且父级可能抛出ProviderNotFound,即使子级中的提供程序已经处理了请求 } catch (AuthenticationException e) { lastException = parentException = e; } } // 身份认证完成 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // 认证完成后,从身份认证中删除凭证和机密数据 ((CredentialsContainer) result).eraseCredentials(); } // 如果是父级执行认证并成功的,成功事件会在父级中触发,所以这边就不要再重复触发了 if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // 如果是父级参与了认证并且失败,失败事件会在父级中触发,所以这边就不要再重复触发了 if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }
认证流程总结:
AbstractAuthenticationProcessingFilter 得到用户提交的身份认证,包装成Authentication的某个子类实例,交给 AuthenticationManager
AuthenticationManager 会循环遍历注册的 AuthenticationProvider , 找到 supports 对应Authentication 的 AuthenticationProvider 去实现认证。
所以我们如果要实现不同的登录方式,就去自定义实现接口 AbstractAuthenticationProcessingFilter、Authentication、AuthenticationProvider
2.2、数据库中的用户名和密码来认证
2.3、用户手机号和验证码验证
- 新建token > PhoneVerificationCodeAuthenticationToken
- 新建filter > PhoneVerificationCodeAuthenticationFilter
- 新建provider > PhoneVerificationCodeAuthenticationProvider
- 认证成功处理 > UserLoginSuccessHandler
- 认证失败处理> SecurityAuthenticationFailureHandler
2.4、授权码直接登录
- 新建token > AuthCodeAuthenticationToken
- 新建filter > AuthCodeAuthenticationFilter
- 新建provider > AuthCodeAuthenticationProvider
- 认证成功处理 > UserLoginSuccessHandler2
- 认证失败处理> SecurityAuthenticationFailureHandler
2.5、个人微信号登录
- 新建token > WeixinAuthenticationToken
- 新建filter > WeixinAuthenticationFilter
- 新建provider > WeixinAuthenticationProvider
- 认证成功处理 > UserLoginSuccessHandler2
- 认证失败处理> SecurityAuthenticationFailureHandler
2.6、企业微信登录
- 新建token > QyWeixinAuthenticationToken
- 新建filter > QyWeixinAuthenticationFilter
- 新建provider > QyWeixinAuthenticationProvider
- 认证成功处理 > UserLoginSuccessHandler2
- 认证失败处理> SecurityAuthenticationFailureHandler
3、用户授权
3.1、授权流程
1、拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子类拦截。
2、获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类
DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限
Collection 。
SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...
3、最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。
代码解读
看FilterSecurityInterceptor中的doFilter() -> invoke(fi)
public void doFilter(){
...
invoke(fi);
}
public void invoke(FilterInvocation fi){
...
InterceptorStatusToken token = super.beforeInvocation(fi); // => AbstractSecurityInterceptor.beforeInvocation()
...
}
所以,实际授权的大部分逻辑代码就在 AbstractSecurityInterceptor.beforeInvocation() 中
// 从上面的代码中,我们可以知道这个object参数就是 FilterInvocation ,这是一个过滤器调用类,从中可以得到过滤器的HttpServletRequest和 HttpServletResponse
protected InterceptorStatusToken beforeInvocation(Object object) {
...
...
// this.obtainSecurityMetadataSource()返回的就是FilterSecurityInterceptor.securityMetadataSource,
// FilterSecurityInterceptor.securityMetadataSource的类型是一个接口 FilterInvocationSecurityMetadataSource
// FilterInvocationSecurityMetadataSource.getAttributes(object)就是返回当前请求url的授权配置信息
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
....
....
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
// 这边是将身份认证信息和url的授权配置信息一起交给accessDecisionManager.decide()方法去做授权决策,如果没有权限,在decide方法中抛出AccessDeniedException 异常
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
...
...
...
}
3.2、使用数据库中的权限表来授权
我们需要定义两个类 UrlFilterInvocationSecurityMetadataSource 和 UrlAccessDecisionManager
UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
UrlAccessDecisionManager implements AccessDecisionManager
在类UrlFilterInvocationSecurityMetadataSource去实现 getAttributes方法,这个方法返回的是当前访问的Url所需要的权限集合;
在类UrlAccessDecisionManager 实现decide方法,在方法中,将当前用户拥有的权限和当前访问的Url所需要的权限进行循环比较,判断是否有权限,没有权限时,抛出异常 throw new AccessDeniedException(“权限不足!”)
4、OAuth2 实现配置
Oauth2.0 四种授权模式使用场景
简化模式不谈
1.客户端模式,用在纯后端应用的服务中。通常用于后端服务api的调用,此模式下,用户是不参与的,只有客户端参与。
2.密码模式,用在系统间高度信任的场景,因为有密码泄露的风险,不建议用在公网。通常授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式,比如app端的登录。
3.授权码模式,用在有前后端的服务中。通常用于web客户端登录。
如果是纯后端接口交互,建议使用客户端模式。
参考资料:
Spring Security OAuth2 引导文档地址:https://docs.spring.io/spring-security-oauth2-boot/docs/2.2.0.RELEASE/reference/html5/
OAuth 2开发人员指南 https://projects.spring.io/spring-security-oauth/docs/oauth2.html
@EnableOauth2Sso注解全分析: https://blog.csdn.net/qq_26934393/article/details/103144687
基础了解:
AuthorizationEndpoint 用于服务于授权请求入口,"/oauth/authorize" 这个接口访问路径就是在这个类里定义的,对应方法是authorize
TokenEndpoint 用于服务访问令牌的请求,定义了"/oauth/token" 接口访问路径
要实现OAuth 2.0资源服务器,需要以下过滤器:
将OAuth2AuthenticationProcessingFilter用于加载的身份验证给定令牌的认证访问请求。
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源服务访问的令牌解析端点。
- /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
4.1、授权服务器 AuthorizationServerConfigurerAdapter
授权服务器,主要是实现
本地测试URL:http://oss.sysoft.com:8188/oauth/authorize?client_id=plat&redirect_uri=http://plat.sysoft.com:8102/login&response_type=code&state=sVsfwD
该@EnableAuthorizationServer批注用于配置OAuth 2.0授权服务器机制以及任何@Beans实现的机制AuthorizationServerConfigurer(有一个便捷的适配器实现,其中包含空方法)。以下功能委托给由Spring创建并传递到的单独的配置器AuthorizationServerConfigurer:
ClientDetailsServiceConfigurer:定义客户端详细信息服务的配置程序。可以初始化客户端详细信息,或者您可以仅引用现有商店。AuthorizationServerSecurityConfigurer:定义令牌端点上的安全性约束。AuthorizationServerEndpointsConfigurer:定义授权和令牌端点以及令牌服务。
主要接口:
- TokenStore OAuth2令牌的持久化存储接口,默认提供的实现类有 InMemoryTokenStore、JdbcTokenStore、RedisTokenStore ,如果有特殊实现,需要自定义实现类;需要在配置中指定使用哪种存储方式;
- AuthorizationServerTokenServices 授权令牌服务接口,只包含三个方法:createAccessToken、refreshAccessToken、getAccessToken,默认实现类是DefaultTokenServices;
- AuthorizationCodeServices 颁发和存储授权码服务接口,包含两个方法:createAuthorizationCode、consumeAuthorizationCode,默认最终实现类是InMemoryAuthorizationCodeServices、JdbcAuthorizationCodeServices;根据需要来配置使用哪种实现方式;
- ClientDetailsService 提供OAuth2客户端详细信息的服务,对应实体类接口是ClientDetails,默认实现类是InMemoryClientDetailsService、JdbcClientDetailsService;
1、因为AuthorizationServerTokenServices 默认实现类就一个,框架已经帮我们默认设置了,所有我们只需要实现定义其他三个接口对应的bean ;
2、在AuthorizationServerEndpointsConfigurer配置中,将3个bean配置上;
授权服务器详细配置代码:
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 引用在WebSecurityConfig定义的bean,和WebSecurityConfig使用同一个AuthenticationManager
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
// 当前认证通过的用户信息
@Autowired
private SecurityUserService userDetailsService;
/**************引用三个bean************/
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
// 密码编码方式,需要定义个bean
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/************* 注入三个bean **************/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}
// 客户端信息配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 加载客户端配置信息
clients.withClientDetails(clientDetailsService);
}
/**
* 声明授权和token的端点以及token的服务的一些配置信息,
* 比如采用什么存储方式、token的有效期等
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore) // 设置token存储方式
.authenticationManager(authenticationManager) // 设置认证管理器
.userDetailsService(userDetailsService) // 设置用户信息
.authorizationCodeServices(authorizationCodeServices) // 设置授权码管理服务
.setClientDetailsService(clientDetailsService); // 设置客户端信息服务
//自定义grant授权页面
// endpoints.pathMapping("/oauth/confirm_access","/custom/confirm_access");
}
/**
* 声明安全约束,哪些允许访问,哪些不允许访问
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//允许表单认证
oauthServer.allowFormAuthenticationForClients();
oauthServer.passwordEncoder(passwordEncoder);
// 对于CheckEndpoint控制器[框架自带的校验]的/oauth/check端点允许所有客户端发送器请求而不会被Spring-security拦截
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
oauthServer.realm("oauth2");
}
}
AuthorizationServerSecurityConfigurer
4.1.1、获取token
/oauth/token
获取token的主要流程
加粗内容为每一步的重点,不想细看的可以只看加粗内容:
1、用户发起获取token的请求。
2、过滤器会验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作。
3、过滤器通过clientId查询生成一个Authentication对象。
4、然后会通过username和生成的Authentication对象生成一个UserDetails对象,并检查用户是否存在。
5、以上全部通过会进入地址/oauth/token,即TokenEndpoint的postAccessToken方法中。
6、postAccessToken方法中会验证Scope,然后验证是否是refreshToken请求等。
7、之后调用AbstractTokenGranter中的grant方法。
8、grant方法中调用AbstractUserDetailsAuthenticationProvider的authenticate方法,通过username和Authentication对象来检索用户是否存在。
9、然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
10、然后将OAuth2AccessToken对象包装进响应流返回。
获取token的主要流程代码分析
1、ClientCredentialsTokenEndpointFilter : 客户端访问 /oauth/token 获取令牌时,由该过滤器进行拦截,该过滤器中的方法 attemptAuthentication 执行认证授权码code和客户端ID、密钥 ,this.getAuthenticationManager().authenticate(authRequest) => ProviderManager.authenticate ;
2、ProviderManager : authenticate方法中 provider.authenticate(authentication) => AbstractUserDetailsAuthenticationProvider.authenticate;
3、AbstractUserDetailsAuthenticationProvider:authenticate 方法里的 retrieveUser(username, (UsernamePasswordAuthenticationToken) =>DaoAuthenticationProvider.retrieveUser;
4、DaoAuthenticationProvider :retrieveUser方法里 this.getUserDetailsService().loadUserByUsername(username) => ClientDetailsUserDetailsService.loadUserByUsername;
5、ClientDetailsUserDetailsService: loadUserByUsername方法执行完后接着返回执行③之后的方法 逻辑;
6、认证完成后,直接进入 /oauth/token 映射的TokenEndpoint类的postAccessToken方法
*分析总结:OAuth2 的 /oauth/token 就是一个授权码登录过程,与security的用户名密码认证流程相似,都需要filter、manager、provider、userDetails .
4.1.2、刷新token
刷新token(refresh token)的流程与获取token的流程只有⑨有所区别:
-
获取token调用的是AbstractTokenGranter中的getAccessToken方法,然后调用tokenStore中的getAccessToken方法获取token。
-
刷新token调用的是RefreshTokenGranter中的getAccessToken方法,然后使用tokenStore中的refreshAccessToken方法获取token。
4.1.3、tokenStore的特点
tokenStore通常情况为自定义实现,一般放置在缓存或者数据库中。此处可以利用自定义tokenStore来实现多种需求,如:
- 同已用户每次获取token,获取到的都是同一个token,只有token失效后才会获取新token。
- 同一用户每次获取token都生成一个完成周期的token并且保证每次生成的token都能够使用(多点登录)。
- 同一用户每次获取token都保证只有最后一个token能够使用,之前的token都设为无效(单点token)。
4.2、资源服务器 ResourceServerConfigurerAdapter
4.3、客户端
在客户端应用的 WebSecurityConfigurerAdapter 配置类中,加上下面的注解,表示启用OAuth2客户端认证
@EnableOAuth2Sso2
OAuth2ClientContextFilter
yml文件相关配置:
security:
oauth2:
sso:
login-path: /login
client:
client-id: plat
client-secret: 123456
user-authorization-uri: ${url.auth}/oauth/authorize
access-token-uri: ${url.auth}/oauth/token
# 用于将http的重定向改为https
pre-established-redirect-uri: ${url.login}
registered-redirect-uri: ${url.login}
use-current-uri: false
resource:
# token-info-uri: ${auth-server.url}/oauth/check_token
# 定义获取用户信息的地址,注意获取信息的函数必须是post,不能是get
# 所有角色权限
user-info-uri: ${url.auth}/user/oauth/sso
4.4、认证模式
- client模式,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,客户端有自己的client_id,client_secret对应于用户的username,password,而客户端也拥有自己的authorities,当采取client模式认证时,对应的权限也就是客户端自己的authorities。
- password模式,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的client_id,client_secret。此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限。
两种模式的理解便是,如果你的系统已经有了一套用户体系,每个用户也有了一定的权限,可以采用password模式;如果仅仅是接口的对接,不考虑用户,则可以使用client模式。
5、SpringSecurity执行分析
springsecurity 的认证授权流程
- web请求过来
| 接口/类 | 描述 | 主要方法 | |
|---|---|---|---|
| CharacterEncodingFilter | spring 配置编码的过滤器 | ||
| FormContentFilter | 对表单提交的method进行了扩展,增加了对"PUT", “PATCH”, "DELETE"提交方式的处理,并将其公开为Servlet请求参数(默认情况下,Servlet规范仅对HTTP POST要求此操作) | ||
| RequestContextFilter | 通常是用于配置第三方servlet,如jsf时;在Spring自己的Web支持中,DispatcherServlet的处理就足够了 | ||
| SpringSecurityFilterChain | 进入springsecurity过滤器链 | ||
| LogoutFilter | 退出过滤器 默认匹配路径规则是 Ant [pattern=‘/logout’, POST]; 默认logoutSuccessHandler是 SimpleUrlLogoutSuccessHandler 轮询一系列logouthandler。处理程序应按其所需的顺序指定。通常,调用注销处理程序tokenbasedrembermeservices和SecurityContextLogoutHandler(按顺序)。 注销后,将执行重定向到由LogoutSuccessHandler配置的或logoutSuccessUrl指定的URL,具体取决于使用的构造函数。 |
||
| UsernamePasswordAuthenticationFilter | 用户名密码认证过滤器 默认匹配路径规则是 Ant [pattern=‘/login’, POST] 默认successHandler 是 SavedRequestAwareAuthenticationSuccessHandler 默认failureHandler 是 SimpleUrlAuthenticationFailureHandler 登录表单必须向此筛选器提供两个参数:用户名和密码 要使用的默认参数名包含在静态字段SPRING_SECURITY_FORM_USERNAME_KEY和SPRING_SECURITY_FORM_PASSWORD_KEY中。还可以通过设置usernamepartment和passwordParameter属性来更改参数名。 |
||
| DefaultLoginPageGeneratingFilter | 登录页面视图过滤器 用于在用户未配置登录页的情况下通过此类来生成默认的登录页面。配置代码将在链中插入此筛选器。仅当重定向用于登录页时才起作用。 过滤路径 /login,/login?error,/login?logout |
||
| DefaultLogoutPageGeneratingFilter | 提供默认的注销页面 默认匹配路径规则是 Ant [pattern=‘/logout’, GET] |
||
| BasicAuthenticationFilter |
处理HTTP请求的BASIC授权标头,然后将结果放入中 SecurityContextHolder。负责处理任何HTTP请求标头为 Authorization的身份验证方案 Basic以及Base64编码username:password令牌的请求。例如,要使用密码“ open sesame”对用户“ Aladdin”进行身份验证,header这样设置:Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== 如果身份验证成功,则将结果Authentication对象放入中SecurityContextHolder。 如果身份验证失败并且ignoreFailure为身份验证false(默认),则将调用AuthenticationEntryPoint(除非将 ignoreFailure属性设置为true) |
||
| AuthenticationEntryPoint | 启动身份验证方案入口 :未认证匿名用户访问需要授权的路径时的处理类 | commence | |
更多推荐


所有评论(0)