Bootstrap

【SpringBoot新手篇】SpringBoot集成SpringSecurity前后端分离开发

1. 安全简介

Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于spring的应用程序的实际标准。Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它很容易扩展以满足定制需求

spring security 的核心功能主要包括:

  • Authentication用户认证 (你是谁)
  • Authorization用户授权 (你能干什么)
  • 防止攻击(防止伪造身份),如会话固定,点击劫持,跨站请求伪造等
  • Servlet API的集成
  • 可选的与Spring Web MVC的集成

实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security

因此,一般来说,常见的安全管理技术栈的组合是这样的:

  • SSM + Shiro
  • Spring Boot/Spring Cloud + Spring Security

2. 认识SpringSecurity

Spring Security的官网

Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

3.SpringBoot整合Security

认证和授权

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能

1、引入 Spring Security 模块

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写 Spring Security 配置类 参考官网:https://spring.io/projects/spring-security

查看我们自己项目中的版本,找到对应的帮助文档:

3、编写基础配置类

@EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       
  }
}

4、定制请求的授权规则

@Override
protected void configure(HttpSecurity http) throws Exception {
   // 定制请求的授权规则
   // 首页所有人可以访问
   http.authorizeRequests().antMatchers("/").permitAll()
  .antMatchers("/level1/**").hasRole("vip1")
  .antMatchers("/level2/**").hasRole("vip2")
  .antMatchers("/level3/**").hasRole("vip3");
}

5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!

6、在configure()方法中加入以下配置,开启自动配置的登录功能!

// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();

7、测试一下:发现,没有权限的时候,会跳转到登录的页面!

8、查看刚才登录页的注释信息; 我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
   //在内存中定义,也可以在jdbc中去拿....
   auth.inMemoryAuthentication()
          .withUser("admin").password("123456").roles("vip2","vip3")
          .and()
          .withUser("root").password("root").roles("vip1","vip2","vip3")
          .and()
          .withUser("guest").password("123456").roles("vip1","vip2");
}

9、测试,我们可以使用这些账号登录进行测试!发现会报错!

There is no PasswordEncoder mapped for the id “null”

10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   //在内存中定义,也可以在jdbc中去拿....
   //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
   //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
   //spring security 官方推荐的是使用bcrypt加密方式。
   
   auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                    .withUser("root").password(new BCryptPasswordEncoder().encode("root"))
                    .roles("vip1","vip2","vip3")
                    .and()
                    .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2")
                    .and()
                    .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip3");
}

11、测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定

权限控制和注销

1、开启自动配置的注销的功能

//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
    //开启自动配置的注销的功能
    // 默认为/logout 注销请求
    http.logout().deleteCookies("remove").invalidateHttpSession(true).logoutSuccessUrl("/");
}

2、我们在前端,增加一个注销的按钮,index.html 导航栏中

<div class="btn-group btn-group-lg">
    <button type="button" class="btn btn-default"><a th:href="@{/logout}">注销</a></button>
</div>

3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?

// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");

5、测试,注销完毕后,发现跳转到首页OK

6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如admin这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?

我们需要结合thymeleaf中的一些功能

sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面

Maven依赖:

<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
   <groupId>org.thymeleaf.extras</groupId>
   <artifactId>thymeleaf-extras-springsecurity5</artifactId>
   <version>3.0.4.RELEASE</version>
</dependency>

7、修改我们的 前端页面

导入命名空间

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

修改导航栏,增加认证判断

重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;

9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();

http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
  http.logout().deleteCookies("remove").invalidateHttpSession(true).logoutSuccessUrl("/");

10、我们继续将下面的角色功能块认证完成!

记住我

现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单

1、开启记住我功能

//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
   //开启记住我功能
   http.rememberMe().rememberMeParameter("remember");
}

2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!

思考:如何实现的呢?其实非常简单

我们可以查看浏览器的cookie

3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie

4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个cookie

定制登录页

现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?

在Spring Security中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是/login,也就是说,默认会存在如下请求:
GET http://localohost:8080/login
POST http://localhost:8080/login
如果是GET请求表示你想访问的登录页面,如果是POST请求,表示你想提交登录数据。

1、在刚才的登录页配置后面指定 loginpage请求

// 开启自动配置的登录功能
// 如果未认证访问资源,浏览器跳转到http://localhost:8080/toLogin登录请求
http.formLogin().loginPage("/toLogin") //配置自己的登入页面请求
        .usernameParameter("username")
        .passwordParameter("password")
         // 登陆表单提交请求
        .loginProcessingUrl("/loginMethod");  

注意: 上面定义的loginPage("/toLogin")是登录页面的请求,所以需要对这个请求进行处理,即告诉security这个登录请求,应该访问那个页面

有两种方式,第一种,在Web配置类中添加一个视图控制
在这里插入图片描述
第二种,在controller层添加一个请求跳转
在这里插入图片描述
如果两个都配置优先访问controller中配置的,当然没必要多此一举
在这里插入图片描述

