2.Spring security认证

Spring security认证

2.1Spring security基本认证
2.1.1快速入门

在spring boot项目中使用spring security非常方便,创建一个新的spring boot项目,只需要引入web和spring security依赖即可:

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

然后在项目中提供一个用于测试的/hello接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello spring security";
    }
}

接下来启动项目,/hello接口就已经被自动保护起来了。当用户访问/hello接口时,会自动跳转到登录页面,用户登录成功后,才能访问到/hello接口。

在这里插入图片描述

默认的登录用户名是user,登录密码则是一个随机生成的UUID字符串,在项目启动日志中可以看到登录密码(这也意味着项目每次启动时,密码都会发生变化)。
输入默认的用户名和密码,就可以成功登录了。这就是spring security的强大之处,只需要引入一个依赖,所有的接口就会被自动保护起来。

2.1.2流程分析

在这里插入图片描述

  1. 客户端(浏览器)发起请求去访问/hello接口,这个接口默认是需要认证之后才能访问的。
  2. 这个请求会走一遍spring security中的过滤器链,在最后的FilterSecurityInterceptor过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException异常。
  3. 抛出的AccessDeniedException异常在ExceptionTranslationFilter过滤器中被捕获,ExceptionTranslationFilter过滤器通过调用LoginUrlAuthenticationEntryPoint#commence方法给客户端返回302,要求客户端重定向到/login
  4. 客户端发送/login请求。
  5. /login请求被DefaultLoginPageGeneratingFilter过滤器拦截下来,并在该过滤器中返回登录页面。所以当用户访问/hello接口时会首先看到登录页面。

在整个过程中,相当于客户端一共发送了两个请求,第一个请求是/hello,服务端收到之后返回302,要求客户端重定向到/login,于是客户端又发送了/login请求。

2.1.3原理分析
2.1.3.1默认用户生成

Spring security中定义了UserDetails接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已经固定的系统集成到spring security认证体系中:

/**
 * 提供核心的用户信息
 */
public interface UserDetails extends Serializable {
    // 返回当前账户所具备的权限
    Collection<? extends GrantedAuthority> getAuthorities();

    // 返回当前账户的密码
    String getPassword();

    // 返回当前账户的用户名
    String getUsername();

    // 返回当前账户是否未过期
    boolean isAccountNonExpired();

    // 返回当前账户是否未锁定
    boolean isAccountNonLocked();

    // 返回当前账户凭证(如密码)是否未过期
    boolean isCredentialsNonExpired();

    // 返回当前账户是否可用
    boolean isEnabled();
}

负责提供用户数据源的接口UserDetailsService

/**
 * 加载用户特定数据的核心接口
 */
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在这里插入图片描述

  • UserDetailsManagerUserDetailsService的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在供5种方法。
  • JdbcDaoImplUserDetailsService的基础上,通过spring-jdbc实现了从数据库查询用户的方法。
  • InMemoryUserDetailsManager实现了UserDetailsManager中关于用户的增删改查方法,不过都是基于内存的操作,数据并没有持久化。
  • JdbcUserDetailsManager继承自JdbcDaoImpl同时又实现了UserDetailsManager接口,因此可以通过JdbcUserDetailsManager实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过JdbcUserDetailsManager有一个局限性,就是操作数据库中用户的SQL都是提前写好的,不够灵活,因此在实际开发中使用并不多。
  • CachingUserDetailsService的特点是会将UserDetailsService缓存起来。
  • UserDetailsServiceDelegator则是提供了UserDetailsService的懒加载功能。
  • ReactiveUserDetailsServiceAdapter是webflux-web-security模块定义的UserDetailsService实现。

当使用spring security时,如果仅仅只是引入一个spring security依赖,则默认使用的用户就是由InMemoryUserDetailsManager提供的。
针对UserDetailsService的自动化配置类是UserDetailsServiceAutoConfiguration

@Configuration(proxyBeanMethods = false)
// 当前classpath下存在AuthenticationManager类
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
// 当前项目中,系统没有提供AuthenticationManager、AuthenticationProvider、UserDetailsService、
// AuthenticationManagerResolver类的实例
@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },
		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
				"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {

	private static final String NOOP_PASSWORD_PREFIX = "{noop}";

	private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

	private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

	// 默认情况下满足条件,因此spring security会提供一个InMemoryUserDetailsManager实例
	@Bean
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
		// 从SecurityProperties.User类中,就可以看到默认的用户名是user,默认的密码是一个UUID字符串
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		// 此处的User是spring security提供了一个实现了UserDetails接口的用户类
		return new InMemoryUserDetailsManager(
				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
						.roles(StringUtils.toStringArray(roles)).build());
	}

	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
		String password = user.getPassword();
		if (user.isPasswordGenerated()) {
			logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
		}
		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
			return password;
		}

		return NOOP_PASSWORD_PREFIX + password;
	}
}
2.1.3.2默认页面生成

和页面相关的过滤器:DefaultLoginPageGeneratingFilter(用来生成默认的登录页面)和DefaultLogoutPageGeneratingFilter(用来生成默认的注销页面,通过访问/logout接口可以看到)。
先来看DefaultLoginPageGeneratingFilter。作为spring security过滤器链中的一员,在第一次请求/hello接口的时候,就会经过它,但是由于/hello接口与登录无关,因此DefaultLoginPageGeneratingFilter过滤器并未干涉/hello接口。等到第二次重定向到/login页面的时候,就和DefaultLoginPageGeneratingFilter有关系了。此时请求就会在该过滤器中进行处理,生成登录页面返回给客户端。

