SpringSecurity(四)【自定义认证流程详解】

四、自定义认证流程详解


4.1 自定义资源权限规则

SpringSecurity(四)【自定义认证流程详解】_第1张图片

  • /index 公共资源
  • /hello 受保护资源

– 在项目中,如果要覆盖默认权限、授权自动配置,需要让 DefaultWebSecurityCondition 这个类失效

SpringSecurity(四)【自定义认证流程详解】_第2张图片

– 添加如下配置可以实现自定义对资源权限规则设置

package com.vinjcent.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index").permitAll()  // 放行资源
                .anyRequest().authenticated()   // 【注意】所有放行的资源放在任何拦截请求前面
                .and()                          // 链式编程,继续拼接
                .formLogin();
    }
}
# 说明
- permitAll()	代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
- anyRequest().authenticated()	代表所有请求必须认证之后才能访问
- formLogin()	代表开启表单认证
# 【注意】放行资源必须放在所有认证请求之前!

4.2 自定义登录界面

根据前面分析可知,校验用户名密码是根据 UsernamePasswordAuthenticationFilter 这个过滤器执行的,要求

  • 用户名参数:username
  • 用户名密码:password
  • 请求类型:POST
  • 请求路径:/login

SpringSecurity(四)【自定义认证流程详解】_第3张图片

修改默认的登录配置信息

  1. 导入thymeleaf依赖
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
    dependencies>
  1. 定义一个跳转到login.html页面的controller
package com.vinjcent.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
    
}
  1. 在 templates 中定义登陆界面
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录title>
head>
<body>
    <h1>用户登录h1>
    <form th:action="@{/login}" method="post">
        用户名: <input type="text" name="uname"> <br>
        密码: <input type="password" name="passwd"> <br>
        <input type="submit" value="登录">
    form>
body>
html>

注意当前login.html页面的文本输入框name参数以及请求路径可根据自定义修改,但必须与 WebSecurityConfiguration 的配置一致

  1. 配置 SpringSecurity 配置类
package com.vinjcent.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()    // 放行登录页面
                .mvcMatchers("/index").permitAll()      // 放行资源
                .anyRequest().authenticated()   // 【注意】所有放行的资源放在任何拦截请求前面
                .and()                          // 链式编程,继续拼接
                .formLogin()
                .loginPage("/toLogin")          // 指定登录页面,【注意】一旦指定登陆页面之后,必须指定登录请求url
                .loginProcessingUrl("/login")   // 指定处理登录请求的url
                .usernameParameter("uname")     // 指定认证用户名参数的名称
                .passwordParameter("passwd")    // 指定认证密码参数的名称
                // .successForwardUrl("/hello")    // 认证成功后跳转的路径(转发),始终在认证成功之后跳转到指定路径
                // .defaultSuccessUrl("/index", true)    // 认证成功之后的跳转(重定向),如果在访问权限资源之前被拦截,那么认证之后将会跳转之前先访问的资源
                .and()
                .csrf().disable();              // 禁止 csrf 跨站请求保护
    }
}

  • successForwardUrldefaultSuccessUrl 这两个方法都可以实现成功之后跳转
  • successForwardUrl 默认使用 dispatchForward 跳转。【注意不会跳转到之前i请求路径
  • defaultSuccessUrl 默认使用 redirect 跳转。【注意如果之前请求路径被拦截,认证之后会优先跳转之前请求路径,可以传入第二个参数进行修改是否跳转之前请求路径,默认为false

4.3 自定义登录成功处理(前后端分离开发解决方案)

有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面(不是交由后端去做,而是前端去做)。此时,我们只需要给前端返回一个 JSON 通知登录成功还是失败。这个时候可以通过自定义接口

  • successHandler 认证成功处理

  • 该处理函数中带有一个 AuthenticationSuccessHandler 接口参数

SpringSecurity(四)【自定义认证流程详解】_第4张图片

  • 根据接口描述信息,得知登陆成功会自动回调这个方法,进一步查看它的实现类可以发现,successForwardUrl、defaultSuccessUrl 也是由它的子类进一步实现的

SpringSecurity(四)【自定义认证流程详解】_第5张图片

自定义 AuthenticationSuccessHandler 实现类

  1. 编写 DivAuthenticationSuccessHandler 实现接口 AuthenticationSuccessHandler
package com.vinjcent.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义认证成功之后处理
 */
public class DivAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg","登陆成功");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=UTF-8");
        String info = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(info);
    }
}

  1. WebSecurityConfiguration 配置类中进行成功认证配置
package com.vinjcent.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())  // 认证成功时处理,前后端分离解决方案
                .and()
                .csrf().disable();
    }
}
  1. 登录测试访问

