详解状态模式以及Spring状态机

详解状态模式以及Spring状态机

前言

讲设计模式之前我们先来了解下设计模式的SOLID原则:

  • S(Single Responsibility Principle):单一职责原则,接口职责应该单一,不要承担过多的职责。
  • O(Open Closed Principle):开闭原则,即对扩展开放,对修改关闭。简单来说就是代码的设计要达到:当别人要修改扩展功能的时候,最好能不要修改我们原有代码,而是新增代码来实现空能的扩展。这也是我们设计时要达到的目标。
  • L(Liskov Substitution Principle):里氏替换原则,子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
  • I(Interface Segregation Principle):接口隔离原则,类之间的依赖关系应该建立在最小的接口上,它不需要的接口不应该被依赖。
  • D(Dependence Inversion Principle):依赖倒置原则,应该依赖一个抽象的服务接口,而不是去依赖一个具体的服务,从依赖具体实现转向到依赖抽象接口,倒置过来。

1.什么是状态模式,它能解决什么问题?

状态模式是行为模式的一种,它是指:当一个对象在状态改变时允许改变其行为,也就是说它可以通过改变对象内部的状态来帮助对象控制自己的行为。

阿里巴巴《Java开发手册》中提到:
超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式来实现。
Java开发手册

状态模式和策略模式很相似,它也能解决多层 if-else 嵌套的问题。但是它们的意图不太一样,策略模式会控制对象使用什么策略,而状态模式会自动改变状态。下面我用一个案例来介绍下状态模式。

2.状态模式实战

如果我们要开发oa系统里面的一个请假模块,功能很简单:员工可以提交请假单,领导来审批该请假单据是通过还是不通过。我们应该怎么开发这么功能呢?
首先我们创建一个请假单的实体类:

public class LeaveBill {

    /**
     * 请假单id
     */
    private long id;

    /**
     * 请假原因
     */
    private String reason;

    /**
     * 单据状态
     */
    private int state;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public LeaveBill(long id, String reason, int state) {
        this.id = id;
        this.reason = reason;
        this.state = state;
    }
}

下面来看请假单处理类:

    /**
     * 假装这是个数据库
     */
    Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
        put(2L, new LeaveBill(2L, "家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 等待领导审批
     */
    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */
    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批不通过
     */
    final int approvalFailed = 3;

    /**
     * 审批通过
     *
     * @param id 请假单id
     */
    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitLeaderApproval) {
            doPass(leaveBill);
        } else if (state == approvalSucceeded) {
            // 已经审批过不做任何操作
            System.out.println("我什么都没做");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过,不能再次审批");
        }
    }

    /**
     * 审批不通过
     *
     * @param id 请假单id
     */
    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitLeaderApproval) {
            doFailed(leaveBill);
        } else if (state == approvalSucceeded) {
            System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
        } else if (state == approvalFailed) {
            // 如果已经失败了,就不做操作
            System.out.println("我什么都没做");
        }
    }

    /**
     * 审批通过,不加任何条件判断
     *
     * @param leaveBill 请假单
     */
    public void doPass(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(approvalSucceeded);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    /**
     * 审批不通过,不加任何状态判断
     *
     * @param leaveBill 请假单
     */
    public void doFailed(LeaveBill leaveBill) {
        // 将状态设置为审批不通过
        leaveBill.setState(approvalFailed);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人审批未通过
        System.out.println("亲爱的xxx,你的请假单未审批通过,未通过原因为:xxx");
    }
}

为了看起来简单,这里我用一个 map 来作为数据库。

简单测试:

@Test
    public void test() {
        // 1.2 审批通过
        pass(1L);
        System.out.println("=========================================");
        // 2.2 审批未通过
        failed(1L);
    }

输出结果:

亲爱的xxx,你的请假单已审批通过
=========================================
已经审批通过的单据还想再给我审批不通过,门也没有!

比如说这个时候添加了一个需求,需要一个保存草稿的功能,就是请假单我可以先保存个草稿,下次再提交。那请假单是不是要多个待提交的状态呢?那就又要添加一堆 if-else 逻辑了。

更改后的代码如下:

public class LeaveService2 {
    /**
     * 假装这是个数据库
     */
    Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
        put(2L, new LeaveBill(2L, "家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 添加 待提交 状态
     */
    final int waitSubmit = 0;

    /**
     * 等待领导审批
     */
    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */
    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批未通过
     */
    final int approvalFailed = 3;

    /**
     * 新增提交方法
     */
    public void submit(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            doSubmit(leaveBill);
        } else if (state == waitLeaderApproval) {
            System.out.println("已提交审批的单据不能再次提交");
        } else if (state == approvalSucceeded) {
            System.out.println("已审批通过的单据不能再次提交");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过的单据不能再次提交");
        }
    }

    /**
     * 审批通过
     *
     * @param id 请假单id
     */
    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            System.out.println("未提交的单据不能审批");
        } else if (state == waitLeaderApproval) {
            doPass(leaveBill);
        } else if (state == approvalSucceeded) {
            // 已经审批过不做任何操作
            System.out.println("我什么都没做");
        } else if (state == approvalFailed) {
            System.out.println("已审批未通过,不能再次审批");
        }
    }

    /**
     * 审批不通过
     *
     * @param id 请假单id
     */
    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(id);
        int state = leaveBill.getState();
        if (state == waitSubmit) {
            System.out.println("未提交的单据不能审批");
        } else if (state == waitLeaderApproval) {
            doFailed(leaveBill);
        } else if (state == approvalSucceeded) {
            System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
        } else if (state == approvalFailed) {
            // 如果已经失败了,就不做操作
            System.out.println("我什么都没做");
        }
    }

    /**
     * 提交审批,不加任何条件判断
     *
     * @param leaveBill 请假单
     */
    public void doSubmit(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(waitLeaderApproval);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已提交成功");
    }
    
    /**
     * 审批通过,不加任何条件判断
     *
     * @param leaveBill 请假单
     */
    public void doPass(LeaveBill leaveBill) {
        // 将状态设置为审批通过
        leaveBill.setState(approvalSucceeded);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    /**
     * 审批不通过,不加任何状态判断
     *
     * @param leaveBill 请假单
     */
    public void doFailed(LeaveBill leaveBill) {
        // 将状态设置为审批不通过
        leaveBill.setState(approvalFailed);
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人审批未通过
        System.out.println("亲爱的xxx,你的请假单未审批通过,未通过原因为:xxx");
    }

    @Test
    public void test() {
        // 1.2 审批通过
        pass(1L);
        System.out.println("=========================================");
        // 2.2 审批未通过
        failed(1L);
    }
}