public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		boolean loginError = isErrorPage(request);
		boolean logoutSuccess = isLogoutSuccess(request);
	
		// 判断是否为登录出错请求、注销成功请求或者登录请求
		if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
			// 如果是的话,则字符串拼接成登录页面并响应
			String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
			response.setContentType("text/html;charset=UTF-8");
			response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
			response.getWriter().write(loginPageHtml);
			// 用return方法跳出过滤器链
			return;
		}

		// 否则,请求继续往下走,执行下一个过滤器
		chain.doFilter(request, response);
	}
}

DefaultLogoutPageGeneratingFilter原理类似,可以自行查看源码。

2.2登录表单配置
2.2.1快速入门

创建项目并引入相关依赖,接下来在resources/static目录下创建一个login.html页面,即自定义的登录页面(仅列出核心代码):

<form id="login-form" class="form" action="/doLogin" method="post">
	<h3 class="text-center text-info">登录</h3>
	<div class="form-group">
		<label for="username" class="text-info">用户名:</label><br>
		<input type="text" name="uname" id="username" class="form-control">
	</div>
	<div class="form-group">
		<label for="password" class="text-info">密码:</label><br>
		<input type="text" name="passwd" id="password" class="form-control">
	</div>
	<div class="form-group">
		<input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
	</div>
</form>

接下来定义两个测试接口,作为受保护的资源。当用户登录成功后才可以访问:

@RestController
public class LoginController {
    @RequestMapping("/index")
    public String index() {
        return "Login success";
    }

    @RequestMapping("/hello")
    public String hello() {
        return "Hello spring security";
    }
}

最后再提供一个spring security的配置类:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * configure方法中是一个链式配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表示开启权限配置
        http.authorizeRequests()
                // 表示所有的请求都要认证之后才能访问
                .anyRequest().authenticated()
                // and方法会返回HttpSecurityBuilder对象的一个子类(实际上就是HttpSecurity),所以and方法相当于又回到了HttpSecurity实例,
                // 并且重新开始新一轮的配置
                .and()
                // 表示开启表单登录配置
                .formLogin()
                // 配置登录页面地址
                .loginPage("/login.html")
                // 配置登录接口地址
                .loginProcessingUrl("/doLogin")
                // 登录成功后的跳转地址,通过SavedRequestAwareAuthenticationSuccessHandler实现
                .defaultSuccessUrl("/index")
                // 也可以实现登录成功后的跳转,通过ForwardAuthenticationSuccessHandler实现
                // .successForwardUrl("/index")
                // 登录失败后的跳转地址
                .failureUrl("/login.html")
                // 登录用户名的参数名称
                .usernameParameter("uname")
                // 登录密码的参数名称
                .passwordParameter("passwd")
                // 表示与登录相关的页面和接口不做拦截,直接通过
                .permitAll()
                .and()
                // 表示禁用CSRF防御功能,方便测试,后面会介绍
                .csrf().disable();
    }
}
2.2.2配置细节
2.2.2.1登录成功

Spring security中专门提供了AuthenticationSuccessHandler接口用来处理登录成功事项:

/**
 * 用于处理成功的用户身份验证的策略
 */
public interface AuthenticationSuccessHandler {
    /**
     * 当用户成功通过身份验证时调用(默认方法)。5.2版本加入,在处理特定的认证请求Authentication Filter中会用到
     */
    default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authentication) throws IOException, ServletException {
		onAuthenticationSuccess(request, response, authentication);
		chain.doFilter(request, response);
	}

	/**
     * 当用户成功通过身份验证时调用
     */
    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upfMtI1k-1642578644528)(en-resource://database/709:1)]@w=900

源码可以自行查看,核心方法是onAuthenticationSuccess

  1. SimpleUrlAuthenticationSuccessHandler继承自AbstractAuthenticationTargetUrlRequestHandler,通过其中的handle方法实现请求重定向。
  2. SavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationSuccessHandler的基础上增加了请求缓存的功能,可以记录之前请求的地址,进而在登录成功之后重定向到一开始访问的地址。
  3. ForwardAuthenticationSuccessHandler的实现则比较容易,就是一个服务器转发。

开发者也可以配置自己的SavedRequestAwareAuthenticationSuccessHandler

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                // 配置自己的SavedRequestAwareAuthenticationSuccessHandler
                .successHandler(successHandler())
                .failureUrl("/login.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }

    SavedRequestAwareAuthenticationSuccessHandler successHandler() {
        SavedRequestAwareAuthenticationSuccessHandler handler =
                new SavedRequestAwareAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/index");
        // 注意在配置时指定了targetUrlParameter为target,这样用户就可以在登录请求中,
        // 通过target来指定跳转地址了,需要修改一下相应的login.html中的form表单
        handler.setTargetUrlParameter("target");
        return handler;
    }
}
<form id="login-form" class="form" action="/doLogin?target=/hello" method="post">
	<h3 class="text-center text-info">登录</h3>
	<div class="form-group">
		<label for="username" class="text-info">用户名:</label><br>
		<input type="text" name="uname" id="username" class="form-control">
	</div>
	<div class="form-group">
		<label for="password" class="text-info">密码:</label><br>
		<input type="text" name="passwd" id="password" class="form-control">
	</div>
	<div class="form-group">
		<input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
	</div>
</form>

AuthenticationSuccessHandler默认的三个实现类,无论是哪一个,都是用来处理页面跳转的。有时候,页面跳转并不能满足需求,特别是前后端分离开发中,用户登录成功后,就不再需要页面跳转了,只需要给前端返回一个JSON数据即可,告诉前端登录成功还是失败,前端收到消息后自行处理。可以通过自定义AuthenticationSuccessHandler的实现类来处理:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 200);
        resp.put("msg", "登录成功!");
        ObjectMapper om = new ObjectMapper();
        response.getWriter().write(om.writeValueAsString(resp));
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().
                anyRequest().authenticated()
                .and()
                .formLogin()
                // ...
                .successHandler(new MyAuthenticationSuccessHandler())   // 使用自定义的AuthenticationSuccessHandler,在前后端分离的场景中给前端返回JSON数据
                // ...
    }
}
2.2.2.2登录失败

