【spring】SpringSecurity学习记录
一、整体架构
在SS中认证和授权是两个独立的模块,独立能比较方便的整合一些外部扩展。任何的权限管理都是先认证、再授权。
认证
AuthenticationManager
在Spring Security
中认证是由AuthenticationManager
接口来负责的
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
- 返回
Authentication
表示认证成功 - 抛出
AuthenticationException
异常,表示认证失败。
AuthenticationManager
主要实现类为 ProviderManager
,在 ProviderManager
中管理了众多 AuthenticationProvider
实例。在一次完整的认证流程中,Spring Security
允许存在多个 AuthenticationProvider
,用来实现多种认证方式(比如短信验证、表单认证等等),这些 AuthenticationProvider
都是由 ProviderManager
进行统一管理的。
Authentication
认证以及认证成功之后的信息主要是由 Authentication
的实现类进行保存的
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取用户权限信息
Object getCredentials();//获取用户凭证信息,一般指密码
Object getDetails();//获取用户详细信息
Object getPrincipal();//获取用户身份信息,用户名、用户对象等
boolean isAuthenticated();//用户是否认证成功
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
也就是说认证成功之后如果想后去用户的信息,就可以通过这个类去get到。
SecurityContextHolder
真正去保存用户信息的是Authentication
这个类,SecurityContextHolder
相当于一种获取其的一种途径。Spring Security
会将登录用户数据保存在 Session
中。但是,为了使用方便,Spring Security
在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security
会将登录成功的用户信息保存到 SecurityContextHolder
中。SecurityContextHolder
中的数据保存默认是通过ThreadLocal
来实现的,使用 ThreadLocal
创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security
会将 SecurityContextHolder
中的数据拿出来保存到 Session
中,同时将 SecurityContexHolder
中的数据清空。
以后每当有请求到来时,Spring Security
就会先从 Session
中取出用户登录数据,保存到 SecurityContextHolder
中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder
中的数据拿出来保存到 Session
中,然后将 Security SecurityContextHolder
中的数据清空。这一策略非常方便用户在 Controller
、Service
层以及任何代码中获取当前登录用户数据。
统一管理更易于获取。
授权
AccessDecisionManager
- AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。
AccessDecisionVoter
-
AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。
-
AccesDecisionVoter
和AccessDecisionManager
都有众多的实现类,在AccessDecisionManager
中会换个遍历AccessDecisionVoter
,进而决定是否允许用户访问,因而AaccesDecisionVoter
和AccessDecisionManager
两者的关系类似于AuthenticationProvider
和ProviderManager
的关系。
ConfigAttribute
-
ConfigAttribute
,用来保存授权时的角色信息 -
在
Spring Security
中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在ConfigAttribute
中只有一个getAttribute
方法,该方法返回一个String
字符串,就是角色的名称。一般来说,角色名称都带有一个ROLE_
前缀,投票器AccessDecisionVoter
所做的事情,其实就是比较用户所具各的角色和请求某个 资源所需的ConfigAtuibute
之间的关系。
认证授权整体流程总结
认证
- 认证的时候会用到
AuthenticationManager
,认证成功之后会把信息存到Authentication
中,同时为了方便获取用户的信息,可以通过SecurityContextHolder
去直接获取
授权
- 当请求访问某个资源时,要先经过
AccessDecisionManager
讲当前用户的信息角色封装成ConfigAttribute
,同时调用AccessDecisionVoter
判断其角色是否能够访问所有访问的资源。
二、实现原理
执行流程
SpringSecurity
中不是用原生Web的Filter- 既然如此就需要代理类
DelegatingFilterProxy
注册Filter到Spring中 FilterChainProxy
作为最顶层的代理托管整个Filter链(支持多个Filter组合拦截)DelegatingFilterProxy
会根据请求路径划分不同的FilterChain
,也就是说我们可以灵活的去配置任何路径过滤器。
也就是说SpringSecurity
大体上是靠各种filter
去实现的
过滤器种类(先后顺序从上至下)
过滤器 | 过滤器作用 | 默认是否加载 |
---|---|---|
ChannelProcessingFilter | 过滤请求协议 HTTP 、HTTPS | NO |
WebAsyncManagerIntegrationFilter |
将 WebAsyncManger 与 SpringSecurity 上下文进行集成 | YES |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到 SecurityContextHolder 中 | YES |
HeaderWriterFilter |
处理头信息加入响应中 | YES |
CorsFilter | 处理跨域问题 | NO |
CsrfFilter |
处理 CSRF 攻击 | YES |
LogoutFilter |
处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter | 处理 OAuth2 认证重定向 | NO |
Saml2WebSsoAuthenticationRequestFilter | 处理 SAML 认证 | NO |
X509AuthenticationFilter | 处理 X509 认证 | NO |
AbstractPreAuthenticatedProcessingFilter | 处理预认证问题 | NO |
CasAuthenticationFilter | 处理 CAS 单点登录 | NO |
OAuth2LoginAuthenticationFilter | 处理 OAuth2 认证 | NO |
Saml2WebSsoAuthenticationFilter | 处理 SAML 认证 | NO |
UsernamePasswordAuthenticationFilter |
处理表单登录 | YES |
OpenIDAuthenticationFilter | 处理 OpenID 认证 | NO |
DefaultLoginPageGeneratingFilter |
配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter |
配置默认注销页面 | YES |
ConcurrentSessionFilter | 处理 Session 有效期 | NO |
DigestAuthenticationFilter | 处理 HTTP 摘要认证 | NO |
BearerTokenAuthenticationFilter | 处理 OAuth2 认证的 Access Token | NO |
BasicAuthenticationFilter |
处理 HttpBasic 登录 | YES |
RequestCacheAwareFilter |
处理请求缓存 | YES |
SecurityContextHolderAwareRequestFilter |
包装原始请求 | YES |
JaasApiIntegrationFilter | 处理 JAAS 认证 | NO |
RememberMeAuthenticationFilter | 处理 RememberMe 登录 | NO |
AnonymousAuthenticationFilter |
配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter | 处理OAuth2认证中授权码 | NO |
SessionManagementFilter |
处理 session 并发问题 | YES |
ExceptionTranslationFilter |
处理认证/授权中的异常 | YES |
FilterSecurityInterceptor |
处理授权相关 | YES |
SwitchUserFilter | 处理账户切换 | NO |
Spring Security
提供了 30 多个过滤器。默认情况下Spring Boot
在对 Spring Security
进入自动化配置时,会创建一个名为 SpringSecurityFilerChain
的过滤器,并注入到 Spring
容器中,这个过滤器将负责所有的安全管理,包括用户认证、授权、重定向到登录页面等。
配置
加载默认配置的类SpringBootWebSecurityConfiguration
,这个类是 spring boot
自动配置类,通过这个源码得知,默认情况下对所有请求进行权限控制:
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)//运行条件为容器是servlet时
class SpringBootWebSecurityConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
//要求所有请求都要认证
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
return http.build();
}
}
这就是为什么在引入 Spring Security 中没有任何配置情况下,请求会被拦截的原因!
通过@ConditionalOnDefaultWebSecurity
注解源码可知,默认生效条件为:
classpath
中存在SecurityFilterChain.class
,HttpSecurity.class
,这个在类路径是一定有的,也就是默认是满足的- 没有自定义
WebSecurityConfigurerAdapter.class
,SecurityFilterChain.class
,
默认情况下上面两个情况是满足的,但也就是说如果我们自定义一个WebSecurityConfigurerAdapter
的bean,那么这个默认情况就会被打破,我们也可以根据这个bean做一些个性化的配置。
数据源的配置
UserDetailService
是顶层的接口,用来修改默认认证的数据源信息,数据源的配置默认InMemory
这种基于内存的数据源,如果我们想修改数据源的实现,只需要自定义UserDetailService
实现,改为Jdbc
的实现,在返回UserDetail
实现就可以了
三、自定义认证
自定义认证配置类
通过注解可以得到我们只要在项目路径中存在WebSecurityConfigerAdpter
这个类的实例,就不会用默认的实现,我们在其中继承WebSecurityConfigerAdpter
重写相关方法就可以实现自定义的认证。
需要在配置类中配置的内容: 1.登录接口的url,参数名字。 2.自定义的认证方法 3.登录成功处理器 4.登录失败处理器 5.退出接口的url,请求方法。 6.退出成功处理器 7.匿名用户访问受限url处理器 8.拦截规则
自定义例子
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
//antMatchers()参数可以有多个url且均为ant风格,denyAll()表示拒绝所有请求.
.antMatchers("/hello").denyAll()
//任何请求都要认证,放行资源写在前面,anyRequest()必须在最后.
.anyRequest().authenticated()
//返回HttpSecurity,链式调用
.and()
//让前面需要认证的资源采用表单认证
.formLogin()
//以下三项设置均是可选的
//确认登录的url
.loginProcessingUrl("/login")
//自定义的usernameParameter
.usernameParameter("username")
//自定义的passwordParameter
.passwordParameter("password")
//认证成功后的处理器,前后端分离的处理,相关介绍在下面。
.successHandler(authenticationSuccessHandler)
//认证成功后的处理器,前后端分离的处理,相关介绍在下面。
.failureHandler(authenticationFailureHandler)
.and()
.logout()
//以下这项设置是可选的
//自定义的退出url以及请求方式
.logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),new AntPathRequestMatcher("/logout2","POST")))
//成功后的处理器,相关介绍在下面。
.logoutSuccessHandler(logoutSuccessHandler)
.and()
//禁用csrf
.csrf().disable();
自定义登录界面
- 表单要为
post
请求 - 前端账号的name 默认为
username
&& 密码的name要为password
,可以使用usernameParameter
、passwordParameter
更改。 - 请求路径要为
/login
- 记得要指定放行
loginPage
放行登录按钮的请求loginProcessingUrl
successForwardUrl("path")
和defaultSuccessUrl("path")
区别(不适用于前后端分离的开发)- 可以通过设置
successForwardUrl("path")
使得登录成功后**转发(forward)**到指定的path,只要认证成功就一定会转到path,地址栏不会变; defaultSuccessUrl("path")
认证成功后可以**重定向(redirect)**到指定path,地址栏会变;还有一个区别就是defaultSuccessUrl("path")
会保存之前用户访问的被保护的请求,认证成功之后直接跳到这个请求,如果没有访问这个请求,就会直接跳转到path。两者只能设置一个。
- 可以通过设置
自定义登录成功处理器
用于前后端分离的情况
Forword..
对应之前的successForwardUrl
,Save..
对应之前的defaultSuccessUrl
.
我们只要实现这个AuthenticationSuccessHandler
这个接口去重写方法就可自定义返回json
例子:
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler{
//自定义登录成功hi后的处理
//登录成功时回调这个方法
@Override
public void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,
Authentication authentication/*认证相关信息(用户数据)*/) throws IOException, ServletException{
HashMap<String,Object> res=new HashMap<>();
res.put("msg","登录成功");
res.put("user",authentication);//authentication存放着用户信息,详见整体架构
res.put("status","200");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(new Gson().toJson(res));
}
}
自定义登录失败处理器
和成功类似
handler例子
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler{
//登录失败时回调这个方法
@Override
public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,
AuthenticationException exception/*异常信息*/) throws IOException, ServletException{
HashMap<String,Object> res=new HashMap<>();
res.put("msg","登录失败");
res.put("exception",exception);
res.put("status","400");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(new Gson().toJson(res));
}
}
注销登录
默认以Get方式访问/logout
地址即可注销登录
前后端分离注销登录handler例子
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler{
//退出成功时回调这个方法
@Override
public void onLogoutSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication/*认证相关信息(用户数据)*/) throws IOException, ServletException{
HashMap<String,Object> res=new HashMap<>();
res.put("msg","退出成功");
res.put("exitingUser",authentication);
res.put("status","200");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(new Gson().toJson(res));
}
登录用户数据获取(认证成功之后
前文已经讲到认证成功之后会将信息放在SecurityContextHolder
中
Spring Security
会将登录用户数据保存在 Session
中。但是,为了使用方便,Spring Security
在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security
会将登录成功的用户信息保存到 SecurityContextHolder
中。
SecurityContextHolder
中的数据保存默认是通过ThreadLocal
来实现的,使用 ThreadLocal
创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security
会将 SecurityContextHolder
中的数据拿出来保存到 Session
中,同时将 SecurityContexHolder
中的数据清空。以后每当有请求到来时,Spring Security
就会先从Session
中取出用户登录数据,保存到SecurityContextHolder
中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder
中的数据拿出来保存到Session
中,然后将SecurityContextHolder
中的数据清空。
实际上 SecurityContextHolder 中存储是 SecurityContext,在 SecurityContext 中存储是 Authentication。
在Controller中获取认证之后的用户数据
通过策略模式来取决于用单线程的还是可以多线程的
单线程模式:MODE_THREADLOCAL(数据只能在本线程中获取,默认实现)
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
//authentication.getPrincipal() 可以强转成User对象,然后user.getUserame() user.get...
System.out.println("身份信息: "+ authentication.getPrincipal());
System.out.println("权限信息: "+ authentication.getAuthorities());
return "hello security";
}
}
父子线程模式:MODE_INHERITABLETHREADLOCAL(数据可以在本线程和后代线程中获取)
在启动参数 VM options
加
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
使之采用父子线程这一策略,子线程会将父线程的数据拷贝一份。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
new Thread(()->{
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
System.out.println("身份信息: "+ authentication.getPrincipal());
System.out.println("权限信息: "+ authentication.getAuthorities());
}).start();
return "hello security";
}
}
自定义数据源
认证流程
关于ProviderManager
AuthenticationManager
是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager
以及 AuthenticationProvider
。
AuthenticationManager
是一个认证管理器,它定义了Spring Security
过滤器要执行认证操作。ProviderManager
AuthenticationManager
接口的实现类。Spring Security
认证时默认使用就是ProviderManager
。AuthenticationProvider
就是针对不同的身份类型执行的具体的身份认证。
ProviderManager
本身也可以再配置一个 AuthenticationManager
作为 parent,
这样当ProviderManager
认证失败之后,就可以进入到 parent
中再次进行认证。理论上来说,ProviderManager
的 parent
可以是任意类型的 AuthenticationManager
,但是通常都是由ProviderManager
来扮演 parent
的角色。
ProviderManager
本身也可以有多个,多个ProviderManager
共用同一个 parent
。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**
),每个组可以有自己的专用 AuthenticationManager
。通常,每个组都是一个ProviderManager
,它们共享一个父级。然后,父级是一种 全局
资源,作为所有提供者的后备资源。
不同的请求路径可以使用不同的请求规则,对于认证规则一样的路径就可以用顶级父亲的认证规则
AuthenticationProvider
是由 DaoAuthenticationProvider
类来实现认证的,在DaoAuthenticationProvider
认证时又通过 UserDetailsService
完成数据源的校验。
UserDetailService
默认实现使用内存实现,如果想要自定义,我们只需要自定义 UserDetailsService
实现,重写其中的loadUserByUsername
方法,最终返回 UserDetails
即可。
自定义数据库作为数据源
表设计
-- 用户表
CREATE TABLE `user`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`accountNonExpired` tinyint(1) DEFAULT NULL,
`accountNonLocked` tinyint(1) DEFAULT NULL,
`credentialsNonExpired` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`name_zh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 用户角色关系表
CREATE TABLE `user_role`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
KEY `rid` (`rid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
自定义UserDetailService
实例
@Component
public class MyUserDetailService implements UserDetailsService {
private final UserDao userDao;
@Autowired
public MyUserDetailService(UserDao userDao) {
this.userDao = userDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.loadUserByUsername(username);
if(ObjectUtils.isEmpty(user))throw new RuntimeException("用户不存在");
user.setRoles(userDao.getRolesByUid(user.getId()));
return user;
}
}
配置 authenticationManager
使用自定义UserDetailService
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Autowired
public WebSecurityConfigurer(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//web security..
}
}
四、密码加密
通过源码分析源码可得,比较密码是通过PasswordEncoder完成的,不同的PasswordEncoder
的实现就可以实现不同方式加密。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder BcryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}