一、整体架构
在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过滤器要执行认证操作。ProviderManagerAuthenticationManager接口的实现类。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();
}
}

京公网安备 11010502036488号