接下来看登录失败的处理逻辑。为了方便在前端页面展示登录失败的异常信息,首先需要引入thymeleaf依赖:

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

然后在resources/templates目录下新建mylogin.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9C9C9C;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登录</h3>
                        <!-- 动态展示登录失败时候的异常信息 -->
                        <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用户名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密码:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登录">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>

由于mylogin.html是动态页面,因此不能像静态页面那样直接访问了,需要给其提供一个访问控制器:

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

最后再在SecurityConfig中配置登录页面:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                // 表示重定向到指定页面,由于重定向是一种客户端跳转,因此不方便携带请求失败的异常信息
                // .failureUrl("/mylogin.html")
                // 如果想要携带相关信息,需要使用服务器端跳转
                .failureForwardUrl("/mylogin.html")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .csrf().disable();
    }
}

无论是failureUrl还是failureForwardUrl,最终所配置的都是AuthenticationFailureHandler接口的实现,其用来规范登录失败的实现:

/**
 * 用于处理身份验证失败尝试的策略
 */
public interface AuthenticationFailureHandler {
    void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QtwBxacu-1642578644528)(en-resource://database/710:1)]

源码可以自行查看,相对来说不是很难:

  1. SimpleUrlAuthenticationFailureHandler默认的处理逻辑就是通过重定向跳转到登录页面,当然也可以通过配置forwardToDestination属性将重定向改为服务器端转发,failureUrl方法的底层实现逻辑就是SimpleUrlAuthenticationFailureHandler
  2. ExceptionMappingAuthenticationFailureHandler可以实现根据不同的异常类型映射到不同的路径。
  3. ForwardAuthenticationFailureHandler表示通过服务器端转发来重新回到登录页面,failureForwardUrl方法的底层实现逻辑就是ForwardAuthenticationFailureHandler
  4. AuthenticationEntryPointFailureHandler是spring security 5.2新引进的处理类,可以通过AuthenticationEntryPoint来处理登录异常。
  5. DelegatingAuthenticationFailureHandler可以实现为不同的异常类型配置不同的登录失败处理回调。

如果是前后端分离开发,登录失败时就不需要页面跳转了,只需要返回JSON字符串给前端即可,此时可以通过自定义AuthenticationFailureHandler的实现类来完成:

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map<String, Object> resp = new HashMap<>();
        resp.put("status", 500);
        resp.put("msg", "登录失败! " + exception.getMessage());
        response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().
                anyRequest().authenticated()
                .and()
                .formLogin()
                // ...
                .failureHandler(new MyAuthenticationFailureHandler())   // 使用自定义的AuthenticationFailureHandler,在前后端分离的场景中给前端返回JSON数据
                // ...
    }
}
2.2.2.3注销登录

Spring security中提供了默认的注销页面,当然开发者也可以根据自己的需求对注销登录进行定制:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                // 省略其他配置
                .and()
                // 开启注销登录配置
                .logout()
                // 注销登录请求地址,默认是get请求,路径为/logout
                .logoutUrl("/logout")
                // 是否使session失效,默认为true
                .invalidateHttpSession(true)
                // 是否清除认证信息,默认为true
                .clearAuthentication(true)
                // 表示注销登录后的跳转地址
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
    }
}

配置完成后,再次启动项目,登录成功后,在浏览器中输入http://localhost:8080/logout就可以发起注销登录请求了。注销成功后,会自动跳转到mylogin.html页面。
如果项目有需要,开发者也可以配置多个注销登录的请求,同时还可以指定请求的方法:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                // 省略其他配置
                .and()
                .logout()
                // 配置多个注销登录的请求,同时指定请求的方法(必须大写,或者使用spring定义的枚举类)
                .logoutRequestMatcher(
                        new OrRequestMatcher(
                        		// 注销请求路径有两个:第一个是/logout1,请求方法是GET;第二个是/logout2,请求方法是POST
                                new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()), 
                                new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
                        )
                )
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/mylogin.html")
                .and()
                .csrf().disable();
}

