知道Spring-Security认证原理,为多种登录方式,多种认证做准备。
Spring-Security认证主要组成成员:
1)认证过滤器抽象类 AbstractAuthenticationProcessingFilter
UsernamePasswordAuthenticationFilter为其默认实现,拦截登录路径。
2)封装的认证实体抽象类 AbstractAuthenticationToken
UsernamePasswordAuthenticationToken为其默认实现,封装输入的用户名密码。
3)认证器接口 AuthenticationProvider
具体的方法认证规则,需实现。
4)认证器管理接口 AuthenticationManager
ProviderManager为其默认实现。
5)登录成功处理器接口 AuthenticationSuccessHandler
需实现
6)登录失败处理器接口 AuthenticationFailureHandler
需实现
7)security配置抽象类 WebSecurityConfigurerAdapter
需实现
1)AbstractAuthenticationProcessingFilter,Spring Security 默认认证过滤器,处于拦截器链当中,AbstractAuthenticationProcessingFilter中的 doFilter()方法中调用了其抽象方法this.attemptAuthentication(request, response),该方法各实现类中具体实现
AbstractAuthenticationProcessingFilter源码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
...
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
...
authResult = this.attemptAuthentication(request, response);
...
}
...
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
}
...
protected AuthenticationManager getAuthenticationManager() {
return this.authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}
2) UsernamePasswordAuthenticationFilter 为过滤器的默认实现,可以看出里面构造方法指定了默认拦截地址 /login。attemptAuthentication()实现方法中,可以看到方法实现是从request里取得用户名密码,最后构建成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的 authenticate 方法传给认证器管理器去认证。
UsernamePasswordAuthenticationFilter 实现的源码:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
...
}
3)UsernamePasswordAuthenticationToken 默认的认证封装实现,认证器管理器遍历执行认证器的时候可根据传入的实体判断改使用哪个认证器。过滤器,认证实体,认证器最好一一对应。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
...
}
4)authenticationManager是AbstractAuthenticationProcessingFilter的一个成员变量,从上面可以看出,这个参数是必须赋值的,采用默认的过滤器认证,spring security会默认给一个实现类ProviderManager。认证管理器中的认证方法会遍历所有的认证器,根据认证器中provider.supports(toTest)方法判断是否改执行认证器的认证方法,这个 toTest 参数就是过滤器传进来的 UsernamePasswordAuthenticationToken
ProviderManager源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
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;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
...
}
...
}
5)认证器接口AuthenticationProvider中有两个方法,一个是根据认证封装的实体具体认证,一个是根据封装的实体判断在认证管理器中是否执行该认证器。
AuthenticationProvider源码:
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
以一个简单的实现类为例:
@Slf4j
public class JsonAuthenticationProvider implements AuthenticationProvider {
@Autowired
private JdbcUserDetailsServiceImpl jdbcUserDetailsService; //UserDetailsService的实现
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginUsername = authentication.getName();
String loginPassword = (String) authentication.getCredentials();
//具体的认证规则
CloudUserDetailsImpl webUserDetail = (CloudUserDetailsImpl)
jdbcUserDetailsService.loadUserByUsername(loginUsername);
return new UsernamePasswordAuthenticationToken(webUserDetail, webUserDetail.getPassword(),
webUserDetail.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
6)看过滤器源码,认证结束后,会根据认证成功或失败,分别调用两个成功失败处理器
this.successHandler.onAuthenticationSuccess(request, response, authResult);
this.failureHandler.onAuthenticationFailure(request, response, failed);
因此,我们可以自定义这两个处理器,来自己处理认证成功失败
public class SimpleLoginHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
}
}
7)Spring Security的配置文件要继承 WebSecurityConfigurerAdapter
@Slf4j
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/", "/webjars/**", "/**/*.js", "/**/*.jpeg",
"/**/*.css", "/**/*.jpg", "/**/*.gif", "/**/*.png", "/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁止匿名用户访问系统
http.anonymous().disable();
//只允许iframe同源引用
http.headers().frameOptions().sameOrigin();
//禁止csrf
http.csrf().disable();
/*
* session 管理 SessionAuthenticationStrategy接口的多个实现
* 参考 : https://silentwu.iteye.com/blog/2213956
* .sessionFixation().changeSessionId() 登录时修改session id 避免会话标识未更新 的漏洞
* .maximumSessions(5) 最大允许同一用户同时创建5个会话
*
*/
http.sessionManagement().sessionFixation().changeSessionId()
.maximumSessions(5).sessionRegistry(new SessionRegistryImpl());
http.httpBasic();
//配置登录的过滤器
http.addFilterAfter(ldapAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
//任何路径都必须认证过才能访问
http.authorizeRequests()
.antMatchers("/ldap/login").permitAll()
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager());
http.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
http.logout()
.logoutUrl("/auth/logout")
.invalidateHttpSession(true)
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(200);
var obj = ResultObject.success("注销成功");
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(obj));
});
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//添加LDAP登录认证器
auth.authenticationProvider(ldapAuthenticationProvider());
}
private AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new CloudDynamicRoleVoter());
return new UnanimousBased(decisionVoters);
}
//登录结果处理器注册
@Bean
public SimpleLoginHandler authenticationHandler() {
return new SimpleLoginHandler();
}
/**
* 过滤器和认证器注册
*/
@Bean
public LdapAuthenticationProvider ldapAuthenticationProvider(){
return new LdapAuthenticationProvider();
}
@Bean
public Filter ldapAuthenticationProcessingFilter() throws Exception {
var filter = new LdapAuthenticationProcessingFilter();
filter.setAuthenticationFailureHandler(authenticationHandler());
filter.setAuthenticationSuccessHandler(authenticationHandler());
filter.setAuthenticationManager(super.authenticationManager());
filter.setFilterProcessesUrl("/ldap/login");
filter.setPostOnly(false);
return filter;
}
注意,认证器中会使用@Autowired注入其他的属性,必须使用@Bean注册认证器才能注入进来,否者为null。
最后我们总结一下配置流程,要实现一个登录认证,首先要自定义一个过滤器 AbstractAuthenticationProcessingFilter,注入一个认证管理器 AuthenticationManager,然后需要绑定一个AbstractAuthenticationToken,注册一个认证处理器 AuthenticationProvider,要想实现多种方式认证可复制参考默认的UsernamePassword的过滤器封装的认证实体即可。如果使用默认认证过滤器,则只需要自定义认证处理器进行认证即可。
参考连接:
https://blog.csdn.net/qq_36521507/article/details/103365805
https://blog.csdn.net/qq_36521507/article/details/103370070