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流程分析
- 客户端(浏览器)发起请求去访问
/hello
接口,这个接口默认是需要认证之后才能访问的。- 这个请求会走一遍spring security中的过滤器链,在最后的
FilterSecurityInterceptor
过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException
异常。- 抛出的
AccessDeniedException
异常在ExceptionTranslationFilter
过滤器中被捕获,ExceptionTranslationFilter
过滤器通过调用LoginUrlAuthenticationEntryPoint#commence
方法给客户端返回302,要求客户端重定向到/login
。- 客户端发送
/login
请求。/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;
}
UserDetailsManager
在UserDetailsService
的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在供5种方法。JdbcDaoImpl
在UserDetailsService
的基础上,通过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;
}
源码可以自行查看,核心方法是
onAuthenticationSuccess
:
SimpleUrlAuthenticationSuccessHandler
继承自AbstractAuthenticationTargetUrlRequestHandler
,通过其中的handle
方法实现请求重定向。SavedRequestAwareAuthenticationSuccessHandler
在SimpleUrlAuthenticationSuccessHandler
的基础上增加了请求缓存的功能,可以记录之前请求的地址,进而在登录成功之后重定向到一开始访问的地址。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;
}
源码可以自行查看,相对来说不是很难:
SimpleUrlAuthenticationFailureHandler
默认的处理逻辑就是通过重定向跳转到登录页面,当然也可以通过配置forwardToDestination
属性将重定向改为服务器端转发,failureUrl
方法的底层实现逻辑就是SimpleUrlAuthenticationFailureHandler
。ExceptionMappingAuthenticationFailureHandler
可以实现根据不同的异常类型映射到不同的路径。ForwardAuthenticationFailureHandler
表示通过服务器端转发来重新回到登录页面,failureForwardUrl
方法的底层实现逻辑就是ForwardAuthenticationFailureHandler
。AuthenticationEntryPointFailureHandler
是spring security 5.2新引进的处理类,可以通过AuthenticationEntryPoint
来处理登录异常。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
中的用户信息进行了封装,封装之后,开发者若再想获取用户登录数据就会有两种不同的思路:
- 从
SecurityContextHolder
中获取。- 从当前请求对象中获取。
另外一种非主流的方式是直接从
HttpSession
中获取。
无论哪种获取方式,都离不开一个重要的对象:Authentication
。在spring security中,Authentication
对象主要有两方面的功能:
- 作为
AuthenticationManager
的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的isAuthenticated
方法返回false
,表示用户还未认证。- 代表已经经过身份认证的用户,此时的
Authentication
可以从SecurityContext
中获取。
一个
Authentication
对象主要包含三个方面的信息:
principal
:定义认证的用户。如果用户使用用户名/密码的方式登录,principal
通常就是一个UserDetails
对象。credentials
:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防止泄露。authorities
:用户被授予的权限信息。
不同的认证方式对应不同的
Authentication
实例,其实现类如图所示:
AbstractAuthenticationToken
:该类实现了Authentication
和CredentialsContainer
两个接口,在AbstractAuthenticationToken
中对Authentication
接口定义的各个数据获取方法进行了实现,CredentialsContainer
则提供了登录凭证擦除方法。一般在登录成功后,为了防止用户信息泄露,可以将登录凭证(例如密码)擦除。RememberMeAuthenticationToken
(最常用):如果用户使用了remember-me的方式登录,登录信息将封装在RememberMeAuthenticationToken
中。TestingAuthenticationToken
:单元测试时封装的用户对象。AnonymousAuthenticationToken
:匿名登录时封装的用户对象。RunAsUserToken
:替换验证身份时封装的用户对象。UsernamePasswordAuthenticationToken
(最常用):表单登录时封装的用户对象。JaasAuthenticationToken
:JAAS认证时封装的用户对象。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
SecurityContextHolder
中存储的是SecurityContext
(通过SecurityContextHolderStrategy
获取),SecurityContext
中存储的则是Authentication
。
SecurityContextHolder
中定义了三种不同的数组存储策略,这实际上是一种典型的策略模式:
MODE_THREADLOCAL
:这种存放策略是将SecurityContext
存放在ThreadLocal
中,其特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合web应用,因为在默认情况下,一个请求无论经过多少Filter
到达Servlet
,都是由一个线程来处理的。这也是SecurityContextHolder
的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中获取登录用户数据,就会获取不到。MODE_INHERITABLETHREADLOCAL
:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。MODE_GLOBAL
:这种存储模式实际上是将数据保存在一个静态变量中,在java web开发中,这种模式很少使用到。Spring security中定义了
SecurityContextHolderStrategy
接口用来规范存储策略中的方法:
public interface SecurityContextHolderStrategy {
// 清除存储的SecurityContext对象
void clearContext();
// 获取存储的SecurityContext对象
SecurityContext getContext();
// 设置存储的SecurityContext对象
void setContext(SecurityContext context);
// 创建一个空的SecurityContext对象
SecurityContext createEmptyContext();
}
其一共有三个实现类,对应了三种不同的存储策略:
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();
}
}
InheritableThreadLocalSecurityContextHolderStrategy
:相对于ThreadLocalSecurityContextHolderStrategy
来说,实现的策略基本一致,不同的是存储数据的载体变了,其变成了InheritableThreadLocal
,继承自ThreadLocal
,但是多了一个特性,就是在子线程创建的一瞬间,会自动将父线程中的数据复制到子线程中。该存储策略正是利用了这一特性,实现了在子线程中获取登录用户信息的功能。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
主要做两件事情:
- 当一个请求到来时,从
HttpSession
中获取SecurityContext
并存入SecurityContextHolder
中,这样在同一个请求的后续处理过程中,开发者始终可以通过SecurityContextHolder
获取到当前登录用户信息。- 当一个请求处理完毕时,从
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);
}
TestSecurityContextRepository
为单元测试提供支持。NullSecurityContextRepository
实现类中,loadContext
方法总是返回一个空的SecurityContext
对象,saveContext
方法未做任何实现,containsContext
总是返回false
,所以NullSecurityContextRepository
实现类实际上未做SecurityContext
的存储工作。HttpSessionSecurityContextRepository
是spring security中默认使用的实现类,实现了将SecurityContext
存储到HttpSession
以及从HttpSession
中加载SecurityContext
出来。
在正式开始介绍
HttpSessionSecurityContextRepository
前,先看下其中定义的关于请求和响应的两个内部类:
首先是定义的关于响应的封装类SaveToSessionResponseWrapper
,其继承关系图:
SaveToSessionResponseWrapper
实际上就是我们所熟知的HttpServletResponse
功能的扩展。有三个关键的实现类:
HttpServletResponseWrapper
:是HttpServletResponse
的装饰类,利用HttpServletResponseWrapper
可以方便地操作参数和输出流等。OnCommittedResponseWrapper
:对HttpServletResponseWrapper
的功能进行了增强,最重要的增强在于可以获取其提交行为。当HttpServletResponse
的sendError
、sendRedirect
、flushBuffer
、flush
以及close
等方法被调用时,onResponseCommitted
方法会被触发,开发者可以在该方法中做一些数据保存操作,例如保存SecurityContext
。不过OnCommittedResponseWrapper
中的onResponseCommitted
只是一个抽象方法,具体的实现在它的实现类SaveContextOnUpdateOrErrorResponseWrapper
中。SaveContextOnUpdateOrErrorResponseWrapper
:对onResponseCommitted
方法做了实现。在该类中声明了一个contextSaved
变量,表示SecurityContext
是否已经存储成功。当HttpServletResponse
提交时,会调用onResponseCommitted
方法,在该方法中调用saveContext
方法,将SecurityContext
保存到HttpSession
中,同时将contextSaved
变量标记为true
。saveContext
也是一个抽象方法,具体的实现在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
中其实主要定义了三个方法:saveContext
、contextChanged
以及createNewSessionIfAllowed
:
saveContext
:该方法主要是用来保存SecurityContext
,如果authentication
对象为null
或者它是一个匿名对象,则不需要保存SecurityContext
;同时,如果httpSession
不为null
并且authBeforeExecution
也不为null
,就从httpSession
中将保存的登录用户数据移除,这个主要是为了防止开发者在注销成功的回调中继续调用chain.doFilter
方法,进而导致原始的登录信息无法清除的问题;如果httpSession
为null
,则去创建一个HttpSession
对象;最后,如果SecurityContext
发生了变化,或者httpSession
中没有保存SecurityContext
,则调用httpSession
中的setAttribute
方法将SecurityContext
保存起来。contextChanged
:该方法主要用来判断SecurityContext
是否发生变化,因为在程序运行过程中,开发者可能修改了SecurityContext
中的Authentication
对象。createNewSessionIfAllowed
:该方法用来创建一个HttpSession
对象。
SaveToSessionResponseWrapper
一个核心的功能就是在HttpServletResponse
提交的时候,将SecurityContext
保存到HttpSession
中。
相对来说,
SaveToSessionRequestWrapper
就要简单很多,源码可以自行查看,其主要作用是禁止在异步servlet提交时,自动保存SecurityContext
(异步servlet使用较少,感兴趣的可以自行了解)。
因为在异步servlet中,当任务执行完毕之后,HttpServletResponse
也会自动提交,在提交的过程中会自动保存SecurityContext
到HttpSession
中,但是由于是在子线程中,因此无法获取到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);
}
Authentication
是Principal
的子类,所以也可以直接在请求参数中放入Principal
来接收当前登录用户的信息。需要注意的是,即使参数是Principal
,真正的实例依然是Authentication
对象。
在spring MVC中,Controller
中方法的参数都是当前请求HttpServletRequest
带来的,因此,Authentication
和Principal
也是这样。而该接口中与安全认证相关的方法,在不同的环境下会有不同的实现。
如果使用了spring security,那么在Controller
参数中拿到的HttpServletRequest
实例将是Servlet3SecurityContextHolderAwareRequestWrapper
,很明显,这是被spring security封装过的请求。
先来看一下相关类的继承关系:
SecurityContextHolderAwareRequestWrapper
类主要实现了servlet 3.0之前和安全管理相关的三个方法,即getRemoteUser
、isUserInRole
以及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);
}
前面直接将
Authentication
或Principal
写到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);
}
}
HttpServletRequest
中getUserPrincipal
方法有了返回值之后,最终在spring MVC的ServletRequestMethodArgumentResolver#resolveArgument(Class<?>, HttpServletRequest)
方法中进行默认参数解析,自动解析出Principal
对象。因此在Controller
中可以通过Principal
和Authentication
对象来接收。
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 {
// 省略
}
}