如果是前后端分离的架构,注销成功后就不需要页面跳转了,只需将注销成功的信息返回给前端即可,此时可以自定义返回内容:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                // 省略其他配置
                .and()
                .logout()
                .logoutRequestMatcher(
                        new OrRequestMatcher(
                                new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()), 
                                new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
                        )
                )
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessHandler((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    Map<String, Object> resp = new HashMap<>();
                    resp.put("status", 200);
                    resp.put("msg", "注销成功!");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
                })
                .and()
                .csrf().disable();
    }
}

如果开发者希望为不同的注销地址返回不同的结果,也是可以的:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                // 省略其他配置
                .and()
                .logout()
                .logoutRequestMatcher(
                        new OrRequestMatcher(
                                new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()), 
                                new AntPathRequestMatcher("/logout2", HttpMethod.POST.name())
                        )
                )
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                // 通过defaultLogoutSuccessHandlerFor方法可以注册多个不同的注销成功回调函数
                .defaultLogoutSuccessHandlerFor((request, response, authentication) -> {    
                        response.setContentType("application/json;charset=utf-8");
                        Map<String, Object> resp = new HashMap<>();
                        resp.put("status", 200);
                        resp.put("msg", "使用logout1注销成功!");
                        response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
                }, new AntPathRequestMatcher("/logout1", HttpMethod.GET.name()))
                .defaultLogoutSuccessHandlerFor((request, response, authentication) -> {
                        response.setContentType("application/json;charset=utf-8");
                        Map<String, Object> resp = new HashMap<>();
                        resp.put("status", 200);
                        resp.put("msg", "使用logout2注销成功!");
                        response.getWriter().write(new ObjectMapper().writeValueAsString(resp));
                }, new AntPathRequestMatcher("/logout2", HttpMethod.POST.name()))
                .and()
                .csrf().disable();
    }
}
2.3登录用户数据获取

在spring security中,用户登录信息本质上还是保存在HttpSession中,但是为了方便使用,spring security对HttpSession中的用户信息进行了封装,封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:

  1. SecurityContextHolder中获取。
  2. 从当前请求对象中获取。

另外一种非主流的方式是直接从HttpSession中获取。
无论哪种获取方式,都离不开一个重要的对象:Authentication。在spring security中,Authentication对象主要有两方面的功能:

  1. 作为AuthenticationManager的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthenticated方法返回false,表示用户还未认证。
  2. 代表已经经过身份认证的用户,此时的Authentication可以从SecurityContext中获取。

一个Authentication对象主要包含三个方面的信息:

  1. principal:定义认证的用户。如果用户使用用户名/密码的方式登录,principal通常就是一个UserDetails对象。
  2. credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。
  3. authorities:用户被授予的权限信息。

不同的认证方式对应不同的Authentication实例,其实现类如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzS0nwRj-1642578644529)(en-resource://database/711:1)]@w=900

  1. AbstractAuthenticationToken:该类实现了AuthenticationCredentialsContainer两个接口,在AbstractAuthenticationToken中对Authentication接口定义的各个数据获取方法进行了实现,CredentialsContainer则提供了登录凭证擦除方法。一般在登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。
  2. RememberMeAuthenticationToken(最常用):如果用户使用了remember-me的方式登录,登录信息将封装在RememberMeAuthenticationToken中。
  3. TestingAuthenticationToken:单元测试时封装的用户对象。
  4. AnonymousAuthenticationToken:匿名登录时封装的用户对象。
  5. RunAsUserToken:替换验证身份时封装的用户对象。
  6. UsernamePasswordAuthenticationToken(最常用):表单登录时封装的用户对象。
  7. JaasAuthenticationToken:JAAS认证时封装的用户对象。
  8. PreAuthenticatedAuthenticationToken:Pre-Authentication场景下封装的用户对象。
2.3.1从SecurityContextHolder中获取
@RestController
public class UserController {
    @GetMapping("/user")
    public void userInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        System.out.println("name = " + name);
        System.out.println("authorities = " + authorities);
    }
}
2.3.1.1SecurityContextHolder

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzXy4P3R-1642578644530)(en-resource://database/712:1)]@w=700

SecurityContextHolder中存储的是SecurityContext(通过SecurityContextHolderStrategy获取),SecurityContext中存储的则是Authentication
SecurityContextHolder中定义了三种不同的数组存储策略,这实际上是一种典型的策略模式:

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

Spring security中定义了SecurityContextHolderStrategy接口用来规范存储策略中的方法:

public interface SecurityContextHolderStrategy {
    // 清除存储的SecurityContext对象
    void clearContext();

    // 获取存储的SecurityContext对象
    SecurityContext getContext();

    // 设置存储的SecurityContext对象
    void setContext(SecurityContext context);

    // 创建一个空的SecurityContext对象
    SecurityContext createEmptyContext();
}

其一共有三个实现类,对应了三种不同的存储策略:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q5JqZ7Vb-1642578644530)(en-resource://database/713:1)]

  1. ThreadLocalSecurityContextHolderStrategy
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    // 存储数据的载体,所以针对SecurityContext的各种操作都是在ThreadLocal中进行操作
    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();
    }

}
  1. InheritableThreadLocalSecurityContextHolderStrategy:相对于ThreadLocalSecurityContextHolderStrategy来说,实现的策略基本一致,不同的是存储数据的载体变了,其变成了InheritableThreadLocal,继承自ThreadLocal,但是多了一个特性,就是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。该存储策略正是利用了这一特性,实现了在子线程中获取登录用户信息的功能。
  2. GlobalSecurityContextHolderStrategy:使用静态变量保存SecurityContext,在web开发中使用得较少。