SpringSecurity(四)【自定义认证流程详解】_第6张图片

4.4 显示登录失败的信息

为了能更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security 在登陆失败之后会将异常信息存储到 requestsession作用域中,key 为 SPRING_SECURITY_LAST_EXCEPTION命名属性

  • SimpleUrlAuthenticationFailureHandler

SpringSecurity(四)【自定义认证流程详解】_第7张图片

SpringSecurity(四)【自定义认证流程详解】_第8张图片

  • 这说明,不同配置会产生作用域不同的异常

验证失败信息

  1. 对应页面修改,显示异常处理
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户登录title>
head>
<body>
    <h2>
        
        
        
        
    h2>
    <h1>用户登录h1>
    <form th:action="@{/login}" method="post">
        用户名: <input type="text" name="uname"> <br>
        密码: <input type="password" name="passwd"> <br>
        <input type="submit" value="登录">
    form>
body>
html>
  1. WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())
                // .failureForwardUrl("/toLogin")      // 认证失败后跳转的路径(转发)
                // .failureUrl("/toLogin")             // 认证失败后跳转的路径(重定向)
                .and()
                .csrf().disable();
    }
}
  1. 任意选择页面的一种情况作用域渲染以及对应后台配置类选择转发/重定向,进行测试

SpringSecurity(四)【自定义认证流程详解】_第9张图片

SpringSecurity(四)【自定义认证流程详解】_第10张图片

4.5 自定义登陆失败处理(前后端分离开发解决方案)

  • failureHandler 认证失败处理

  • 该处理函数中带有一个 AuthenticationFailureHandler 接口参数

SpringSecurity(四)【自定义认证流程详解】_第11张图片

  • 根据接口描述信息,得知登陆失败会自动回调这个方法,进一步查看它的实现类可以发现,successForwardUrl、defaultSuccessUrl 也是由它的子类进一步实现的

SpringSecurity(四)【自定义认证流程详解】_第12张图片

自定义 DivAuthenticationFailureHandler 实现类

  1. 编写 DivAuthenticationFailureHandler 实现接口 AuthenticationFailureHandler
package com.vinjcent.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义认证失败之后处理
 */
public class DivAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登陆失败: " + exception.getMessage());
        result.put("status", 500);
        response.setContentType("application/json;charset=UTF-8");
        String info = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(info);
    }
}
  1. WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;

import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())  // 认证成功时处理,前后端分离解决方案
                // .failureForwardUrl("/toLogin")
                // .failureUrl("/toLogin")
                .failureHandler(new DivAuthenticationFailureHandler())  // 认证失败时处理,前后端分离解决方案
                .and()
                .csrf().disable();
    }
}
  1. 登录测试访问

SpringSecurity(四)【自定义认证流程详解】_第13张图片

4.6 注销登录

SpringSecurity 中也提供了默认的注销登录(访问**/logout**),在开发时可以按照自己的需求对注销进行个性化定制

  1. 开启注销登录(默认开启
package com.vinjcent.config;

import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())
                // .failureForwardUrl("/toLogin")
                // .failureUrl("/toLogin")
                .failureHandler(new DivAuthenticationFailureHandler())
                .and()
                .logout()                       // 注销登录的logout
                .logoutUrl("/logout")           // 指定注销的路径url   【注意】请求方式类型必须是GET
                .invalidateHttpSession(true)    // 默认开启,会话清除
                .clearAuthentication(true)      // 默认开启,清除认证标记
                .logoutSuccessUrl("/toLogin")   // 注销登录成功之后跳转的页面
                .and()
                .csrf().disable();
    }
}
  • 通过 logout() 方法开启注销配置
  • logoutUrl 指定退出登录请求地址,默认为 GET 请求,路径为logout
  • invalidateHttpSession 退出时是否使 session 失效,默认值为 true
  • clearAuthentication 退出时是否清除认证信息,默认值为 true
  • logoutSuccessUrl 退出登录时跳转地址
  1. 配置多个注销登录请求
  • 如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求方法
package com.vinjcent.config;

import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())
                // .failureForwardUrl("/toLogin")
                // .failureUrl("/toLogin")
                .failureHandler(new DivAuthenticationFailureHandler())
                .and()
                .logout()
                // .logoutUrl("/logout")
                .logoutRequestMatcher(new OrRequestMatcher(		// 配置多个注销登录的请求
                        new AntPathRequestMatcher("/aLogout", "GET"),
                        new AntPathRequestMatcher("/bLogout", "POST")
                ))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/toLogin")
                .and()
                .csrf().disable();
    }
}
  1. 添加一个/logout接口
package com.vinjcent.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {

    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }

    @RequestMapping("/toLogout")
    public String toLogout() {
        return "logout";
    }

}
  1. 添加一个logout.html页面(验证POST请求注销)
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>注销登录title>
head>
<body>