最重要的一步,需要把登录页面请求发行
在这里插入图片描述
在这里插入图片描述

2、然后前端也需要指向我们自己定义的 /tologin请求

<div class="btn-group btn-group-lg" >
    <!--如果未登录-->
     <button type="button" class="btn btn-default"><a th:href="@{/toLogin}">登入</a></button>
</div>

3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html配置提交请求及方式,方式必须为post:

loginPage()源码中的注释上有写明:

<form th:action="@{/loginMethod}" method="post">
            <div class="form-group has-feedback">
                <input type="text" class="form-control" placeholder="Email" name="username">
                <span class="glyphicon glyphicon-envelope form-control-feedback"></span>
            </div>
            <div class="form-group has-feedback">
                <input type="password" class="form-control" placeholder="Password" name="password">
                <span class="glyphicon glyphicon-lock form-control-feedback"></span>
            </div>
            <div class="row">
                <div class="col-xs-8">
                    <div class="checkbox icheck">
                        <label>
                            <input type="checkbox" name="remember"> Remember Me
                        </label>
                    </div>
                </div>
                <!-- /.col -->
                <div class="col-xs-4">
                    <button type="submit" class="btn btn-primary btn-block btn-flat">Sign In</button>
                </div>
                <!-- /.col -->
            </div>
        </form>

4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!

// 开启自动配置的登录功能
// /login?error 重定向到这里表示登录失败
http.formLogin().loginPage("/toLogin") //配置自己的登入页面
        .usernameParameter("username") //接收前端参数username
        .passwordParameter("password") //接收前端参数password
        .loginProcessingUrl("/loginMethod");  // 登陆表单提交请求

5、在登录页增加记住我的多选框

<div class="col-xs-8">
    <div class="checkbox icheck">
        <label>
            <input type="checkbox" name="remember"> Remember Me
        </label>
    </div>
</div>

6、后端验证处理!

//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");

7、测试,OK

完整配置代码

package cn.zysheep.springboot.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * @version v1.0.0
 * @ProjectName: springboot-learning-examples
 * @ClassName: SecurityConfig
 * @Author: 三月三
 */
@EnableWebSecurity //开启WebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 定制请求的授权规则
        http.authorizeRequests().antMatchers("/").permitAll()  // 首页所有人可以访问
                .antMatchers("/level1/**").hasRole("vip1")  // 功能页只有对应的权限可以访问
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        // 开启自动配置的登陆功能,效果,如果没有登陆,没有权限就会来到登陆页面
        // /login?error 重定向到这里表示登录失败
        http.formLogin().loginPage("/toLogin") //配置自己的登入页面
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/loginMethod");  // 登陆表单提交请求

        // 开启自动配置的注销的功能
        // 默认为/logout 注销请求
        http.logout().deleteCookies("remove").invalidateHttpSession(true).logoutSuccessUrl("/");//注销成功以后来到首页

        http.csrf().disable(); // 关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求

        // 开启记住我功能
        http.rememberMe().rememberMeParameter("remember");
        // 登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
        // 点击注销会删除cookie
    }


    // 定义认证规则
    // 在内存中定义,也可以在jdbc中去拿,或者在xml中定义
    // Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        // 要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
        // spring security 官方推荐的是使用bcrypt加密方式。
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("root").password(new BCryptPasswordEncoder().encode("root"))
                .roles("vip1", "vip2", "vip3")
                .and()
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip3");
    }
}

4. 前后端分离开发

问题:

  1. 上面的案例中在认证中的账号root密码root是我们自己构造的,而项目开发中往往会去数据库中读取。
  2. 上面的案例中当我们登录成功后会返回我们一个自定义的视图页面。现在项目比较流行前后端分离开发,前端使用ajax请求后端接口,后端返回接口json数据。

pom

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
   <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.yml

spring:
  thymeleaf:
    cache: false
    encoding: UTF-8
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql:///test_mybaitsplus?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC

logging:
  level:
    cn.zysheep.springbootmybatisplus: info

工具类

/**
 * 全局统一返回结果类
 */
@Data
public class Result<T> {

   //"返回码"
    private Integer code;

    //"返回消息"
    private String message;

    //"返回数据"
    private T data;

    public Result(){}

    public static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }





    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }

    public static<T> Result<T> ok(){
        return Result.ok(null);
    }

    /**
     * 操作成功
     * @param data
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }

    public static<T> Result<T> fail(){
        return Result.fail(null);
    }

    /**
     * 操作失败
     * @param data
     * @param <T>
     * @return
     */
    public static<T> Result<T> fail(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.FAIL);
    }

    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }

    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
}

ResultCodeEnum

/**
 * 统一返回结果状态信息类
 */
@Getter
public enum ResultCodeEnum {



    /* 成功 */
    SUCCESS(200, "成功"),

    LOGOUT_SUCCESS(200, "退出成功"),

    /* 默认失败 */
    FAIL(999, "失败"),

    /* 参数错误:1000~1999 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    /* 业务错误 */
    NO_PERMISSION(3001, "没有权限");