最后再来看一下SecurityContextHolder的源码(仅列出核心部分):

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

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

	// 默认的存储策略是通过System.getProperty加载的,因此可以通过配置系统变量来修改默认的存储策略,
	// 例如idea中配置VM options参数
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	private static SecurityContextHolderStrategy strategy;

	private static int initializeCount = 0;

	static {
		initialize();
	}

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

	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}
}

从源码中可以看到, SecurityContextHolder定义了三个静态常量来描述三种不同的存储策略;存储策略strategy会在静态代码块中进行初始化,根据不同的strategyName初始化不同的存储策略;strategyName变量表示目前正在使用的存储策略,开发者可以通过配置系统变量或者调用setStrategyName来修改存储策略,调用setStrategyName后会重新初始化strategy

SecurityContextHolder默认是将用户信息存储在ThreadLocal中,在spring boot中,不同的请求都是由不同的线程处理的,之所以每一次请求都能从SecurityContextHolder中获取到登录用户信息,其依赖于spring security过滤器链中重要的一环——SecurityContextPersistenceFilter

2.3.1.2SecurityContextPersistenceFilter

默认情况下,在spring security过滤器链中,SecurityContextPersistenceFilter是第二道防线,位于WebAsyncManagerIntegretionFilter之后,其作用是为了存储SecurityContext而设计的。
整体来说,SecurityContextPersistenceFilter主要做两件事情:

  1. 当一个请求到来时,从HttpSession中获取SecurityContext并存入SecurityContextHolder中,这样在同一个请求的后续处理过程中,开发者始终可以通过SecurityContextHolder获取到当前登录用户信息。
  2. 当一个请求处理完毕时,从SecurityContextHolder中获取SecurityContext并存入HttpSession中(主要针对异步servlet),方便下一个请求到来时,再从HttpSession中拿出来使用,同时擦除SecurityContextHolder中的登录用户信息。

需要注意的是,在SecurityContextPersistenceFilter过滤器中,当一个请求处理完毕时,从SecurityContextHolder中获取SecurityContext存入HttpSession中,这一步的操作主要是针对异步servlet。如果不是异步servlet,在响应提交时,就会将SecurityContext保存到HttpSession中了,而不会等到在SecurityContextPersistenceFilter过滤器中再去存储。

SecurityContext存入HttpSession,或者从HttpSession中加载数据并转为SecurityContext对象,这些事情都是由SecurityContextRepository接口的实现类完成的:

public interface SecurityContextRepository {
	/**
	 * 加载SecurityContext出来,对于没有登录的用户,这里会返回一个空的SecurityContext对象。
	 * 注意,空的SecurityContext对象是指其中不存在Authentication对象,而不是该方法返回null
	 */
	SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

	// 保存一个SecurityContext对象
	void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);

	// 判断SecurityContext对象是否存在
	boolean containsContext(HttpServletRequest request);
}

在这里插入图片描述

  1. TestSecurityContextRepository为单元测试提供支持。
  2. NullSecurityContextRepository实现类中,loadContext方法总是返回一个空的SecurityContext对象,saveContext方法未做任何实现,containsContext总是返回false,所以NullSecurityContextRepository实现类实际上未做SecurityContext的存储工作。
  3. HttpSessionSecurityContextRepository是spring security中默认使用的实现类,实现了将SecurityContext存储到HttpSession以及从HttpSession中加载SecurityContext出来。