<h1>注销登录h1>
<form th:action="@{/bLogout}" method="post">
    <input type="submit" value="注销登录">
form>
body>
html>
  1. 测试访问

SpringSecurity(四)【自定义认证流程详解】_第14张图片

SpringSecurity(四)【自定义认证流程详解】_第15张图片

4.7 注销登录配置(前后端分离解决方案)

  • 前后端分离注销登录配置

如果是前后端分离开发,注销成功后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时可以通过自定义 LogoutSuccessHandler 实现类返回内容注销之后信息

  • logoutSuccessHandler 认证失败处理

  • 该处理函数中带有一个 LogoutSuccessHandler 接口参数

SpringSecurity(四)【自定义认证流程详解】_第16张图片

  • 根据接口描述信息,得知注销登陆成功会自动回调这个方法

SpringSecurity(四)【自定义认证流程详解】_第17张图片

自定义 DivLogoutSuccessHandler 实现类

  1. 编写 DivLogoutSuccessHandler 实现接口 LogoutSuccessHandler
package com.vinjcent.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定义注销成功之后处理
 */
public class DivLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg","注销成功,当前认证对象为:" + authentication);
        result.put("status", 200);
        response.setContentType("application/json;charset=UTF-8");
        String info = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(info);
    }
}

  1. WebSecurityConfiguration 配置类中进行失败认证配置
package com.vinjcent.config;

import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/toLogin")
                .loginProcessingUrl("/login")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                // .successForwardUrl("/hello")
                // .defaultSuccessUrl("/index", true)
                .successHandler(new DivAuthenticationSuccessHandler())
                // .failureForwardUrl("/toLogin")
                // .failureUrl("/toLogin")
                .failureHandler(new DivAuthenticationFailureHandler())
                .and()
                .logout()
                // .logoutUrl("/logout")
                // .logoutRequestMatcher(new OrRequestMatcher(
                //        new AntPathRequestMatcher("/aLogout", "GET"),
                //        new AntPathRequestMatcher("/bLogout", "POST")
                //))
                .logoutSuccessHandler(new DivLogoutSuccessHandler())    // 成功退出登录时,前后端分离解决方案
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/toLogin")
                .and()
                .csrf().disable();
    }
}
  1. 注销登录测试访问

SpringSecurity(四)【自定义认证流程详解】_第18张图片

当前完整所有Security配置信息

package com.vinjcent.config;

import com.vinjcent.handler.DivAuthenticationFailureHandler;
import com.vinjcent.handler.DivAuthenticationSuccessHandler;
import com.vinjcent.handler.DivLogoutSuccessHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;

/**
 *  重写 WebSecurityConfigurerAdapter 类使得默认 DefaultWebSecurityCondition 条件失效
 */
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/toLogin").permitAll()    // 放行登录页面
                .mvcMatchers("/index").permitAll()      // 放行资源
                .anyRequest().authenticated()   // 【注意】所有放行的资源放在任何拦截请求前面
                .and()                          // 链式编程,继续拼接
                .formLogin()
                .loginPage("/toLogin")          // 指定登录页面,【注意】一旦指定登陆页面之后,必须指定登录请求url
                .loginProcessingUrl("/login")   // 指定处理登录请求的url
                .usernameParameter("uname")     // 指定认证用户名参数的名称
                .passwordParameter("passwd")    // 指定认证密码参数的名称
                // .successForwardUrl("/hello")    // 认证成功后跳转的路径(转发),始终在认证成功之后跳转到指定路径
                // .defaultSuccessUrl("/index", true)    // 认证成功之后的跳转(重定向),如果在访问权限资源之前被拦截,那么认证之后将会跳转之前先访问的资源
                .successHandler(new DivAuthenticationSuccessHandler())  // 认证成功时处理,前后端分离解决方案
                // .failureForwardUrl("/toLogin")      // 认证失败后跳转的路径(转发)
                // .failureUrl("/toLogin")             // 认证失败后跳转的路径(重定向)
                .failureHandler(new DivAuthenticationFailureHandler())  // 认证失败时处理,前后端分离解决方案
                .and()
                .logout()                       // 注销登录的logout
                // .logoutUrl("/logout")           // 指定注销的路径url   【注意】请求方式类型必须是GET
                //.logoutRequestMatcher(new OrRequestMatcher(     // 配置多个注销登录的请求
                //        new AntPathRequestMatcher("/aLogout", "GET"),
                //        new AntPathRequestMatcher("/bLogout", "POST")
                //))
                .logoutSuccessHandler(new DivLogoutSuccessHandler())    // 成功退出登录时,前后端分离解决方案
                .invalidateHttpSession(true)    // 默认开启,会话清除
                .clearAuthentication(true)      // 默认开启,清除认证标记
                .logoutSuccessUrl("/toLogin")   // 注销登录成功之后跳转的页面
                .and()
                .csrf().disable();              // 禁止 csrf 跨站请求保护
    }

}

