一、整体架构

alt

​ 在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 进行统一管理的。

alt

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 中的数据清空。这一策略非常方便用户在 ControllerService 层以及任何代码中获取当前登录用户数据。

​ 统一管理更易于获取。

授权

AccessDecisionManager

  • AccessDecisionManager (访问决策管理器),用来决定此次访问是否被允许。 alt

AccessDecisionVoter

  • AccessDecisionVoter (访问决定投票器),投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票。 alt

  • AccesDecisionVoter AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter AccessDecisionManager 两者的关系类似于 AuthenticationProvider ProviderManager 的关系。

ConfigAttribute

  • ConfigAttribute用来保存授权时的角色信息 alt

  • Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符串,就是角色的名称。一般来说,角色名称都带有一个 ROLE_ 前缀,投票器 AccessDecisionVoter 所做的事情,其实就是比较用户所具各的角色和请求某个 资源所需的 ConfigAtuibute 之间的关系。

认证授权整体流程总结

alt

认证

  • 认证的时候会用到AuthenticationManager ,认证成功之后会把信息存到Authentication中,同时为了方便获取用户的信息,可以通过SecurityContextHolder去直接获取

授权

  • 当请求访问某个资源时,要先经过AccessDecisionManager讲当前用户的信息角色封装成ConfigAttribute,同时调用AccessDecisionVoter判断其角色是否能够访问所有访问的资源。

二、实现原理

官方文档

执行流程

alt

  • 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();
    }
}

alt

这就是为什么在引入 Spring Security 中没有任何配置情况下,请求会被拦截的原因!

alt

通过@ConditionalOnDefaultWebSecurity注解源码可知,默认生效条件为:

  • classpath中存在 SecurityFilterChain.class, HttpSecurity.class,这个在类路径是一定有的,也就是默认是满足的
  • 没有自定义 WebSecurityConfigurerAdapter.class, SecurityFilterChain.class

默认情况下上面两个情况是满足的,但也就是说如果我们自定义一个WebSecurityConfigurerAdapter的bean,那么这个默认情况就会被打破,我们也可以根据这个bean做一些个性化的配置

数据源的配置

alt

UserDetailService是顶层的接口,用来修改默认认证的数据源信息,数据源的配置默认InMemory这种基于内存的数据源,如果我们想修改数据源的实现,只需要自定义UserDetailService实现,改为Jdbc的实现,在返回UserDetail实现就可以了

三、自定义认证

自定义认证配置类

alt

​ 通过注解可以得到我们只要在项目路径中存在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,可以使用usernameParameterpasswordParameter更改。
  • 请求路径要为/login
  • 记得要指定放行loginPage 放行登录按钮的请求loginProcessingUrl
  • successForwardUrl("path")defaultSuccessUrl("path") 区别(不适用于前后端分离的开发
    • 可以通过设置successForwardUrl("path") 使得登录成功后**转发(forward)**到指定的path,只要认证成功就一定会转到path,地址栏不会变;
    • defaultSuccessUrl("path")认证成功后可以**重定向(redirect)**到指定path,地址栏会变;还有一个区别就是defaultSuccessUrl("path")会保存之前用户访问的被保护的请求,认证成功之后直接跳到这个请求,如果没有访问这个请求,就会直接跳转到path。两者只能设置一个

自定义登录成功处理器

用于前后端分离的情况 alt

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));
    }
}

自定义登录失败处理器

和成功类似 alt

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";
    }
}

自定义数据源

认证流程

alt

关于ProviderManager

AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider

  • AuthenticationManager 是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
  • ProviderManager AuthenticationManager接口的实现类。Spring Security 认证时默认使用就是 ProviderManager
  • AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证。

alt

ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,ProviderManager parent 可以是任意类型的 AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的角色。

ProviderManager 本身也可以有多个,多个ProviderManager 共用同一个 parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局资源,作为所有提供者的后备资源。

不同的请求路径可以使用不同的请求规则,对于认证规则一样的路径就可以用顶级父亲的认证规则 alt

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();
     }
}