在正式开始介绍HttpSessionSecurityContextRepository前,先看下其中定义的关于请求和响应的两个内部类:
首先是定义的关于响应的封装类SaveToSessionResponseWrapper,其继承关系图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98OhiSpk-1642578644531)(en-resource://database/715:1)]@w=500

SaveToSessionResponseWrapper实际上就是我们所熟知的HttpServletResponse功能的扩展。有三个关键的实现类:

  1. HttpServletResponseWrapper:是HttpServletResponse的装饰类,利用HttpServletResponseWrapper可以方便地操作参数和输出流等。
  2. OnCommittedResponseWrapper:对HttpServletResponseWrapper的功能进行了增强,最重要的增强在于可以获取其提交行为。当HttpServletResponsesendErrorsendRedirectflushBufferflush以及close等方法被调用时,onResponseCommitted方法会被触发,开发者可以在该方法中做一些数据保存操作,例如保存SecurityContext。不过OnCommittedResponseWrapper中的onResponseCommitted只是一个抽象方法,具体的实现在它的实现类SaveContextOnUpdateOrErrorResponseWrapper中。
  3. SaveContextOnUpdateOrErrorResponseWrapper:对onResponseCommitted方法做了实现。在该类中声明了一个contextSaved变量,表示SecurityContext是否已经存储成功。当HttpServletResponse提交时,会调用onResponseCommitted方法,在该方法中调用saveContext方法,将SecurityContext保存到HttpSession中,同时将contextSaved变量标记为truesaveContext也是一个抽象方法,具体的实现在SaveToSeesionResponseWrapper中。

接下来看一下SaveToSessionResponseWrapper的定义:

final class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper {
	private final HttpServletRequest request;
	private final boolean httpSessionExistedAtStartOfRequest;
	private final SecurityContext contextBeforeExecution;
	private final Authentication authBeforeExecution;

	SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request,
				boolean httpSessionExistedAtStartOfRequest, SecurityContext context) {
		super(response, HttpSessionSecurityContextRepository.this.disableUrlRewriting);
		this.request = request;
		this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
		this.contextBeforeExecution = context;
		this.authBeforeExecution = context.getAuthentication();
	}

	@Override
	protected void saveContext(SecurityContext context) {
		final Authentication authentication = context.getAuthentication();
		HttpSession httpSession = this.request.getSession(false);
		String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey;
		// 如果authentication为null或者它是一个匿名对象,则不需要保存SecurityContext
		// See SEC-776
		if (authentication == null
				|| HttpSessionSecurityContextRepository.this.trustResolver.isAnonymous(authentication)) {
			if (httpSession != null && this.authBeforeExecution != null) {
				// SEC-1587 A non-anonymous context may still be in the session
				// SEC-1735 remove if the contextBeforeExecution was not anonymous
				httpSession.removeAttribute(springSecurityContextKey);
				this.isSaveContextInvoked = true;
			}
			
			return;
		}
		httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication);
		// If HttpSession exists, store current SecurityContext but only if it has
		// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
		if (httpSession != null) {
			// We may have a new session, so check also whether the context attribute
			// is set SEC-1561
			if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
				httpSession.setAttribute(springSecurityContextKey, context);
				this.isSaveContextInvoked = true;
			}
		}
	}

	private boolean contextChanged(SecurityContext context) {
		return this.isSaveContextInvoked || context != this.contextBeforeExecution
				|| context.getAuthentication() != this.authBeforeExecution;
	}

	private HttpSession createNewSessionIfAllowed(SecurityContext context, Authentication authentication) {
		if (isTransientAuthentication(authentication)) {
			return null;
		}
		if (this.httpSessionExistedAtStartOfRequest) {
			return null;
		}
		if (!HttpSessionSecurityContextRepository.this.allowSessionCreation) {
			return null;
		}
		// Generate a HttpSession only if we need to
		if (HttpSessionSecurityContextRepository.this.contextObject.equals(context)) {
			return null;
		}
		try {
			HttpSession session = this.request.getSession(true);
			return session;
		}
		catch (IllegalStateException ex) {
			// Response must already be committed, therefore can't create a new
			// session
			this.logger.warn("Failed to create a session, as response has been committed. "
					+ "Unable to store SecurityContext.");
		}
		return null;
	}
}

SaveToSessionResponseWrapper中其实主要定义了三个方法:saveContextcontextChanged以及createNewSessionIfAllowed

  1. saveContext:该方法主要是用来保存SecurityContext,如果authentication对象为null或者它是一个匿名对象,则不需要保存SecurityContext;同时,如果httpSession不为null并且authBeforeExecution也不为null,就从httpSession中将保存的登录用户数据移除,这个主要是为了防止开发者在注销成功的回调中继续调用chain.doFilter方法,进而导致原始的登录信息无法清除的问题;如果httpSessionnull,则去创建一个HttpSession对象;最后,如果SecurityContext发生了变化,或者httpSession中没有保存SecurityContext,则调用httpSession中的setAttribute方法将SecurityContext保存起来。
  2. contextChanged:该方法主要用来判断SecurityContext是否发生变化,因为在程序运行过程中,开发者可能修改了SecurityContext中的Authentication对象。
  3. createNewSessionIfAllowed:该方法用来创建一个HttpSession对象。

SaveToSessionResponseWrapper一个核心的功能就是在HttpServletResponse提交的时候,将SecurityContext保存到HttpSession中。

相对来说,SaveToSessionRequestWrapper就要简单很多,源码可以自行查看,其主要作用是禁止在异步servlet提交时,自动保存SecurityContext(异步servlet使用较少,感兴趣的可以自行了解)。
因为在异步servlet中,当任务执行完毕之后,HttpServletResponse也会自动提交,在提交的过程中会自动保存SecurityContextHttpSession中,但是由于是在子线程中,因此无法获取到SecurityContext对象(默认存储在ThreadLocal中),所以会保存失败。如果开发者使用了异步servlet,则默认情况下会禁用HttpServletResponse提交时自动保存SecurityContext这一功能,改为在SecurityContextPersistenceFilter过滤器中完成SecurityContext保存操作。

