跳到主要内容

06、Spring Security 实战 - 登录成功后如何获取用户认证信息?设计模式之策略模式

1. 回顾策略模式

策略模式(Strategy Pattern)是一种比较简单的模式,其定义如下:定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

①策略模式的通用类图:
 

Context封装角色:它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,
封装可能存在的变化

Strategy抽象策略角色:策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。

ConcreteStrategy具体策略角色:实现抽象策略中的操作,该类含有具体的算法。

②策略模式的通用源码:

抽象策略角色:一个普通的接口,定义一个或多个具体的算法:

public interface Strategy {
   
     
    /**
     * 策略模式的运算法则
     */
    public void doSomething();
}

具体策略:一个实现类现接口中的方法

public class ConcreteStrategy1 implements Strategy{
   
     
    @Override
    public void doSomething() {
   
     
        System.out.println("具体策略1的运算法则");
    }
}

public class ConcreteStrategy2 implements Strategy{
   
     
    @Override
    public void doSomething() {
   
     
        System.out.println("具体策略2的运算法则");
    }
}

封装角色:

public class Context {
   
     
    // 抽象策略
    private Strategy strategy;

    /**
     * 构造函数设置具体策略
     * @param strategy
     */
    public Context(Strategy strategy){
   
     
        this.strategy = strategy;
    }

    /**
     * 封装后的策略方法
     */
    public void doAnything(){
   
     
        this.strategy.doSomething();
    }
}

高层模块的调用非常简单,知道要用哪个策略,产生出它的对象,然后放到封装角色中就完成任务了:

public class Client {
   
     
    public static void main(String[] args) {
   
     
        // 声明一个具体的策略
        Strategy strategy = new ConcreteStrategy1();
        // 声明上下文对象
        Context context = new Context(strategy);
        // 执行封装后的方法
        context.doAnything();
    }
}

2. SecurityContextHolder 策略模式实现

Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到SecurityContextHolder 中。

SecurityContextHolderSpring Security 存储身份验证者详细信息的位置,使用的是策略模式实现。

1. 抽象策略角色 SecurityContextHolderStrategy

Strategy抽象策略角色:策略的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性;

public interface SecurityContextHolderStrategy {
   
     
   /**
    * 该方法用来清除存储的 SecurityContext 对象
    */
   void clearContext();

   /**
    * 该方法用来获取存储的 SecurityContext 对象。
    */
   SecurityContext getContext();

   /**
    * 该方法用来设置存储的 SecurityContext 对象。
    */
   void setContext(SecurityContext context);

   /**
    * 该方法则用来创建一个空的 SecurityContext 对象。
    */
   SecurityContext createEmptyContext();
}

2. 具体策略角色

SecurityContextHolderStrategy 接口有4个实现类,他们是具体策略角色,实现抽象策略中的具体操作和算法。

 

1. 策略类 ThreadLocalSecurityContextHolderStrategy
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
   
     

   // 使用ThreadLocal来存储SecurityContext(安全上下文)
   private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

   @Override
   public void clearContext() {
   
     
      contextHolder.remove();
   }

   @Override
   public SecurityContext getContext() {
   
     
      SecurityContext ctx = contextHolder.get();
      if (ctx == null) {
   
     
         ctx = createEmptyContext();
         contextHolder.set(ctx);
      }
      return ctx;
   }

   @Override
   public void setContext(SecurityContext context) {
   
     
      Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
      contextHolder.set(context);
   }

   @Override
   public SecurityContext createEmptyContext() {
   
     
      return new SecurityContextImpl();
   }
}

2. 策略类 InheritableThreadLocalSecurityContextHolderStrategy
final class InheritableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
   
     

   private static final ThreadLocal<SecurityContext> contextHolder = new InheritableThreadLocal<>();

   @Override
   public void clearContext() {
   
     
      contextHolder.remove();
   }

   @Override
   public SecurityContext getContext() {
   
     
      SecurityContext ctx = contextHolder.get();
      if (ctx == null) {
   
     
         ctx = createEmptyContext();
         contextHolder.set(ctx);
      }
      return ctx;
   }

   @Override
   public void setContext(SecurityContext context) {
   
     
      Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
      contextHolder.set(context);
   }

   @Override
   public SecurityContext createEmptyContext() {
   
     
      return new SecurityContextImpl();
   }

}