    private Integer code;

    private String message;

    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

业务层实现UserDetailsService

@Service
public class UserServicesImpl extends ServiceImpl<UserMapper, User> implements UserServices, UserDetailsService {


    @Autowired
    private UserMapper userMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        queryWrapper.lambda().eq(User::getUsername,username);

        User user = userMapper.selectOne(queryWrapper);

        if(user == null){
            throw new UsernameNotFoundException("用户不存在");
        }

        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin_user");

        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                                    new BCryptPasswordEncoder().encode(user.getPassword()),grantedAuthorities);
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig  extends WebSecurityConfigurerAdapter{

    @Autowired
    private UserServiceImpl userService;


//    @Autowired
//    private CustomizeAbstractSecurityInterceptor securityInterceptor;
//
//    @Autowired
//    private CustomizeAccessDecisionManager accessDecisionManager;
//
//    @Autowired
//    private CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;



    /**
     * 不经过请求过滤器,忽略的路径,一般是静态资源
     * @param web
     * @throws Exception
     */
//    @Override
//    public void configure(WebSecurity web) throws Exception {
//        super.configure(web);
//    }

    /**
     * 请求认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    /**
     * 请求授权
     * 包括登入登出、记住我、异常处理、会话管理
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();

        // 开启自动配置登录功能
        http.formLogin()
                /**
                 * 登录成功处理逻辑
                 */
                .successHandler((request,response,authentication)->{
            log.info(" authentication.getName():{}", authentication.getName());
                    QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                    userQueryWrapper.lambda().eq(User::getAccount,authentication.getName());
                    User user = userService.getOne(userQueryWrapper);
                    user.setLastLoginTime(LocalDateTime.now());
                    boolean updateById = userService.updateById(user);

                    //此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
                    //进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展


                    response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(Result.ok()));
            out.flush();
            out.close();

        })
        /**
         * 登录失败处理逻辑
         */
        .failureHandler((request,response,e)->{
            Result result = null;

            if (e instanceof AccountExpiredException) {
                //账号过期
                result = Result.build(null,ResultCodeEnum.USER_ACCOUNT_EXPIRED);
            } else if (e instanceof BadCredentialsException) {
                //密码错误
                result = Result.build(null,ResultCodeEnum.USER_CREDENTIALS_ERROR);
            } else if (e instanceof CredentialsExpiredException) {
                //密码过期
                result = Result.build(null,ResultCodeEnum.USER_CREDENTIALS_EXPIRED);
            } else if (e instanceof DisabledException) {
                //账号不可用
                result = Result.build(null,ResultCodeEnum.USER_ACCOUNT_DISABLE);
            } else if (e instanceof LockedException) {
                //账号锁定
                result = Result.build(null,ResultCodeEnum.USER_ACCOUNT_LOCKED);
            } else if (e instanceof InternalAuthenticationServiceException) {
                //用户不存在
                result = Result.build(null,ResultCodeEnum.USER_ACCOUNT_NOT_EXIST);
            }else{
                //其他错误
                result = Result.fail(ResultCodeEnum.FAIL);
            }


            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(result));
            out.flush();
            out.close();
        })
        /**
         *  异常处理(权限拒绝、登录失效等): 未登录就访问资源
         * 
         * 屏蔽Spring Security默认重定向登录页面以实现前后端分离功能
         */
        .and().exceptionHandling()
                //匿名用户访问无权限资源时的异常处理
                .authenticationEntryPoint((request,response,e)->{
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write(new ObjectMapper().writeValueAsString(Result.build(null,ResultCodeEnum.USER_NOT_LOGIN)));
                    out.flush();
                    out.close();

                })
        /**
         *  登出成功处理逻辑
         */
        .and().logout()
        .logoutSuccessHandler((request,response,authentication)->{
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(Result.build(null,ResultCodeEnum.LOGOUT_SUCCESS)));
            out.flush();
            out.close();

        })
        .deleteCookies("JSESSIONID")//登出之后删除cookie
        /**
         * 处理账号被挤下线处理逻辑
         * 会话信息过期策略会话信息过期策略(账号被挤下线)
         */
        .and()
        .sessionManagement()
        .maximumSessions(1)
        .expiredSessionStrategy((sessionInformationExpiredEvent)->{

            HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(Result.fail(ResultCodeEnum.USER_ACCOUNT_USE_BY_OTHERS)));
            out.flush();
            out.close();
        });







//        http.authorizeRequests()
//                //.antMatchers("/sys/user/**").hasAuthority("query_user");
//            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
//                @Override
//                public <O extends FilterSecurityInterceptor> O postProcess(O o) {
//                    o.setAccessDecisionManager(accessDecisionManager);//决策管理器
//                    o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
//                    return o;
//                }
//            });
//
//
//        http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中



    }



    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}


使用PostMan测试登录时必须携带_csrf参数,如果不携带,要在授权中关闭csrf

在这里插入图片描述

详细查看Security前后端分离开发教程

;