接下来整体看一下HttpSessionSecurityContextRepository类的功能:

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
	// 定义SecurityContext在HttpSession中存储的key,可以通过该key来手动操作HttpSession中存储的SecurityContext
	public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
	private final Object contextObject = SecurityContextHolder.createEmptyContext();
	private boolean allowSessionCreation = true;
	private boolean disableUrlRewriting = false;
	private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;
	// 用户身份评估器,用来判断当前用户是匿名用户还是remember-me登录的用户
	private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

	@Override
	public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
		HttpServletRequest request = requestResponseHolder.getRequest();
		HttpServletResponse response = requestResponseHolder.getResponse();
		HttpSession httpSession = request.getSession(false);
		// 获取SecurityContext对象
		SecurityContext context = readSecurityContextFromSession(httpSession);
		if (context == null) {
			// 如果获取到的对象为null,则生成一个空的SecurityContext对象
			context = generateNewContext();
		}
		
		// 最后构造请求和响应的装饰类并存入requestResponseHolder
		SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request,
				httpSession != null, context);
		requestResponseHolder.setResponse(wrappedResponse);
		requestResponseHolder.setRequest(new SaveToSessionRequestWrapper(request, wrappedResponse));
		return context;
	}

	/**
	 * 用来保存SecurityContext。正常情况下,在HttpServletResponse提交时SecurityContext就已经保存
	 * 到HttpSession中了;如果是异步servlet,则提交时不会自动将SecurityContext保存到HttpSession,
	 * 此时会在这里进行保存操作
	 */
	@Override
	public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
		SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
				SaveContextOnUpdateOrErrorResponseWrapper.class);
		responseWrapper.saveContext(context);
	}

	/**
	 * 判断请求中是否存在SecurityContext对象
	 */
	@Override
	public boolean containsContext(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if (session == null) {
			return false;
		}
		return session.getAttribute(this.springSecurityContextKey) != null;
	}

	/**
	 * 执行具体的SecurityContext读取逻辑
	 */
	private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
		if (httpSession == null) {
			return null;
		}
		// Session exists, so try to obtain a context from it.
		Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
		if (contextFromSession == null) {
			return null;
		}

		// We now have the security context object from the session.
		if (!(contextFromSession instanceof SecurityContext)) {
			return null;
		}

		// Everything OK. The only non-null return from this method.
		return (SecurityContext) contextFromSession;
	}

	protected SecurityContext generateNewContext() {
		return SecurityContextHolder.createEmptyContext();
	}

	/**
	 * 设置是否允许创建HttpSession,默认是true
	 */
	public void setAllowSessionCreation(boolean allowSessionCreation) {
		this.allowSessionCreation = allowSessionCreation;
	}

	/**
	 * 是否禁用URL重写,默认是false
	 */
	public void setDisableUrlRewriting(boolean disableUrlRewriting) {
		this.disableUrlRewriting = disableUrlRewriting;
	}

	public void setSpringSecurityContextKey(String springSecurityContextKey) {
		this.springSecurityContextKey = springSecurityContextKey;
	}

	/**
	 * 用来判断Authentication是否免于存储
	 */
	private boolean isTransientAuthentication(Authentication authentication) {
		return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null;
	}

	public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
		this.trustResolver = trustResolver;
	}
}

HttpSessionSecurityContextRepository中提供的所有功能都将在SecurityContextPersistenceFilter过滤器中进行调用:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    // 确保请求只执行一次该过滤器
    if (request.getAttribute(FILTER_APPLIED) != null) {
        chain.doFilter(request, response);
        return;
    }

    request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

    // 表示是否要在过滤器链执行之前确保会话有效,由于这是一个比较耗费资源的操作,因此默认值为false
    if (this.forceEagerSessionCreation) {
        // 过滤器链执行之前确保会话有效
        HttpSession session = request.getSession();
        if (this.logger.isDebugEnabled() && session.isNew()) {
            this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
        }
    }
    HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
    // 加载SecurityContext实例,如果没有则创建,具体看实现方法
    SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
    try {
        // 将SecurityContext保存在SecurityContextHolder中方便后续使用
        SecurityContextHolder.setContext(contextBeforeChainExecution);
        
        // 使请求继续往下走,但是要注意,此时传递的请求和响应对象是在HttpSessionSecurityContextRepository
        // 中封装后的对象,即SaveToSessionRequestWrapper和SaveToSessionResponseWrapper的实例
        chain.doFilter(holder.getRequest(), holder.getResponse());
    }
    finally {
        // 当请求处理完毕后,在finally块中,获取最新的SecurityContext对象(开发者可能在后续处理中
        // 修改了SecurityContext中的Authentication对象)
        SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        // 然后清空SecurityContextHolder中的数据
        SecurityContextHolder.clearContext();
        // 保存SecurityContext
        this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
        // 最后,从request中移除FILTER_APPLIED属性
        request.removeAttribute(FILTER_APPLIED);
    }
}

总的来说,就是请求在到达SecurityContextPersistenceFilter过滤器之后,先从HttpSession中读取SecurityContext出来,并存入SecurityContextHolder之中以备后续使用;当请求离开SecurityContextPersistenceFilter过滤器的时候,获取最新的SecurityContext并存入HttpSession中,同时清空SecurityContextHolder中的登录用户信息。

2.3.2从当前请求对象中获取
// 从当前请求对象中获取
@RequestMapping("/authentication")
public void authentication(Authentication authentication) {
    System.out.println("authentication = " + authentication);
}

// 真正的实例依然是Authentication的实例
@RequestMapping("/principal")
public void principal(Principal principal) {
    System.out.println("principal = " + principal);
}