4.8 获取用户认证信息

4.8.1 SecurityContextHolder 的源码解析

SecurityContextHolder

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

  • SecurityContextHolder 中的数据保存默认是通过ThreadLocal来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后, SpringSecurity 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,SpringSecurity 就会先从Session中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security 中的 SecurityContextHolder 中的数据清空。这一策略非常方便用户在Controller、Service层以及任何代码中获取当前登录用户数据

SpringSecurity(四)【自定义认证流程详解】_第19张图片

  • 实际上 SecurityContextHolder 存储的是 SecurityContext,在 SecurityContext 中存储的是 Authentication

SpringSecurity(四)【自定义认证流程详解】_第20张图片

这种设计是典型的策略设计模式

public class SecurityContextHolder {

   public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

   public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";

   public static final String MODE_GLOBAL = "MODE_GLOBAL";

   private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

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

   private static SecurityContextHolderStrategy strategy;

   // ...

   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)) {
         // Set default
         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 to load a custom strategy
      try {
         Class<?> clazz = Class.forName(strategyName);
         Constructor<?> customStrategy = clazz.getConstructor();
         strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
      }
      catch (Exception ex) {
         ReflectionUtils.handleReflectionException(ex);
      }
   }
    // ...
}
  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,由于 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb 开发中,这种模式很少使用到

SecurityContextHolderStrategy

通过 SecurityContextHolderStrategy 得知, SecurityContextHolderStrategy 接口用来定义存储策略方法

SpringSecurity(四)【自定义认证流程详解】_第21张图片

接口中一共定义了四个方法:

  • clearContext:该方法用来清除存储的 SecurityContext 对象
  • getContext:该方法用来获取存储的 SecurityContext 对象
  • setContext:该方法用来设置存储的 SecurityContext 对象
  • createEmptyContext:该方法用来创建一个空的 SecurityContext 对象

在这里插入图片描述

从上面可以看到,每一个实现类对应一种策略的实现

4.8.2 登录用户数据获取

  • 代码中获取认证之后的用户数据
  1. application.yml文件
# 端口号
server:
  port: 3035
# 服务应用名称
spring:
  application:
    name: SpringSecurity02
  # 关闭thymeleaf缓存(用于修改完之后立即生效)
  thymeleaf:
    cache: false
  # 配置登录用户名、密码
  security:
    user:
      name: root
      password: root
      roles:
        - admin
        - user
  1. HelloController
package com.vinjcent.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        System.out.println("Hello Security");
        // 1.获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        System.out.println("身份信息: " + user.getUsername());
        System.out.println("权限信息: " + user.getAuthorities());

        // 模拟子线程获取(默认策略为本地线程,不支持多线程获取)
        new Thread(() -> {
            Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
            System.out.println("子线程获取: " + authentication1);
        }).start();

        return "Hello Security";
    }
}

单线程情况下

SpringSecurity(四)【自定义认证流程详解】_第22张图片

多线程情况下

# 根据下图 SYSTEM_PROPERTY 系统属性配置
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
# MODE_THREADLOCAL
# MODE_INHERITABLETHREADLOCAL
# MODE_GLOBAL
# MODE_PRE_INITIALIZED

SpringSecurity(四)【自定义认证流程详解】_第23张图片

SpringSecurity(四)【自定义认证流程详解】_第24张图片

SpringSecurity(四)【自定义认证流程详解】_第25张图片

  • 从上面可以看出,默认策略(单线程)是无法在子线程中获取用户信息的,如果需要在子线程中获取必须使用第二种策略(多线程),默认策略是通过SYSTEM_PROPERTY加载的,因此我们可以通过增加 VM Options 参数进行修改

4.8.3 页面上获取用户信息

  1. 引入依赖

<dependency>
    <groupId>org.thymeleaf.extrasgroupId>
    <artifactId>thymeleaf-extras-springsecurity5artifactId>
    <version>3.0.4.RELEASEversion>
dependency>
  1. 页面加入命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  1. 页面中使用
<ul>
    <li sec:authentication="principal.username">li>
    <li sec:authentication="principal.authorities">li>
    <li sec:authentication="principal.accountNonExpired">li>
    <li sec:authentication="principal.accountNonLocked">li>
    <li sec:authentication="principal.credentialsNonExpired">li>
ul>

SpringSecurity(四)【自定义认证流程详解】_第26张图片

你可能感兴趣的:(#,Security,spring,boot,java,spring)