3. 策略类 GlobalSecurityContextHolderStrategy
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
   
     

   private static SecurityContext contextHolder;

   @Override
   public void clearContext() {
   
     
      contextHolder = null;
   }

   @Override
   public SecurityContext getContext() {
   
     
      if (contextHolder == null) {
   
     
         contextHolder = new SecurityContextImpl();
      }
      return contextHolder;
   }

   @Override
   public void setContext(SecurityContext context) {
   
     
      Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
      contextHolder = context;
   }

   @Override
   public SecurityContext createEmptyContext() {
   
     
      return new SecurityContextImpl();
   }

}

3. 封装角色 SecurityContextHolder

它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。

public class SecurityContextHolder {
   
     

    // ThreadLocalSecurityContextHolderStrategy 策略类
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

    // InheritableThreadLocalSecurityContextHolderStrategy 策略类
    public static final String MODE_INHERITABLETHREADLOCAL 
        = "MODE_INHERITABLETHREADLOCAL";

    // GlobalSecurityContextHolderStrategy 策略类
    public static final String MODE_GLOBAL = "MODE_GLOBAL";

    private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

    public static final String SYSTEM_PROPERTY = "spring.security.strategy";

    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

    // 定义抽象策略接口变量,接收具体的策略类
    private static SecurityContextHolderStrategy strategy;

    private static int initializeCount = 0;

    static {
   
     
        initialize();
    }

    private static void initialize() {
   
     
        initializeStrategy();
        initializeCount++;
    }

    private static void initializeStrategy() {
   
     
        if (MODE_PRE_INITIALIZED.equals(strategyName)) {
   
     
            Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
                         + ", setContextHolderStrategy must be called with the fully constructed strategy");
            return;
        }
        if (!StringUtils.hasText(strategyName)) {
   
     
            // 如果strategyName不为null, "", " "
            // Set default:设置默认的策略类为 ThreadLocalSecurityContextHolderStrategy
            strategyName = MODE_THREADLOCAL;
        }
        if (strategyName.equals(MODE_THREADLOCAL)) {
   
     
            strategy = new ThreadLocalSecurityContextHolderStrategy();
            return;
        }
        if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
   
     
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            return;
        }
        if (strategyName.equals(MODE_GLOBAL)) {
   
     
            strategy = new GlobalSecurityContextHolderStrategy();
            return;
        }
        // 如果以上策略都不符合,尝试加载自定义策略
        try {
   
     
            // 通过反射加载自定义策略
            Class<?> clazz = Class.forName(strategyName);
            Constructor<?> customStrategy = clazz.getConstructor();
            strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
        }
        catch (Exception ex) {
   
     
            ReflectionUtils.handleReflectionException(ex);
        }
    }

    /**
    * 从当前线程显式清除上下文值。
    */
    public static void clearContext() {
   
     
        strategy.clearContext();
    }

    /**
    * 获取当前的SecurityContext
    */
    public static SecurityContext getContext() {
   
     
        return strategy.getContext();
    }

    /**
    * 主要出于故障排除目的,此方法显示该类重新初始化其 SecurityContextHolderStrategy 的次数
    */
    public static int getInitializeCount() {
   
     
        return initializeCount;
    }

    /**
    * 将新的 SecurityContext 与当前执行线程相关联。
    */
    public static void setContext(SecurityContext context) {
   
     
        strategy.setContext(context);
    }

    /**
    * 更改首选策略。 
    * 不要为给定的 JVM 多次调用此方法,因为它会重新初始化策略并对使用旧策略的任何现有线程产生不利影响。
    */
    public static void setStrategyName(String strategyName) {
   
     
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    /**
    * Use this SecurityContextHolderStrategy
    *
    * 调用#setStrategyName(String) 或此方法,但不能同时调用。
    * 此方法不是线程安全的。 在请求进行时更改策略可能会导致竞争条件。
    *
    * SecurityContextHolder 维护对提供的SecurityContextHolderStrategy 的静态引用。 
    * 这意味着在您删除策略之前,策略及其成员不会被垃圾收集。
    *
    * 为确保垃圾回收,请记住原来的策略,像这样:
    *
    * <pre>
    *     SecurityContextHolderStrategy original 
    *				      = SecurityContextHolder.getContextHolderStrategy();
    *     SecurityContextHolder.setContextHolderStrategy(myStrategy);
    * </pre>
    *
    * 然后,当您准备好对 {@code myStrategy} 进行垃圾收集时,您可以执行以下操作:
    *
    * <pre>
    *     SecurityContextHolder.setContextHolderStrategy(original);
    * </pre>
    */
    public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
   
     
        Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
        SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
        SecurityContextHolder.strategy = strategy;
        initialize();
    }

    /**
    * 允许检索上下文策略
    */
    public static SecurityContextHolderStrategy getContextHolderStrategy() {
   
     
        return strategy;
    }

    /**
    * 将创建新的空上下文委托给配置的策略。
    */
    public static SecurityContext createEmptyContext() {
   
     
        return strategy.createEmptyContext();
    }
}

  • MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
  • MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
  • MODE GLOBAL :这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到。