AuthenticationPrincipal的子类,所以也可以直接在请求参数中放入Principal来接收当前登录用户的信息。需要注意的是,即使参数是Principal,真正的实例依然是Authentication对象。
在spring MVC中,Controller中方法的参数都是当前请求HttpServletRequest带来的,因此,AuthenticationPrincipal也是这样。而该接口中与安全认证相关的方法,在不同的环境下会有不同的实现。
如果使用了spring security,那么在Controller参数中拿到的HttpServletRequest实例将是Servlet3SecurityContextHolderAwareRequestWrapper,很明显,这是被spring security封装过的请求。
先来看一下相关类的继承关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nrf6NDDG-1642578644532)(en-resource://database/716:1)]@w=500

SecurityContextHolderAwareRequestWrapper类主要实现了servlet 3.0之前和安全管理相关的三个方法,即getRemoteUserisUserInRole以及getUserPrincipal。而servlet 3.0中新增的三个安全管理相关的方法则在Servlet3SecurityContextHolderAwareRequestWrapper类中实现。获取用户登录信息主要和前面三个方法有关,源码相对来说比较简单,可以自行查看。
因此,在使用了spring security之后,通过HttpServletRequest就可以获取到很多当前登录用户信息了:

@RequestMapping("/info")
public void info(HttpServletRequest request) {
    String remoteUser = request.getRemoteUser();    // 返回当前登录用户的用户名
    Authentication auth = (Authentication) request.getUserPrincipal();  // 返回当前登录的用户对象,其实就是Authentication的实例
    boolean admin = request.isUserInRole("admin");  // 判断当前用户是否具备某一个指定角色的功能
    System.out.println("remoteUser = " + remoteUser);
    System.out.println("auth.getName() = " + auth.getName());
    System.out.println("admin = " + admin);
}

前面直接将AuthenticationPrincipal写到Controller参数中,实际上就是spring MVC框架从Servlet3SecurityContextHolderAwareRequestWrapper中提取的用户信息。而其中就涉及到另一个重要的过滤器——SecurityContextHolderAwareRequestFilter。该过滤器的主要作用就是对HttpServletRequest请求进行再包装,重写其中的和安全管理相关的方法。HttpServletRequest在整个请求过程中会被包装多次,每一次的包装都会给它增添新的功能。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    // 调用requestFactory.create对请求重新进行包装,里边直接创建了一个Servlet3SecurityContextHolderAwareRequestWrapper的实例,
    // 对请求的HttpServletRequest进行包装之后,接下来在过滤器链中传递的HttpServletRequest对象,它的getRemoteUser、isUserInRole
    // 以及getUserPrincipal方法就可以直接使用了
    chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res);
}

private HttpServletRequestFactory createServlet3Factory(String rolePrefix) {
    HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix);
    factory.setTrustResolver(this.trustResolver);
    factory.setAuthenticationEntryPoint(this.authenticationEntryPoint);
    factory.setAuthenticationManager(this.authenticationManager);
    factory.setLogoutHandlers(this.logoutHandlers);
    return factory;
}

final class HttpServlet3RequestFactory implements HttpServletRequestFactory {
    public HttpServletRequest create(HttpServletRequest request, HttpServletResponse response) {
		return new Servlet3SecurityContextHolderAwareRequestWrapper(request, this.rolePrefix, response);
	}
}

HttpServletRequestgetUserPrincipal方法有了返回值之后,最终在spring MVC的ServletRequestMethodArgumentResolver#resolveArgument(Class<?>, HttpServletRequest)方法中进行默认参数解析,自动解析出Principal对象。因此在Controller中可以通过PrincipalAuthentication对象来接收。

2.4用户定义

Spring security支持多种用户定义方式,自定义用户其实就是使用UserDetailsService的不同实现类来提供用户数据,同时将配置好的UserDetailsService配置给AuthenticationManagerBuilder,系统再将UserDetailsService提供给AuthenticationProvider使用。

2.4.3基于myBatis(目前主流)
public class User implements UserDetails {
    // ...
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();

        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }

        return authorities;
    }
    // ...
}

public class Role {
    private Integer id;
    private String name;
    private String nameZh;
    // ...
}

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);

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

        user.setRoles(userMapper.getRolesByUid(user.getId()));

        return user;
    }
}

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 基于mybatis
        auth.userDetailsService(myUserDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 省略
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; 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; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public JwtTokenProvider jwtTokenProvider() { return new JwtTokenProvider(); } @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/api/**").authenticated() .anyRequest().permitAll(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } }请根据上述代码构建一个JwtTokenFilter类,让上述代码不报错
05-25
可以按照以下方式编写JwtTokenFilter类: ``` import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserDetailsService userDetailsService; public JwtTokenFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) { this.jwtTokenProvider = jwtTokenProvider; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { UserDetails userDetails = userDetailsService.loadUserByUsername(jwtTokenProvider.getUsername(token)); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } } ``` 在此JwtTokenFilter类中,我们注入了JwtTokenProvider和UserDetailsService,其中JwtTokenProvider用于解析和验证JWT令牌,UserDetailsService用于获取用户详细信息。在doFilterInternal方法中,我们首先从请求中获取JWT令牌并验证其是否有效。如果令牌有效,我们使用UserDetailsService加载用户详细信息,并创建一个UsernamePasswordAuthenticationToken对象,将其设置为安全上下文的认证对象。最后,我们调用filterChain.doFilter方法继续处理请求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值