新增了 waitSubmit 状态,新增了 submit 方法,每个方法里面都加了 waitSubmit 的判断逻辑。

那我上面写的这段代码有什么问题呢?你可以暂停下来思考一下。







上文代码缺点:

  1. 状态混乱,各种 if-else 判断,状态转换不明显
  2. 可扩展性差,如果加状态,又要在每个方法里面都加入if else逻辑,没有遵守开放-关闭原则(对扩展开放,对修改关闭)
  3. 没有把会改变的那部分代码包起来,没有遵守”封装变化“原则

那我们用状态模式我们应该怎么解决这个问题呢?我们应该把会变得那部分代码封装起来,将每一种状态都单独抽成一个类,将每个状态的行为都放在各自的类中,那么每个状态只需要实现它自己的动作就可以了,也就是将操作委托给当前状态的状态对象。这样我们添加新的状态时,添加一个对应的状态类,然后实现具体的操作就好了。这就是状态模式。

状态模式的UML图如下:

这里对其中的类做下解释:

  • State:可以是抽象类或者接口,在这个接口/抽象类 中定义每个动作的方法
  • ConcreteStateA、ConcreteStateB:State 具体的实现类,对应某一种状态的实现
  • Context:串联所有状态的封装类,封装的目的是为了让内部状态的变化不被调用类知晓(迪米特法则)

下面我按照状态模式来改造下代码:

首先建一个抽象类 LeaveState,里面包含了 Context 和所有方法。

由于这里面没有需要统一进行处理的操作,所以你将抽象类改为接口也是完全OK的

public abstract class LeaveState {

    protected Context context;

    public void setContext(Context context) {
        this.context = context;
    }

    public abstract void pass(long id);

    public abstract void failed(long id);
}

将所有的状态都抽成一个类,然后继承抽象类 LeaveState

待领导审批状态:

public class WaitLeaderApprovalState extends LeaveState {
    @Override
    public void pass(long id) {
        // 修改要处理的状态类
        super.context.setState(super.context.APPROVAL_SUCCEEDED);
        // 动作委派
        super.context.getState().pass(id);
    }

    @Override
    public void failed(long id) {
        // 修改要处理的状态类
        super.context.setState(super.context.APPROVAL_FAILED);
        // 委派给要处理的状态类
        super.context.getState().failed(id);
    }
}

审批成功状态:

public class ApprovalSucceededState extends LeaveState {

    @Override
    public void pass(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为审批通过
        leaveBill.setState(super.context.approvalSucceeded);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    @Override
    public void failed(long id) {
        System.out.println("已经审批通过的单据还想再给我审批不通过,门也没有!");
    }
}

审批失败状态:

public class ApprovalFailedState extends LeaveState {
    @Override
    public void pass(long id) {
        System.out.println("已审批未通过,不能再次审批");
    }

    @Override
    public void failed(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为审批未通过
        leaveBill.setState(super.context.approvalFailed);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        System.out.println("您好,您的请假单审批未通过");
    }
}

状态上下文封装类:

public class Context {

    public Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1L, new LeaveBill(1L, "心情不好,申请请假一天", waitLeaderApproval));
        put(2L, new LeaveBill(2L, "家里有事,申请请假", waitLeaderApproval));
    }};

    /**
     * 等待领导审批
     */
    final int waitLeaderApproval = 1;

    /**
     * 审批成功,请假成功
     */
    final int approvalSucceeded = 2;

    /**
     * 万恶的领导不给批假,审批未通过
     */
    final int approvalFailed = 3;

    protected final WaitLeaderApprovalState WAIT_LEADER_APPROVAL = new WaitLeaderApprovalState();

    protected final ApprovalSucceededState APPROVAL_SUCCEEDED = new ApprovalSucceededState();

    protected final ApprovalFailedState APPROVAL_FAILED = new ApprovalFailedState();

    private LeaveState state;


    public LeaveState getState() {
        return state;
    }

    public void setState(LeaveState state) {
        this.state = state;
        // 将当前环境通知到各个类中
        this.state.setContext(this);
    }
}

这个类里面包含了所有的状态类,

测试:

@Test
public void test(){
    Context context = new Context();
    context.setState(context.WAIT_LEADER_APPROVAL);
    context.getState().pass(1L);
    context.getState().failed(1L);
}

结果:

亲爱的xxx,你的请假单已审批通过
已经审批通过的单据还想再给我审批不通过,门也没有!

那么此时,保存 草稿的需求来了,我们应该怎么改动呢?

  1. 在 LeaveState 中添加 submit() 方法
  2. 添加待提交的状态类 WaitSubmitState,继承 LeaveState 并实现里面的抽象方法。
  3. 给 LeaveState 所有的子类添加 submit() 方法的实现。
  4. Context 类中添加 WaitSubmitState 的依赖。

各个类改动的代码如下(为了看起来简洁,这里只贴做了新增的代码,原来的代码就不贴了):

LeaveState:

public abstract class LeaveState {
    // ...
    public abstract void submit(long id);
}

WaitSubmitState:

public class WaitSubmitState extends LeaveState {

    @Override
    public void submit(long id) {
        // 从数据库中查询请假单
        LeaveBill leaveBill = super.context.database.get(id);
        // 将状态设置为待领导审批
        leaveBill.setState(super.context.waitLeaderApproval);
        // 更新数据库
        super.context.database.put(leaveBill.getId(), leaveBill);
        super.context.setState(super.context.WAIT_LEADER_APPROVAL);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单提交审批");
    }

    @Override
    public void pass(long id) {
        System.out.println("请假单未提交不能审批");
    }

    @Override
    public void failed(long id) {
        System.out.println("请假单未提交不能审批");
    }
}

Context:

public class Context {
    /**
     * 待提交
     */
    final int waitSubmit = 0;

    protected final WaitSubmitState WAIT_SUBMIT = new WaitSubmitState();
}

WaitLeaderApprovalState:

public class WaitLeaderApprovalState extends LeaveState {
    @Override
    public void submit(long id) {
        System.out.println("待审批状态不能再次提交");
    }
}

ApprovalSucceededState:

public class ApprovalSucceededState extends LeaveState {

    @Override
    public void submit(long id) {
        System.out.println("已审批通过,不能提交");
    }
}

ApprovalFailedState:

public class ApprovalFailedState extends LeaveState {

    @Override
    public void submit(long id) {
        System.out.println("已审批失败,不能再次提交");
    }
}

我们做的这些操作基本都没有更改原来方法的逻辑,而是添加了新的方法,这就符合了开闭原则。以后再添加别的状态,比如 待经理审批待老板审批等都可以按这种方式来做改动。

通过上面的案例我们来总结下状态模式的优缺点:

优点是:状态模式将每个状态的行为都收敛到了它自己的类中,将容易产生问题的if语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。
缺点:

  1. 创建的类多了,每个状态都要对应一个类。
  2. 新增的状态如果有对应的处理方法,则每个子类都要实现该处理方法。

3.状态模式和状态机

我们说的状态机一般是指有限状态机,它是一种计算机模型,它是指有限个状态之间的转换。而状态模式是一种设计模式,它们两个不是同一个东西,
很多文章经常都把这两个词混淆使用,也完全可以理解。因为当你要用状态模式实现一个功能的时候,这个功能结构肯定不适合被称为"状态模式",而更适合称为"状态机"。可以理解成状态机是状态模式的一种应用。

4.Spring StateMachine (Spring 状态机)

本节中我会使用 Spring StateMachine 来完成请假单审批的功能,你可以做个对比。

首先在 pom.xml 中引入 Spring StateMachine

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-core</artifactId>
</dependency>

将请假单状态定义为枚举:

public enum LeaveStateEnum {

    /**
     * 待提交
     */
    WAIT_SUBMIT,

    /**
     * 等待领导审批
     */
    WAIT_LEADER_APPROVAL,

    /**
     * 审批成功,请假成功
     */
    APPROVAL_SUCCEEDED,

    /**
     * 万恶的领导不给批假,审批未通过
     */
    APPROVAL_FAILED
}

将状态转换的动作定义为枚举类:

public enum LeaveStateEvent {
    /**
     * 提交审批
     */
    SUBMIT,

    /**
     * 审批通过
     */
    PASS,

    /**
     * 审批不通过
     */
    FAILED
}

状态机配置类:

@Configuration
@EnableStateMachine
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<LeaveStateEnum, LeaveStateEvent> {

    @Override
    public void configure(StateMachineStateConfigurer<LeaveStateEnum, LeaveStateEvent> states) throws Exception {
        // 初始化状态
        states.withStates().initial(LeaveStateEnum.WAIT_SUBMIT).states(EnumSet.allOf(LeaveStateEnum.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<LeaveStateEnum, LeaveStateEvent> transitions) throws Exception {
        // 定义各个状态之间转换时对应的动作(事件)
        transitions.withExternal().source(LeaveStateEnum.WAIT_SUBMIT).target(LeaveStateEnum.WAIT_LEADER_APPROVAL).event(LeaveStateEvent.SUBMIT).and().withExternal().source(LeaveStateEnum.WAIT_LEADER_APPROVAL).target(LeaveStateEnum.APPROVAL_SUCCEEDED).event(LeaveStateEvent.PASS).and().withExternal().source(LeaveStateEnum.WAIT_LEADER_APPROVAL).target(LeaveStateEnum.APPROVAL_FAILED).event(LeaveStateEvent.FAILED);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<LeaveStateEnum, LeaveStateEvent> config) throws Exception {
        // 设置状态机id
        config.withConfiguration().machineId("leaveStateMachine");
    }
}

通过 @EnableStateMachine 注解用来启用 Spring StateMachine 状态机功能。

配置状态机对应的动作处理方法:

@WithStateMachine(name = "leaveStateMachine")
@Configuration
public class StateMachineEventConfig {

    public Map<Long, LeaveBill> database = new ConcurrentHashMap<Long, LeaveBill>() {{
        put(1L, new LeaveBill(1L, "心情不好,申请请假一天", LeaveStateEnum.WAIT_SUBMIT.ordinal()));
        put(2L, new LeaveBill(2L, "家里有事,申请请假", LeaveStateEnum.WAIT_SUBMIT.ordinal()));
    }};

    @OnTransition(source = "WAIT_SUBMIT", target = "WAIT_LEADER_APPROVAL")
    public void submit(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为待领导审批
        leaveBill.setState(LeaveStateEnum.WAIT_LEADER_APPROVAL.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已提交审批");
    }

    @OnTransition(source = "WAIT_LEADER_APPROVAL", target = "APPROVAL_SUCCEEDED")
    public void pass(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为审批通过
        leaveBill.setState(LeaveStateEnum.APPROVAL_SUCCEEDED.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        // 通知申请人
        System.out.println("亲爱的xxx,你的请假单已审批通过");
    }

    @OnTransition(source = "WAIT_LEADER_APPROVAL", target = "APPROVAL_FAILED")
    public void failed(Message<LeaveStateEvent> msg) {
        Long leaveId = (Long) msg.getHeaders().get("leaveId");
        // 从数据库中查询请假单
        LeaveBill leaveBill = database.get(leaveId);
        // 将状态设置为审批未通过
        leaveBill.setState(LeaveStateEnum.APPROVAL_FAILED.ordinal());
        // 更新数据库
        database.put(leaveBill.getId(), leaveBill);
        System.out.println("您好,您的请假单审批未通过");
    }
}

@OnTransition 中 source 指定原始状态,target 指定目标状态,当事件触发时将会被监听到从而调用该方法。

单元测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class StateTest {
    @Autowired
    private StateMachine<LeaveStateEnum, LeaveStateEvent> stateMachine;

    @Test
    public void test() throws Exception {
        stateMachine.start();
        stateMachine.sendEvent(MessageBuilder.withPayload(LeaveStateEvent.SUBMIT).setHeader("leaveId", 1L).build());
        stateMachine.sendEvent(MessageBuilder.withPayload(LeaveStateEvent.PASS).setHeader("leaveId", 1L).build());
    }
}

可以通过 Message来传递参数。

结果如下:

亲爱的xxx,你的请假单已提交审批
亲爱的xxx,你的请假单已审批通过

可以看到使用 Spring StateMachine 之后,我们只需要配置各个状态之间的流转以及对应的状态,就可以实现相应的功能了。在实际应用中,我们的系统一般不会只有一个状态机在运行,可以通过 StateMachineBuilder 来构建多个状态机,如果你有兴趣可以去Spring官网研究下,这里不再做赘述。

5.小结

本文通过请假审批的案例来讲解了状态模式:它可以通过改变对象内部的状态来帮助对象控制自己的行为。状态模式将每个状态的行为都收敛到了它自己的类中,将容易产生问题的if语句删除,以方便日后的维护,并且代码更容易阅读和理解。让每一个状态“对修改关闭”,对扩展开放。在项目中我们可以使用 Spring StateMachine 来帮助我们更简单的应用状态模式。当然,如果 Spring StateMachine 不能满足你的需求,你也可以根据状态模式的思想来自定义一个自己的状态机。

好了,这篇文章就到这里了,感谢大家的观看!如有错误,请及时指正!欢迎大家关注我的公众号:贾哇技术指南

参考

历史文章回顾

分布式事务解决方案汇总

Java各种内存溢出异常实践

详解JVM运行时数据区

Java线程启动流程

  • 7
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Spring Security 参考 1 第一部分前言 15 1.入门 16 2.介绍 17 2.1什么是Spring Security? 17 2.2历史 19 2.3版本编号 20 2.4获得Spring安全 21 2.4.1使用Maven 21 Maven仓库 21 Spring框架 22 2.4.2 Gradle 23 Gradle存储库 23 使用Spring 4.0.x和Gradle 24 2.4.3项目模块 25 核心 - spring-security-core.jar 25 远程处理 - spring-security-remoting.jar 25 Web - spring-security-web.jar 25 配置 - spring-security-config.jar 26 LDAP - spring-security-ldap.jar 26 ACL - spring-security-acl.jar 26 CAS - spring-security-cas.jar 26 OpenID - spring-security-openid.jar 26 测试 - spring-security-test.jar 26 2.4.4检出来源 26 3. Spring Security 4.2的新特性 27 3.1 Web改进 27 3.2配置改进 28 3.3杂项 28 4.样品和指南(从这里开始) 28 5. Java配置 29 5.1 Hello Web安全Java配置 29 5.1.1 AbstractSecurityWebApplicationInitializer 31 5.1.2 AbstractSecurityWebApplicationInitializer不存在Spring 31 5.1.3使用Spring MVC的AbstractSecurityWebApplicationInitializer 32 5.2 HttpSecurity 32 5.3 Java配置和表单登录 34 5.4授权请求 35 5.5处理注销 36 5.5.1 LogoutHandler 37 5.5.2 LogoutSuccessHandler 37 5.5.3更多注销相关参考 38 5.6认证 38 5.6.1内存认证 38 5.6.2 JDBC认证 39 5.6.3 LDAP认证 39 5.6.4 AuthenticationProvider 41 5.6.5 UserDetailsService 41 5.6.6 LDAP认证 41 5.7多个HttpSecurity 41 5.8方法安全性 43 5.8.1 EnableGlobalMethodSecurity 43 5.8.2 GlobalMethodSecurityConfiguration 44 5.9后处理配置的对象 45 5.10自定义DSL 46 6.安全命名空间配置 47 6.1简介 47 6.1.1命名空间的设计 49 6.2安全命名空间配置入门 50 6.2.1 web.xml配置 50 6.2.2最小的配置 50 6.2.3表单和基本登录选项 52 设置默认的登录目的地 54 6.2.4注销处理 54 6.2.5使用其他身份验证提供程序 55 添加密码编码器 56 6.3高级Web功能 56 6.3.1记得我认证 56 6.3.2添加HTTP / HTTPS通道安全 57 6.3.3会话管理 57 检测超时 57 并发会话控制 58 会话固定攻击保护 59 6.3.4 OpenID支持 60 属性交换 61 6.3.5响应头 62 6.3.6添加你自己的过滤器 62 设置一个自定义的AuthenticationEntryPoint 64 6.4方法安全 64 6.4.1 元素 65 使用protect-pointcut添加安全性切入点 66 6.5默认AccessDecisionManager 67 6.5.1自定义AccessDecisionManager 67 6.6验证管理器和命名空间 67 7.示例应用程序 69 7.1教程示例 69 7.2联系人 69 7.3 LDAP样本 71 7.4 OpenID示例 71 7.5 CAS样品 71 7.6 JAAS样品 72 7.7预认证样本 72 8. Spring Security社区 72 8.1问题跟踪 72 8.2成为参与 73 8.3更多信息 73 第二部分 架构与实现 73 9.技术概述 73 9.1运行环境 73 9.2核心组件 74 9.2.1 SecurityContextHolder,SecurityContext和认证对象 74 获取有关当前用户的信息 75 9.2.2 UserDetailsService 75 9.2.3授予权力 77 9.2.4总结 77 9.3认证 78 9.3.1什么是Spring Security中的认证? 78 9.3.2直接设置SecurityContextHolder内容 80 9.4 Web应用程序中的身份验证 81 9.4.1 ExceptionTranslationFilter 82 9.4.2 AuthenticationEntryPoint 82 9.4.3认证机制 82 9.4.4在请求之间存储SecurityContext 83 9.5 Spring Security中的访问控制(授权) 84 9.5.1安全和AOP建议 84 9.5.2安全对象和AbstractSecurityInterceptor 85 什么是配置属性? 85 RunAsManager 86 AfterInvocationManager 86 扩展安全对象模型 87 9.6本地化 87 10.核心服务 89 10.1 AuthenticationManager,ProviderManager和AuthenticationProvider 89 10.1.1成功认证时清除证书 91 10.1.2 DaoAuthenticationProvider 91 10.2 UserDetailsService实现 92 10.2.1内存认证 92 10.2.2 JdbcDaoImpl 93 权威组织 94 10.3密码编码 94 10.3.1什么是散列? 95 10.3.2添加盐到哈希 95 10.3.3散列和认证 96 10.4Jackson 支持 96 第三部分 测试 97 11.测试方法安全性 97 11.1安全测试设置 98 11.2 @WithMockUser 98 11.3 @WithAnonymousUser 100 11.4 @用户详细信息 101 11.5 @WithSecurityContext 102 11.6测试元注释 104 12. Spring MVC测试集成 104 12.1设置MockMvc和Spring Security 104 12.2 SecurityMockMvcRequestPostProcessors 105 12.2.1使用CSRF保护进行测试 105 12.2.2在Spring MVC测试中以用户身份运行测试 106 12.2.3使用RequestPostProcessor在Spring MVC测试中以用户身份运行 106 作为用户在Spring MVC测试中使用注释运行 108 12.2.4测试HTTP基本认证 109 12.3 SecurityMockMvcRequestBuilders 109 12.3.1测试基于表单的认证 109 12.3.2测试注销 110 12.4 SecurityMockMvcResultMatchers 110 12.4.1未经认证的声明 111 12.4.2认证断言 111 第四部分 Web应用程序安全 112 13.安全过滤器链 112 13.1 DelegatingFilterProxy 112 13.2 FilterChainProxy 113 13.2.1绕过滤网链 115 13.3过滤器排序 115 13.4请求匹配和HttpFirewall 116 13.5与其他基于过滤器的框架一起使用 118 13.6高级命名空间配置 118 14.核心安全筛选器 119 14.1 FilterSecurityInterceptor 119 14.2 ExceptionTranslationFilter 121 14.2.1 AuthenticationEntryPoint 122 14.2.2 AccessDeniedHandler 122 14.2.3 SavedRequest和RequestCache接口 123 14.3 SecurityContextPersistenceFilter 123 14.3.1 SecurityContextRepository 124 14.4 UsernamePasswordAuthenticationFilter 125 14.4.1认证成功与失败的应用流程 125 15. Servlet API集成 127 15.1 Servlet 2.5+集成 127 15.1.1 HttpServletRequest.getRemoteUser() 127 15.1.2 HttpServletRequest.getUserPrincipal() 127 15.1.3 HttpServletRequest.isUserInRole(String) 128 15.2 Servlet 3+集成 128 15.2.1 HttpServletRequest.authenticate(HttpServletResponse) 128 15.2.2 HttpServletRequest.login(String,String) 129 15.2.3 HttpServletRequest.logout() 129 15.2.4 AsyncContext.start(Runnable) 129 15.2.5异步Servlet支持 130 15.3 Servlet 3.1+集成 131 15.3.1 HttpServletRequest#changeSessionId() 132 16.基本和摘要式身份验证 132 16.1 BasicAuthenticationFilter 132 16.1.1配置 132 16.2 DigestAuthenticationFilter 133 16.2.1配置 135 17.记住我的身份验证 136 17.1概述 136 17.2简单的基于哈希的令牌方法 136 17.3持久性令牌方法 137 17.4记住我的接口和实现 138 17.4.1 TokenBasedRememberMeServices 138 17.4.2 PersistentTokenBasedRememberMeServices 139 18.跨站点请求伪造(CSRF) 140 18.1 CSRF攻击 140 18.2同步器令牌模式 141 18.3何时使用CSRF保护 142 18.3.1 CSRF保护和JSON 142 18.3.2 CSRF和无状态浏览器应用程序 143 18.4使用Spring Security CSRF保护 143 18.4.1使用适当的HTTP动词 144 18.4.2配置CSRF保护 144 18.4.3包含CSRF令牌 145 表单提交 145 Ajax和JSON请求 145 CookieCsrfTokenRepository 146 18.5 CSRF警告 147 18.5.1超时 148 18.5.2登录 148 18.5.3注销 149 18.5.4多部分(文件上传) 149 在Spring Security之前放置MultipartFilter 150 包含CSRF令牌 151 18.5.5隐藏的HttpMethodFilter 151 18.6覆盖默认值 151 19. CORS 152 20.安全性HTTP响应头 154 20.1默认的安全头 154 20.1.1缓存控制 157 20.1.2内容类型选项 158 20.1.3 HTTP严格传输安全(HSTS) 159 20.1.4 HTTP公钥密码(HPKP) 161 20.1.5 X-Frame-Options 163 20.1.6 X-XSS保护 164 20.1.7内容安全策略(CSP) 165 配置内容安全策略 166 其他资源 168 20.1.8推荐人政策 168 配置引用者策略 169 20.2自定义标题 169 20.2.1静态头 169 20.2.2标题作者 170 20.2.3 DelegatingRequestMatcherHeaderWriter 171 21.会议管理 172 21.1 SessionManagementFilter 173 21.2 SessionAuthenticationStrategy 173 21.3并发控制 174 21.3.1查询当前通过身份验证的用户及其会话的SessionRegistry 176 22.匿名身份验证 177 22.1概述 177 22.2配置 178 22.3 AuthenticationTrustResolver 179 23. WebSocket安全 180 23.1 WebSocket配置 181 23.2 WebSocket身份验证 182 23.3 WebSocket授权 182 23.3.1 WebSocket授权说明 183 消息类型的WebSocket授权 184 目的地上的WebSocket授权 184 23.3.2出站消息 185 23.4执行相同的来源政策 185 23.4.1为什么同源? 185 23.4.2 Spring WebSocket允许的来源 186 23.4.3添加CSRF到Stomp头 186 23.4.4在WebSockets中禁用CSRF 187 23.5使用SockJS 187 23.5.1 SockJS和框架选项 187 23.5.2轻松放松CSRF 188 第五部分授权 190 24.授权体系结构 190 24.1当局 190 24.2预调用处理 191 24.2.1 AccessDecisionManager 191 24.2.2基于投票的AccessDecisionManager实现 192 RoleVoter 193 AuthenticatedVoter 194 自定义选民 194 24.3调用处理后 194 24.4分层角色 196 25.安全的对象实现 197 25.1 AOP联盟(MethodInvocation)安全拦截器 197 25.1.1显式MethodSecurityInterceptor配置 197 25.2 AspectJ(JoinPoint)安全拦截器 198 26.基于表达式的访问控制 200 26.1概述 200 26.1.1通用内置表达式 201 26.2网络安全表达式 202 26.2.1在Web安全表达式中引用Bean 203 26.2.2 Web安全表达式中的路径变量 204 26.3方法安全表达式 204 26.3.1 @Pre和@Post注释 205 访问控制使用@PreAuthorize和@PostAuthorize 205 使用@PreFilter和@PostFilter进行过滤 207 26.3.2内置表达式 207 PermissionEvaluator接口 208 方法安全元注释 209 第六部分 其他主题 209 27.域对象安全(ACL) 209 27.1概述 209 27.2重要概念 211 27.3入门 214 28.预认证方案 216 28.1预认证框架类 216 28.1.1 AbstractPreAuthenticatedProcessingFilter 217 J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource 217 28.1.2 PreAuthenticatedAuthenticationProvider 218 28.1.3 Http403ForbiddenEntryPoint 218 28.2具体实施 219 28.2.1请求头认证(Siteminder) 219 Siteminder示例配置 219 28.2.2 Java EE容器认证 220 29. LDAP认证 220 29.1概述 220 29.2在Spring Security中使用LDAP 221 29.3配置LDAP服务器 221 29.3.1使用嵌入式测试服务器 222 29.3.2使用绑定认证 222 29.3.3加载权限 223 29.4实现类 223 29.4.1 LdapAuthenticator实现 224 通用功能 224 认证者 225 PasswordComparisonAuthenticator 225 29.4.2连接到LDAP服务器 225 29.4.3 LDAP搜索对象 225 FilterBasedLdapUserSearch中 225 29.4.4 LdapAuthoritiesPopulator 226 29.4.5 Spring Bean配置 226 29.4.6 LDAP属性和定制的UserDetails 227 29.5 Active Directory认证 228 29.5.1 ActiveDirectoryLdapAuthenticationProvider 228 活动目录错误代码 229 30. JSP标签库 230 30.1声明Taglib 230 30.2授权标签 230 30.2.1禁用测试的标签授权 231 30.3认证标签 232 30.4 accesscontrollist标签 232 30.5 csrfInput标签 233 30.6 csrfMetaTags标签 233 31 Java认证和授权服务(JAAS)提供者 235 31.1概述 235 31.2摘要:Java认证提供者 235 31.2.1 JAAS CallbackHandler 235 31.2.2 JAAS权威机构 236 31.3 DefaultJaasAuthenticationProvider 237 31.3.1 InMemoryConfiguration 237 31.3.2 DefaultJaasAuthenticationProvider示例配置 238 31.4 JaasAuthenticationProvider 239 31.5作为主题运行 240 32. CAS认证 240 32.1概述 240 32.2 CAS的工作原理 240 32.2.1 Spring安全和CAS交互序列 241 32.3 CAS客户端的配置 244 32.3.1服务票据认证 244 32.3.2单一注销 246 32.3.3使用CAS认证无状态服务 249 配置CAS以获取代理授予票证 249 使用代理票证调用无状态服务 250 32.3.4代理票证认证 251 33. X.509认证 253 33.1概述 253 33.2将X.509身份验证添加到您的Web应用程序 253 33.3在Tomcat中设置SSL 254 34.运行认证替换 255 34.1概述 255 34.2配置 255 35. Spring Security加密模块 257 35.1简介 257 35.2加密器 257 35.2.1 BytesEncryptor 257 35.2.2 TextEncryptor 258 35.3关键发电机 258 35.3.1 BytesKeyGenerator 258 35.3.2 StringKeyGenerator 259 35.4密码编码 259 36.并发支持 260 36.1 DelegatingSecurityContextRunnable 260 36.2 DelegatingSecurityContextExecutor 262 36.3 Spring安全性并发类 264 37. Spring MVC集成 265 37.1 @EnableWebMvcSecurity 265 37.2 MvcRequestMatcher 265 37.3 @AuthenticationPrincipal 268 37.4 Spring MVC异步集成 271 37.5 Spring MVC和CSRF集成 271 37.5.1自动令牌包含 271 37.5.2解析CsrfToken 272 第七部分 Spring数据集成 273 38. Spring Data&Spring安全配置 273 39. @Query中的安全表达式 273 第八部分 附录 274 40.安全数据库模式 274 40.1用户模式 274 40.1.1集团当局 274 40.2持久登录(记得我)架构 275 40.3 ACL模式 275 40.3.1 HyperSQL 276 40.3.2 PostgreSQL 277 40.3.3 MySQL和MariaDB 278 40.3.4 Microsoft SQL Server 279 40.3.5 Oracle数据库 280 41.安全命名空间 282 41.1 Web应用程序安全性 282 41.1.1 282 41.1.2 282 属性 283 的子元素 285 41.1.3 286 的父元素 286 属性 286 41.1.4 286 属性 287 父元素 287 41.1.5 <headers> 287 <headers>属性 288 <headers>的父元素 288 <headers>的子元素 288 41.1.6 289 属性 289 的父元素 289 41.1.7 289 属性 289 的父元素 290 41.1.8 290 属性 290 的父元素 290 41.1.9 290 的子元素 290 41.1.10 291 属性 291 的父元素 291 41.1.11 291 属性 291 的父元素 291 41.1.12 291 属性 292 的父元素 292 41.1.13 <frame-options> 292 <frame-options>属性 292 <frame-options>的父元素 293 41.1.14 [removed] 293 [removed]属性 293 [removed]的父元素 294 41.1.15 294 属性 294 的父元素 294 41.1.16 <header> 294 <header-attributes>属性 294 <header>的父元素 295 41.1.17 295 的父元素 295 属性 295 41.1.18 295 父元素 296 属性 296 41.1.19 296 的父元素 296 属性 296 41.1.20 <expression-handler> 297 <expression-handler>的父元素 297 属性 297 41.1.21 <form-login> 297 <form-login>的父元素 298 <form-login>属性 298 41.1.22 299 的父元素 300 属性 300 41.1.23 元素 300 属性 300 41.1.24 300 的父元素 300 属性 301 41.1.25 302 的父元素 302 属性 302 41.1.26 302 父元素 302 属性 303 41.1.27 303 的父元素 303 属性 303 的子元素 305 41.1.28 305 的父元素 305 属性 305 的子元素 305 41.1.29 306 的父元素 306 属性 306 41.1.30 306 的父元素 306 的子元素 307 41.1.31 307 的父元素 307 属性 307 41.1.32 307 的父元素 307 属性 307 41.1.33 元素 309 的父元素 309 属性 309 41.1.34 309 的父元素 309 属性 309 的子元素 310 41.1.35 311 的父元素 311 属性 311 41.1.36 312 的父元素 312 属性 312 41.1.37 313 属性 313 的子元素 313 41.1.38 313 的父元素 313 属性 313 41.1.39 314 属性 314 的子元素 314 41.2 WebSocket安全 314 41.2.1 315 属性 315 的子元素 316 41.2.2 316 的父元素 316 属性 316 41.3认证服务 317 41.3.1 317 属性 317 的子元素 317 41.3.2 318 的父元素 318 属性 318 的子元素 318 41.3.3 319 属性 319 41.3.4 320 的父元素 320 属性 320 的子元素 320 41.3.5 320 的父元素 321 属性 321 41.3.6 321 属性 321 的子元素 321 41.3.7 321 的父元素 322 属性 322 41.4方法安全 322 41.4.1 322 属性 322 的子元素 324 41.4.2 324 的父元素 324 属性 324 41.4.3 324 的父元素 325 325 41.4.4 325 的父元素 325 属性 325 41.4.5 325 的父元素 325 属性 325 41.4.6 326 的父元素 326 属性 326 41.4.7使用安全方法 326 父节点 326 属性 326 41.4.8 326 属性 327 的子元素 327 41.4.9 327 属性 327 的子元素 327 41.4.10 327 父元素 328 属性 328 41.5 LDAP名称空间选项 328 41.5.1使用。定义LDAP服务器 328 属性 329 41.5.2 329 的父元素 329 属性 329 的子元素 331 41.5.3 331 的父元素 331 属性 332 的子元素 332 41.5.4 332 属性 332 42.春季安全依赖 333 42.1 spring-security-core 334 42.2 spring-security-remoting 334 42.3 spring-security-web 335 42.4 spring-security-ldap 335 42.5 spring-security-config 336 42.6 spring-security-acl 336 42.7 spring-security-cas 337 42.8 spring-security-openid 337 42.9 spring-security-taglibs 338 43.代理服务器配置 338 44. Spring Security FAQ 339 44.1一般问题 339 44.1.1 Spring Security是否会处理我所有的应用程序安全要求? 339 44.1.2为什么不使用web.xml安全? 339 44.1.3需要哪些Java和Spring Framework版本? 341 44.1.4我是Spring Security的新手,我需要构建一个支持通过HTTPS进行CAS单点登录的应用程序,同时允许对某些URL进行本地基本身份验证,并对多个后端用户信息源(LDAP和JDBC)进行身份验证。我已经复制了一些我发现的配置文件,但不起作用。什么可能是错的? 341 44.2常见问题 342 44.2.1当我尝试登录时,我收到一条错误消息,指出“Bad Credentials”。怎么了? 343 44.2.2当我尝试登录时,我的应用程序进入“无限循环”,发生了什么事? 344 44.2.3我收到一条异常消息“访问被拒绝(用户是匿名的)”。怎么了? 344 44.2.4即使在我退出应用程序之后,为什么还能看到安全的页面? 345 44.2.5我得到一个异常,消息“在SecurityContext中没有找到认证对象”。怎么了? 345 44.2.6我无法使LDAP认证正常工作。我的配置有什么问题? 345 44.2.7会话管理 346 44.2.8我使用Spring Security的并发会话控制来防止用户一次登录多次。登录后打开另一个浏览器窗口时,不会阻止我再次登录。为什么我可以多次登录? 347 44.2.9为什么在通过Spring Security进行身份验证时会话ID发生了变化? 347 44.2.10我正在使用Tomcat(或其他一些servlet容器),并为我的登录页面启用了HTTPS,之后切换回HTTP。这是行不通的 - 我只是在认证之后回到登录页面。 347 44.2.11我没有在HTTP和HTTPS之间切换,但是我的会话仍然丢失 348 44.2.12我试图使用并发会话控制支持,但是不会让我重新登录,即使我确定我已经注销并且没有超出允许的会话。 348 44.2.13 Spring Security正在创建一个会话,即使我已经配置了它,通过设置create-session属性为永远不会。 348 44.2.14执行POST时,我得到了一个403 Forbidden 349 44.2.15我正在使用RequestDispatcher将请求转发到另一个URL,但是我的安全限制没有被应用。 349 44.2.16我已经将Spring Security的元素添加到我的应用程序上下文中,但是如果将安全注释添加到我的Spring MVC控制器bean(Struts操作等)中,那么它们似乎没有效果。 349 44.2.17我有一个肯定被认证的用户,但是当我在一些请求期间尝试访问SecurityContextHolder时,认证是空的。为什么我看不到用户信息? 350 44.2.18在使用URL属性时,授权JSP标记不尊重我的方法安全注释。 350 44.3 Spring安全体系结构问题 350 44.3.1我如何知道X是哪个包? 350 44.3.2名称空间元素如何映射到传统的bean配置? 351 44.3.3“ROLE_”是什么意思,为什么我的角色名字需要它? 351 44.3.4如何知道添加到我的应用程序中的哪些依赖关系与Spring Security一起使用? 352 44.3.5运行嵌入式ApacheDS LDAP服务器需要哪些依赖关系? 352 44.3.6什么是UserDetailsService,我需要一个吗? 353 44.4共同的“Howto”请求 353 44.4.1我需要登录更多的信息,而不仅仅是用户名。如何添加对额外登录字段(例如公司名称)的支持? 354 44.4.2如果只有所请求的URL的片段值不同(例如/ foo#bar和/ foo#blah),我该如何应用不同的拦截url链接? 354 44.4.3如何在UserDetailsService中访问用户的IP地址(或其他Web请求数据)? 354 44.4.4如何从UserDetailsService访问HttpSession? 355 44.4.5如何在UserDetailsService中访问用户的密码? 355 44.4.6如何动态定义应用程序中的安全URL? 355 44.4.7如何针对LDAP进行身份验证,但从数据库加载用户角色? 357 44.4.8我想修改由命名空间创建的bean的属性,但是模式中没有任何东西支持它。我可以做什么放弃命名空间的使用? 358 45.从3.x迁移到4.x 359

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值