4. 安全上下文信息 SecurityContext

从、SecurityContextHolder中 获取并包含当前经过身份验证的用户的Authentication。

// 定义与当前执行线程相关联的最小安全信息的接口。
public interface SecurityContext extends Serializable {
   
     
   /**
    * 获取当前经过身份验证的主体,或身份验证请求令牌。
    */
   Authentication getAuthentication();

   /**
    * 更改当前经过身份验证的主体,或删除身份验证信息。
    */
   void setAuthentication(Authentication authentication);
}

通过以上分析可知, SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security就会先从 Session 中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。

Spring Security 身份验证模型的核心是SecurityContextHolder. 它包含SecurityContext。SecurityContextHolder是 Spring Security 存储身份验证者详细信息的地方。Spring Security 不关心如何SecurityContextHolder填充。如果它包含一个值,则将其用作当前经过身份验证的用户。

 

3. 代码中获取认证之后用户数据

①login.html 页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录</title>
</head>
<body>

<h1>用户登录</h1>
<h2>
    <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div>
</h2>
<form method="post" th:action="@{/doLogin}">
    用户名: <input name="uname" type="text"> <br>
    密码: <input name="passwd" type="text"> <br>
    <input type="submit" value="登录">
</form>

</body>
</html>

②控制器

@Controller
public class LoginController {
   
     
    @RequestMapping("/login.html")
    public String login() {
   
     
        return "login";
    }
}

③WebSecurityConfigurer 配置类

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                // 放行访问登录页面的/login.html请求
                .mvcMatchers("/login.html").permitAll()
                // 放行/index请求
                .mvcMatchers("/index").permitAll()
                // 其他所有的请求都需要去认证
                .anyRequest().authenticated()
                .and()
                // 认证方式为表单认证
                .formLogin()
                    // 指定默认的登录页面
                    .loginPage("/login.html")
                    // 指定登录请求路径
                    .loginProcessingUrl("/doLogin")
                    // 指定表单用户名的 name 属性为 uname
                    .usernameParameter("uname")
                    // 指定表单密码的 name 属性为 passwd
                    .passwordParameter("passwd")
                    // 指定登录成功后的自定义处理逻辑
                    .defaultSuccessUrl("/index")
                .and()
                // 禁止csrf跨站请求保护
                .csrf().disable();
    }
}

④控制器

@Slf4j
@RestController
public class HelloController {
   
     
    @RequestMapping("/hello")
    public String hello() {
   
     
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("身份信息:{}",authentication.getPrincipal());
        log.info("权限信息:{}",authentication.getAuthorities());
        return "hello security";
    }
}

⑤application.yml 配置文件

spring:
  security:
    user:
      name: root
      password: 123
      roles: admin,super
  thymeleaf:
    cache: false
logging:
  level:
    com:
      hh: DEBUG
    org:
      springframework:
        security: DEBUG
  pattern:
    console: '%clr(%d{
    
      E HH:mm:ss.SSS}){
   
     blue} %clr(%-5p) %clr(${
    
      PID}){
   
     faint} %clr(---){
   
     faint}
              %clr([%8.15t]){
   
     cyan} %clr(%-40.40logger{
    
      0}){
   
     blue} %clr(:){
   
     red} %clr(%m){
   
     faint}%n'