源码分析
Spring Security 的核心功能即为:认证(Authentication)、授权(Authorization)
一、概览
1、在 Spring Security 中,用户的认证信息主要由 Authentication 的实现类来保存,
Authentication 接口定义如下:【保存用户认证信息】
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取用户权限
Object getCredentials();//获取用户凭证,一般是密码
Object getDetails();//获取用户携带的详细信息
Object getPrincipal();//获取当前用户
boolean isAuthenticated();//当前用户是否认证成功
void setAuthenticated(boolean isAuthenticated);
}
2、Spring Security 中的认证工作主要由 AuthenticationManager 接口来负责
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) //用于做认证
throws AuthenticationException;
}
AuthenticationManager 最主要的实现类是 ProviderManager,
providerManager里有 List<AuthenticationProvider> providers,用于管理多个authenticationProvider.
在一次完整的认证流程中,可能会同时存在多个 AuthenticationProvider(例如,项目同时
支持 form 表单登录和短信验证码登录),多个 AuthenticationProvider 统一由 ProviderManager
来管理。同时,ProviderManager 具有一个可选的 parent,如果所有的 AuthenticationProvider
都认证失败,那么就会调用 parent 进行认证
3、Spring Security 的授权体系中,有两个关键接口
AccessDecisionManager 是一个决策器,来决定此次访问是否被允许。
AccessDecisionVoter 是一个投票器,投票器会检查用户是否具备应有的角色。
在 AccessDecisionManager中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问。
二、认证
1、核心类
①、AuthenticationManager与Authentication
AuthenticationManager作为整个身份验证核心最外层的封装负责与外部使用者进行交互。
AuthenticationManager接口有且仅有一个对外的服务便是“身份验证”。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
外部使用者通过将身份验证的必要信息,比如用户名和密码封装一个Authentication传递、调用AuthenticationMananger的
authenticate方法。如果没有返回异常和null值,验证服务便是完成。
完成身份验证的Authentication不仅包含用户的身份验证信息,比如用户名,还会将该用户身份下所有对应的权限列表也一并封装返回。
ProviderManager是AuthenticationManager的一个重要实现。
为了向外部提供身份验证服务,Spring Security通过ProviderMananger实现AuthenticationManager的身份验证接口。
【ProviderManager中管理了 多个 AuthenticationProvider】
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。
AuthenticationManager 中可以定义有多个 AuthenticationProvider。
当我们使用 authenticationProvider 元素来定义一个 AuthenticationProvider 时需要实现AuthenticationProvider接口,
如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 【默认会使用 DaoAuthenticationProvider】。
DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息
UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,
我们可以实现自己的 AuthenticationProvider;
如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。
Authentication的职责有两个:
· 第一个是封装验证请求的参数
· 第二个便是封装用户的权限信息
principal用于存放用户的身份标识信息,比如用户名,
credentials用于存放用户的验证凭证比如密码,
authorities用于存放用户的权限列表
details则存放除了用户名和密码其他可能会被用于身份验证的信息,比如应用限定用户的使用ip范围场景下,ip信息可能便会被存放在details做辅
助的验证信息使用。
##下面我们重写一个用户认证逻辑
@Service
public class AuthenticationProviderService implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private SCryptPasswordEncoder sCryptPasswordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
//根据用户名从数据库中获取CustomUserDetails
CustomUserDetails user = userDetailsService.loadUserByUsername(username);
//根据所配置的密码加密算法来分别验证用户密码
switch (user.getUser().getPasswordEncoderType()) {
case BCRYPT:
return checkPassword(user, password, bCryptPasswordEncoder);
case SCRYPT:
return checkPassword(user, password, sCryptPasswordEncoder);
}
throw new BadCredentialsException("Bad credentials");
}
@Override
public boolean supports(Class<?> aClass) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
if (encoder.matches(rawPassword, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
} else {
throw new BadCredentialsException("Bad credentials");
}
}
}
###重写用户查询校验逻辑
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public CustomUserDetails loadUserByUsername(String username) {
//JPA实现
Supplier<UsernameNotFoundException> s =
() -> new UsernameNotFoundException("Username" + username + "is invalid!");
User u = userRepository.findUserByUsername(username).orElseThrow(s);
return new CustomUserDetails(u);
}
}
②、UserDetails
UserDetails 接口来规范开发者自定义的用户对象
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); //返回当前账户所具备的权限
String getPassword();//返回当前账户的密码
String getUsername();//返回当前账户的用户名。
boolean isAccountNonExpired();//返回当前账户是否未过期。
boolean isAccountNonLocked();//返回当前账户是否未锁定。
boolean isCredentialsNonExpired();//返回当前账户凭证(如密码)是否未过期。
boolean isEnabled();//返回当前账户是否可用。
}
③、UserDetailsService
##UserDetailsService 负责提供【用户数据源】的接口.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername 有一个参数是 username,这是用户在认证时传入的用户名,这里拿到用户名之后,再去数据库中查询用户,
最终返回一个 UserDetails 实例。
【一般需要开发者 自行实现 UserDetailsService 的loadUserByUsername方法。如果未实现,走SpringSecurity提供的默认实现】
【默认的实现是InMemoryUserDetailsManager,它继承自UserDetailsManager】
其中JdbcUserDetailsManagerr 实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过 JdbcUserDetailsManager
有一个局限性,就是操作数据库中用户的 SQL都是提前写好的,不够灵活,因此在实际开发中 JdbcUserDetailsManager 使用幵不多。
【它定义了 很多 数据库的字段,都是写死的,对一些已经定义好数据结构的系统不太友好】
针对 UserDetailsService 的自动化配置类是 UserDetailsServiceAutoConfiguration。
UserDetailsServiceAutoConfiguration中
@ConditionalOnMissingBean(
value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class}
)
public class UserDetailsServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean(
type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
)
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(
SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
//.....
}
private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
//....
}
}
可以看出当,系统默认没有实现AuthenticationManager, AuthenticationProvider, UserDetailsService,ClientRegistrationRepository
的时候会默认执行InMemoryUserDetailsManager基于内存的用户实现。
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
private User user = new User();
public User getUser() {
return this.user;
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList<>();
//省略 getter/setter
}
}
默认用户是user,默认密码是 UUID,即控制台 输出的那个UUID。
我们可以通过在配置文件中增加下面的内容,来修改默认的用户名和密码以及角色
spring.security.user.name=admin
spring.security.user.password=admin123
spring.security.user.roles=admin,user
④、DefaultLoginPageGeneratingFilter
DefaultLoginPageGeneratingFilter指定和生成默认登录页面
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
boolean loginError = this.isErrorPage(request); //登录错误
boolean logoutSuccess = this.isLogoutSuccess(request); //登出
if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
chain.doFilter(request, response);
} else {
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess); //生成一个登录页面的html
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
}
}
}
⑤、DefaultLogoutPageGeneratingFilter
DefaultLogoutPageGeneratingFilter指定和生成默认的登出页面
public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain){
if (this.matcher.matches(request)) {
this.renderLogout(request, response); //生成一个登出页面
} else {
filterChain.doFilter(request, response);
}
}
private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
String page = "<!DOCTYPE html>xxxx登出页面xxxx</html>";
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(page);
}
}
⑥、Spring Security的配置类
@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")
.defaultSuccessUrl("/index")
//.successForwardUrl("/index")
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
}
## authorizeRequests()方法表示开启权限配置
## anyRequest().authenticated()表示所有的请求都要认证之后才能访问
## and()方法会返回 HttpSecurityBuilder 对象的一个子类(实际上就是 HttpSecurity)。所以 and()方法相当于又回到 HttpSecurity 实例,
重新开启新 一 轮 的 配 置。
如果觉得and()方法很难理解 , 也可以不用and()方法 ,在.anyRequest().authenticated()配置完成后直接用分号(;)结束,
然后通过 http.formLogin()继续配置表单登录。
## formLogin() 表 示 开 启 表单 登录 配 置
## loginPage 用 来配 置 登录页 面 地址
## loginProcessingUrl 用来配置登录接口地址
## defaultSuccessUrl 和 successForwardUrl 表示登录成功后的跳转地址
defaultSuccessUrl 表示当用户登录成功之后,会自动重定向到登录之前的地址上。
如果用户本身就是直接访问的登录页面,则登录成功后就会重定向到 defaultSuccessUrl 指定的页面中。
successForwardUrl 则不会考虑用户之前的访问地址,只要用户登录成功,就会通过服务器端跳转到 successForwardUrl 所指定的页面。
## failureUrl 表示登录失败后的跳转地址
## usernameParameter 表示登录用户名的参数名称,不设置就会用默认的用户名
## passwordParameter 表示登录密码的参数名称
## permitAll 表示跟登录相关的页面和接口不做拦截,直接通过。
## csrf().disable()表示禁用 CSRF 防御功能
⑦、AuthenticationSuccessHandler 登录成功
Spring Security 中专门提供了 AuthenticationSuccessHandler 接口用来处理【登录成功】事项。
无论是 defaultSuccessUrl 还是 successForwardUrl,最终所配置的都是 AuthenticationSuccessHandler 接口的实例。
6-1、当通过defaultSuccessUrl设置登陆成功后跳转地址时,实际上对应的实现类就是
【SaveRequestAwareAuthenticationSuccessHandler】
public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private RequestCache requestCache = new HttpSessionRequestCache();
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
} else {
String targetUrlParameter = this.getTargetUrlParameter();
if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
this.logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
} else {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
//...
}
###代码分析
1、首先从requestCache中获取缓存下来的请求,如果requestCache为空,表明用户访问登录页面之前没有访问其他页面。
此时直接调通super.onAuthenticationSuccess方法处理,会直接跳转至defaultSuccessUrl指定的地址。
2、targetUrlParameter 是用户显式指定的 登录成功后访问的地址。如果targetUrlParameter存在且 用户设置了
alwaysUseDefaultTargetUrl为TRUE,则重定向到defaultSuccessUrl指定的地址。
3、如果前面条件都不满足,则从requestCache中获取之前缓存下来的请求地址,然后进行重定向。
6-2、当通过successForwardUrl设置登陆成功后跳转地址时,实际上对应的实现类就是
【ForwardAuthenticationSuccessHandler】
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String forwardUrl;
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
##代码分析:
1、代码特别简单就是request,getRequestDispatcher(),一个服务转发。登录成功后直接跳转至susccessForwardUrl指定地址。
6-3、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();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
在自定义的 MyAuthenticationSuccessHandler 中,重写 onAuthenticationSuccess 方法,在该方法中,通过HttpServletResponse
对象返回一段登录成功的 JSON 字符串给前端即可。最后,在 SecurityConfig 中配置自定义的 MyAuthenticationSuccessHandler.
@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")
.successHandler(new MyAuthenticationSuccessHandler()) //登录成功handler
.failureUrl("/login.html")
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
}
⑧、AuthenticationFailureHandler登录失败
上面SecurityConfig.configure配置中failureUrl 表示登录失败后重定向到 mylogin.html 页面.
重定向是一种客户端行为不方便携带失败的异常信息(如果我们要把失败信息显式到登录页面上的话)。
我们可以 使用 .failureForwardUrl("/mylogin.html")来代替.failureUrl("/login.html")。failureForwardUrl跳转是一种
服务端行为。好处是可以携带登录异常信息。
无论是 failureForwardUrl 还是 failureUrl,最终所指向的都是AuthenticationFailureHandler. springSecurity指定
AuthenticationFailureHandler来规范登录失败的实现。
public interface AuthenticationFailureHandler {
void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException ex);
}
##其中 AuthenticationException 表示登录失败的异常信息
AuthenticationFailureHandler共有五个实现类:
1> SimpleUrlAuthenticationFailureHandler 默认的处理逻辑就是通过重定向跳转到登录页面,当然也可以通过配置
forwardToDestination属性将重定向改为服务器端跳转,failureUrl方法的底层实现逻辑就是SimpleUrlAuthenticationFailureHandler。
2> ExceptionMappingAuthenticationFailureHandler 可以实现根据不同的异常类型,映射到不同的路径。
3> ForwardAuthenticationFailureHandler 表示通过服务器端跳转来重新回到登录页面,failureForwardUrl 方法的底层实现逻辑就是
ForwardAuthenticationFailureHandler。
4> AuthenticationEntryPointFailureHandler 是 Spring Security5.2 新引进的处理类,可以通过 AuthenticationEntryPoint 来处理登录异常。
5> DelegatingAuthenticationFailureHandler 可以实现为不同的异常类型配置不同的登录失败处理回调。
如果是前后端分离开发,登录失败时就不需要页面跳转了,只需要返回 JSON 字符串给前端即可,此时可以通过自定义 AuthenticationFailureHandler 的实现类来完成。
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse
resp,AuthenticationException ex) 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());
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(resp);
response.getWriter().write(s);
}
}
##然后在 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")
.failureHandler(new MyAuthenticationFailureHandler()) //登录失败handler
.usernameParameter("uname")
.passwordParameter("passwd")
.permitAll()
.and()
.csrf().disable();
}
}
⑨、logoutSuccessHandler注销登录
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
//省略其他配置
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/mylogin.html")
.and()
.csrf().disable();
}
}
1>、通过logout方法开启注销登录配置
2>、logoutUrl指定了注销登录请求地址,默认是GET请求,路径为/logout
3>、invalidateHttpSession标识是否使Session失效,默认true
4>、clearAuthentication是否清除认证信息,默认true
5>、logoutSuccessUrl标识注销成功后的跳转地址
在浏览器中输入 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()
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout1", "GET"),
new AntPathRequestMatcher("/logout2", "POST")
)
)
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/mylogin.html")
.and()
.csrf().disable();
}
}
上面这个配置表示注销请求路径有两个:
· 第一个是/logout1,请求方法是 GET。
· 第二个是/logout2,请求方法是 POST。
使用任意一个请求都可以完成登录注销。
##如果开发者希望为不同的注销地址返回不同的结果,也是可以的,配置如下:
@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", "GET"),
new AntPathRequestMatcher("/logout2", "POST")
)
)
.invalidateHttpSession(true)
.clearAuthentication(true)
.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "使用 logout1 注销成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(result);
resp.getWriter().write(s);
},new AntPathRequestMatcher("/logout1","GET"))
.defaultLogoutSuccessHandlerFor((req,resp,auth)->{
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "使用 logout2 注销成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(result);
resp.getWriter().write(s);
},new AntPathRequestMatcher("/logout2","POST"))
.and()
.csrf().disable();
}
}
通过 defaultLogoutSuccessHandlerFor 方法可以注册多个不同的注销成功回调函数,该方法第一个参数是注销成功回调,
第二个参数则是具体的注销请求
前后端分离的架构,注销成功后就不需要页面跳转了,只需将注销成功的信息返回给前端即可。
@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", "GET"),
new AntPathRequestMatcher("/logout2", "POST")
)
)
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessHandler((req,resp,auth)->{
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 200);
result.put("msg", "注销成功!");
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(result);
resp.getWriter().write(s);
})
.and()
.csrf().disable();
}
}
2、登录用户数据获取
①、相关对象和概念
登录成功之后,在后续的业务逻辑中,开发者可能还需要获取登录成功的用户对象,如果不使用任何安全管理框架,那么可以将用户信息保存在
HttpSession 中,以后需要的时候直接从 HttpSession 中获取数据。在 Spring Security 中,用户登录信息本质上还是保存在
HttpSession 中,但是为了方便使用,Spring Security 对 HttpSession 中的用户信息进行了封装,封装之后,开发者若再想获取用户登录
数据就会有两种不同的思路:
(1)从 SecurityContextHolder 中获取。
(2)从当前请求对象中获取。
无论是哪种获取方式,都离不开一个重要的对象:Authentication。
在 Spring Security 中,Authentication 对象主要有两方面的功能:
(1)作为 AuthenticationManager 的输入参数,提供用户身份认证的凭证,当它作为一个输入参数时,它的 isAuthenticated 方法返回
false,表示用户还未认证。
(2)代表已经经过身份认证的用户,此时的 Authentication 可以从 SecurityContext 中获取。
②、Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取用户权限
Object getCredentials();//获取用户凭证,一般是密码
Object getDetails();//获取用户的详细信息,可能是当前请求之类
Object getPrincipal();//获取当前用户信息,可以是一个用户名、也可以是一个用户对象
boolean isAuthenticated();//当前用户是否认证成功
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
##Authentication最常用的两个实现类是 UsernamePasswordAuthenticationToken 和 RememberMeAuthenticationToken
③、SecurityContextHolder
我们可以从Security上下文中(SecurityContextHolder)获取Authentication对象:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
## SecurityContextHolder中 存储的是SecurityContext, SecurityContext中存储的才是Authentication。
##源码如下:
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";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty("spring.security.strategy");
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
public SecurityContextHolder() {
}
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static int getInitializeCount() {
return initializeCount;
}
private static void initialize() {
//设置strategy 存储策略,默认使用MODE_THREADLOCAL
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
public static void setStrategyName(String strategyName) {
SecurityContextHolder.strategyName = strategyName;
initialize();
}
public static SecurityContextHolderStrategy getContextHolderStrategy() {
return strategy;
}
public static SecurityContext createEmptyContext() {
return strategy.createEmptyContext();
}
static {
initialize();
}
}
##三种存储策略
(1)MODE_THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中。【默认存储策略】
意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
【ThreadLocalSecurityContextHolderStrategy】
(2)MODE_INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境。存放在 InheritableThreadLocal 中。
如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
【InheritableThreadLocalSecurityContextHolderStrategy】
(3)MODE_GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,
在 JavaWeb 开发中,这种模式很少使用到。
【GlobalSecurityContextHolderStrategy】
④、SecurityContextHolderStrategy存储策略
为了规范存储策略的实现,security定义了 SecurityContextHolderStrategy 接口:
public interface SecurityContextHolderStrategy {
void clearContext(); //用来 清除存储的SecurityContext对象
SecurityContext getContext();//用来 获取存储的SecurityContext对象
void setContext(SecurityContext var1);//用来设置存储的SecurityContext对象
SecurityContext createEmptyContext();//创建一个空的SecurityContext对象
}
如果想在子线程中获取到登录用户信息,可以在启动参数中修改 存储策略:
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
⑤、SecurityContextRepository
将 SecurityContext 存入 HttpSession,或者从 HttpSession 中加载数据并转为 SecurityContext 对象,
这些事情都是由 SecurityContextRepository 接口的实现类完成的
public interface SecurityContextRepository {
//加载SecurityContext对象,没有登录的用户会返回一个没有Authentication的"空"SecurityContext对象。
SecurityContext loadContext(HttpRequestResponseHolder holder);
//保存一个SecurityContext对象
void saveContext(SecurityContext context, HttpServletRequest req, HttpServletResponse resp);
//判断SecurityContext对象是否存在
boolean containsContext(HttpServletRequest req);
}
【HttpSessionSecurityContextRepository是默认的实现类】
⑥、SecurityContextPersistenceFilter
在 Spring Boot 中不同的请求都是由不同的线程处理的,那为什么每一次请求都还能从 SecurityContextHolder 中获取到登录用户信息呢?
这就不得不提到 Spring Security 过滤器链中重要的一环 : SecurityContextPersistenceFilter
从名字上可以看出,SecurityContextPersistenceFilter就是为了【持久化(存储)SercurityContext】而生的。
SecurityContextPersistenceFilter主要做两件事:
1>、当一个请求到来时,从 HttpSession 中获取 SecurityContext 并存入 SecurityContextHolder 中,这样在同一个请求的后续处理过
程中,开发者始终可以通过 SecurityContextHolder获取到当前登录用户信息。
2>、当一个请求处理完毕时,从 SecurityContextHolder中获取 SecurityContext 并存入HttpSession 中(主要针对异步 Servlet),
方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登录用户信息。
【再次存储是为了防止securityContext发生变化】(在程序运行的过程中开发者可能会修改SecurityContext中的Authentication对象)
##拓展:
Servlet规范中,最早又三个和安全管理相关的方法:【HttpServletRequest】
·String getRemoteUser();//获取登录用户名
·boolean isUserInRole(String role);//判断当前登录用户是否具备某个角色
·Principal getUserPrincipal();//获取当前认证主体
从Servlet3.0之后,在上面三个方法的基础上,又增加了三个安全管理相关的方法
·boolean authenticate(HttpServletResponse resp);//判断当前请求是否认证成功
·void login(String username,String password);//执行登录操作
·void logout();//执行注销操作
如果是一个普通的web项目,不使用你和框架,HttpSevletRequest的默认实现类是Tomcat的RequestFacade.
如果使用了Spring security框架,那么我们拿到的httpServletRequest实例是:
【Servlet3SecurityContextHolderAwareRequestWrapper】
因此我们在使用SpringSecurity之后就可以通过HttpServletRequest获取到登录的用户信息啦:
@RequestMapping("/info")
public void info(HttpServletRequest req) {
String remoteUser = req.getRemoteUser();
Authentication auth = ((Authentication) req.getUserPrincipal());
boolean admin = req.isUserInRole("admin");
System.out.println("remoteUser = " + remoteUser);
System.out.println("auth.getName() = " + auth.getName());
System.out.println("admin = " + admin);
}
3、用户定义
用户自定义其实就是使用了UserDetailsService的不同实现类来提供的用户数据。
同时将配置好的UserDetailsService配置给AuthenticationManagerBuilder,
系统再将UserDetailsService提供给AuthenticationProvider使用。
①、基于内存InMemoryUserDetailsManager
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
auth.userDetailsService(manager);
}
InMemoryUserDetailsManager 的实现原理很简单,它间接实现了 UserDetailsService 接口并重写了它里边的 loadUserByUsername
方法,同时它里边维护了一个 HashMap 变量,Map 的key 就是用户名,value 则是用户对象,createUser 就是往这个 Map 中存储数据,
loadUserByUsername 方法则是从该 Map 中读取数据
②、基于数据库JdbcUserDetailsManager
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
if (!manager.userExists("javaboy")) {
manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
}
if (!manager.userExists("sang")) {
manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
}
auth.userDetailsService(manager);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//省略
}
}
//代码解析:
1、首先是创建了一个JdbcUserDetailsManager实例。
2、userExists(username) 方法会根据用户名去数据库里查找对应的 用户,如果不存在 则通过createUser()方法创建一个用户并插入数据库
3、将JdbcUserDetailsManager实例 设置到AuthenticationManagerBuilder中
##源码:
public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {
//初始化数据源
public JdbcUserDetailsManager(DataSource dataSource) {
setDataSource(dataSource);
}
//查询用户
@Override
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
}
//创建用户
@Override
public void createUser(final UserDetails user) {
}
//更新用户
@Override
public void updateUser(final UserDetails user) {
}
//判断用户是否存在
@Override
public boolean userExists(String username) {
}
//....
}
在 JdbcUserDetailsManager 的继承体系中,首先是 JdbcDaoImpl 实现了 UserDetailsService接 口, 并实现 了基 本 的 loadUserByUsername 方 法。
JdbcUserDetailsManager 则 继承 自JdbcDaoImpl,同时完善了数据库操作,又封装了用户的增删改查方法。
③、基于Mybatis
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//省略
}
}
@Slf4j
@Service
public class MyUserDetailsService implements UserDetailsService, Serializable {
@Resource
private UserInfoService userInfoService;
@Resource
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String mobileOrEmail) throws UsernameNotFoundException {
UserInfo userInfo = userInfoService.findUserByMobileOrEmail(mobileOrEmail);
if (StringUtils.isNull(userInfo)) {
log.info("登录用户:{} 不存在.", mobileOrEmail);
throw new BizException("登录用户:" + mobileOrEmail + " 不存在");
} else if (EnableEnum.DELETE.getEnableCode().equals(userInfo.getEnable())) {
log.info("登录用户:{} 已被删除.", mobileOrEmail);
throw new BizException("对不起,您的账号:" + mobileOrEmail + " 已被删除");
} else if (EnableEnum.FORBIDDEN.getEnableCode().equals(userInfo.getEnable())) {
log.info("登录用户:{} 已被停用.", mobileOrEmail);
throw new BizException("对不起,您的账号:" + mobileOrEmail + " 已停用");
}
//验证密码
userInfoService.pwdValidate(userInfo);
//查询权限、返回登录信息
return createLoginUser(userInfo);
}
public UserDetails createLoginUser(UserInfo userInfo) {
return new LoginInfo(userInfo.getUserCode(), userInfo,
permissionService.getMenuPermission(userInfo));
}
}
4、认证流程分析
要搞清楚SpringSecurity的认证流程,我们需要搞清楚对应的三个基本组件:
·AuthenticationManager
·ProviderManager
·AuthenticationProvider
①、认证管理器AuthenticationManager
![26](imgs/springsecurity/26.png)从名称上可以看出,AuthenticationManager 是一个认证管理器。它定义了SpringSecurity如何进行认证操作。
##源码如下:
public interface AuthenticationManager {
//认证操作
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
##解析:
AuthenticationManager 在认证成功后,会返回一个 Authentication对象,这个 Authentication 对象会被设置到
SecurityContextHolder 中。
传入的 Authentication 参数只有用户名/密码等简单的属性,如果认证成功,返回的 Authentication 的属性会得到完全填充,
包括用户所具备的角色信息等。
如果开发者不想用 SpringSecurity 提供的一套认证机制,那么也可以自定义认证流程,认证成功后,手动将 Authentication
存入 SecurityContextHolder 中。
在Spring Security 框架中,默认也是使用【 ProviderManager】。
②、AuthenticationProvider
AuthenticationProvider 就是针对不同的身份类型执行具体的身份认证。
例如,常见的DaoAuthenticationProvider 用来支持用户名/密码登录认证;
RememberMeAuthenticationProvider用来支持“记住我”的认证.
##源码如下:
public interface AuthenticationProvider {
//执行具体的身份认证
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//判断当前的AuthenticationProvider是否支持对应的身份类型
boolean supports(Class<?> authentication);
}
当时用 用户名/密码 登录认证的时候,对应的实现类是DaoAuthenticationProvider
DaoAuthenticationProvider 继 承 自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,
所以具体的认证逻辑在 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中:
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//从当前authentication中获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//根据用户名从缓存中 查询用户详情
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//根据用户名 从 userDetailsService.loadByUsername中获取用户信息
//由 DaoAuthenticationProvider实现
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}catch (UsernameNotFoundException ex) {
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("xxxxxxxxxxxx", "Bad credentials"));
}
}
try {
//进行用户状态校验
this.preAuthenticationChecks.check(user);
//进行密码的操作校验
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//检查密码是否过期
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//返回一个 UsernamePasswordAuthenticationToken 继承自 Authentication
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
③、ProviderManager
ProviderManager 是 AuthenticationManager 的一个重要实现类。
在 Spring Security 中,由于系统可能同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe 认证、
手机号码动态认证等,而不同的认证方式对应了不同的AuthenticationProvider,
所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。
ProviderManager就 管理了 多个 AuthenticationProvider。
ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当
ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,
ProviderManager 的 parent 可以是任意类型的 AuthenticationManager。
ProviderManager 本身也可以有多个,多个 ProviderManager 共用同一个 parent,当存在多
个过滤器链的时候非常有用。当存在多个过滤器链时,不同的路径可能对应不同的认证方式,
但是不同路径可能又会同时存在一些共有的认证方式,这些共有的认证方式可以在 parent 中统
一处理。
##源码如下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
//管理多个AuthenticationProvider 实现类
private List<AuthenticationProvider> providers = Collections.emptyList();
//可以再配一个AuthenticationManager 作为parent
private AuthenticationManager parent;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//循环执行所有的AuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
//该provider是否支持对应的Authentication
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//provider的认证,如DaoAuthenticationProvider的认证 或者RememberMeAuthenticationProvider的认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
//所有的provider的认证方法返回的Authentication为空,且parent不为空
if (result == null && this.parent != null) {
try {
//调用parent的authentication方法
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
④、AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter可以用来处理 任何提交给他的身份认证。
标签:登录,用户,SpringSecurity,Authentication,源码,new,认证,public,第四章
From: https://www.cnblogs.com/pilotspeed/p/17970064