SpringSecurity

SpringSecurity学习

  • SpringSecurity
    • 一、配置SpringSecurity
      • 1.配置登录方式
    • 二、登录方式的实现
    • 三、设置密码加密规则
      • 自定义加密规则
    • 四、自定义SpringSecurity的登录页面以及登录请求
      • 自定义登录页面:
    • 五、前后端分离项目中使用使用SpringSecurity登录
      • 自定登录成功返回Json字符串
      • 自定义登录失败返回Json字符串
    • 六、获取用户登录信息
    • 七、Spring Security登录流程解析
    • 八、自定义过滤器
    • 九、RememberMe功能
    • 十、登录认证源码详细分析

SpringSecurity

记录SpringSecurity学习和使用过程中的一些问题和知识点。

一、配置SpringSecurity

配置SpringSecurity需要自定义类继承WebConfigSecurityAdapter类,并重写要配置的方法。

1.配置登录方式

//标识这是一个配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

我们通过重写 configure(HttpSecurity http)方法来修改登录方式和需要登录的请求。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// 重写这个方法中的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 这个方法主要是用来配置登录相关信息,比如登录方式,需要登录才能访问的请求等。
        http.httpBasic().and().authorizeRequests().anyRequest().authenticated();
    }
}

重写上面的方法后,我们重新访问系统接口,出现如下登录界面(HttpBasic登录):
SpringSecurity_第1张图片
再次修改这个方法,配置为使用表单登录(formLogin):

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// 重写这个方法中的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 这个方法主要是用来配置登录相关信息,比如登录方式,需要登录才能访问的请求等。
        http.formLogin().and().authorizeRequests().anyRequest().authenticated();
    }
}

配置后重启项目访问的接口,出现如下登录界面(form表单登录):
SpringSecurity_第2张图片

二、登录方式的实现

要实习登录,需要继承UserDetailsService接口,并实现接口中唯一的方法public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException,这个方法能接受一个用户名参数,我们通过这个用户名参数去查询数据库或其他地方存储的用户数据,并封装成一个UserDetails对象返回。

// 加入Spring容器管理
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里的用户名是,前台登录时用户输入的用户名,拿到这个用户名后,我们需要通过dao查询出对应的用户对象
        //userDao.getUserByUsername(username);
        if (username.equals("xiaoming") ) {
        	// 这里的用户信息是重数据库中查出来的用户信息,并不是自己设置的,if里面就是一些拿到用户名过后查询数据库的逻辑,真正需要的就是封装后返回的对象
            User user = new User();
            user.setUsername("xiaoming");
            user.setPassword("123456");
            user.setUserId(1L);
            // 构建一个User对象返回,注意这里的User对象是Spring security提供的,他实现了UserDetails接口
            // 在构建密码参数时要指定加密类型(前提是自己没有配置加密方式的时候),{noop}+密码,{}中的值就是加密方式,这里noop就是没有加密方式
            return new org.springframework.security.core.userdetails.User(user.getUsername(), "{noop}” + user.getPassword(), AuthorityUtils.createAuthorityList("admin"));
        }
        // 如果拿到用户名,没有找到用户或者不满足登录要求可以返回null
        return null;
    }
}

我们注意到,自定义的UserDetailsService要加入到Spring容器中,这样就能使用我们自定义的UserDetailsService做登录相关操作了。当然也可以自己配置要使用的UserDetailsService:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	// 重写这个类configure(AuthenticationManagerBuilder auth),通过auth的userDetailsService来设置我们自己定义的UserDetailsService实现类。
        auth.userDetailsService(new MyUserDetailsService());
    }
}

三、设置密码加密规则

SpringSecuri提供了许多加密方式,我们在自定义UserDetailsService中的loadUserByUsername方法中返回的return new org.springframework.security.core.userdetails.User(user.getUsername(), "{noop}” + user.getPassword(), AuthorityUtils.createAuthorityList(“admin”));这里password字段钱的**{noop}就是用来指定加密规则的。如果不指定加密规则,就会抛出*There is no PasswordEncoder mapped for the id “null”***这错误 。如果没有加密规则,我们需要在返回UserDetails中指定密码没有使用加密规则:{noop},即

 @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里的用户名是,前台登录时用户输入的用户名,拿到这个用户名后,我们需要通过dao查询出对应的用户对象
        //userDao.getUserByUsername(username);
        log.info(username + "登录系统");
        // ....逻辑
        // 设置用户密码时指定密码不使用加密
            return User(user.getUsername(), "{noop}" + user.getPassword(), 		AuthorityUtils.createAuthorityList("admin"));
        }
        return null;
    }

spring Security中提供的默认的密码加密规则还有:

bcrypt - BCryptPasswordEncoder (Also used for encoding)
ldap - LdapShaPasswordEncoder
MD4 - Md4PasswordEncoder
MD5 - new MessageDigestPasswordEncoder("MD5")
noop - NoOpPasswordEncoder
pbkdf2 - Pbkdf2PasswordEncoder
scrypt - SCryptPasswordEncoder
SHA-1 - new MessageDigestPasswordEncoder("SHA-1")
SHA-256 - new MessageDigestPasswordEncoder("SHA-256")
sha256 - StandardPasswordEncoder
--------------------- 
作者:王小雷-多面手 
来源:CSDN 
原文:https://blog.csdn.net/dream_an/article/details/79381459 

我们可以在用户密码部分使用这些加密规则,在登陆时,Spring Security后把用户提交的密码项进行指定加密规则的一个加密,然后再与数据库中的密码进行比对,所有在保存用户密码的时候,一定要保存对应的加密后的内容。

自定义加密规则

不使用Spring Security提供的加密方式,我们可以自定义加密规则,自定加密规则要实现Spring Security提供的PasswordEncoder接口,并实现里面的encode和matchs方法 ,这两个分别用于加密、用户密码比对。

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        // charSequence是密码项,我们要在这个方法中为这个密码设置加密规则
        return charSequence.toString();
    }
    @Override
    public boolean matches(CharSequence charSequence, String s) {
    	// charSequence是用户输入密码 s是存储的密码
        return charSequence.toString().equals(s);
    }
}

设置好加密方式后,我们需要应用这个加密方式到框架中:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	// 配置类中重写configure(AuthenticationManagerBuilder auth)方法通过auth.passwordEncoder(new MyPasswordEncoder());来设置我们的自定义加密方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.passwordEncoder(new MyPasswordEncoder());
    }
}

由于我们已经指定过加密方式了,我们就不能在UserDetailsService中再指定了
上面我们重写configure(AuthenticationManagerBuilder auth)方法来设置我们需要的加密规则,同样的,我们也可以把我们定义的加密规则加入Spring 容器即可。

private PasswordEncoder getPasswordEncoder() {
            if (this.passwordEncoder != null) {
                return this.passwordEncoder;
            } else {
            	// 从容器中获取根据类型PasswordEncoder获取
                PasswordEncoder passwordEncoder = (PasswordEncoder)this.getBeanOrNull(PasswordEncoder.class);
                if (passwordEncoder == null) {
                    passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
                }

                this.passwordEncoder = passwordEncoder;
                return passwordEncoder;
            }
        }

四、自定义SpringSecurity的登录页面以及登录请求

SpringSecurity提供默认的登录页面在实际项目中是完全不符合要求的,这就需要我们自定义一个登录界面来替换默认页面:

自定义登录页面:

在WebSecurityConfigurerAdapter实现类的protected void configure(HttpSecurity http) throws Exception方法中,我们配置自定义的登录页面的信息:


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
        			// 设置登录页面请求 -> 映射/loginpage.html这个请求,在Springboot项目中,在main/src/resources/static下新建一个loginpage.html页面,这个页面将会作为我们的登录页面
                .loginPage("/loginpage.html")
                // 修改登录请求映射地址
                .loginProcessingUrl("/login")
                .and().
                authorizeRequests()
                // 设置好登录页面时,我们要配置允许登录页面被访问,否则登录页面也需要登录就会出错了...
                .antMatchers("/loginpage.html").permitAll().
                anyRequest().
                authenticated()
                // 关闭csrf防护
                .and().csrf().disable();
    }

上面的配置就能实现我们自定义登录页面需求了。我们可以在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() {
    	// 这个请求的请求路径为/login,请求方式为POST
        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);
        }
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

从上面的源码中我们也能看到,接收表单请求的参数名分别是username和password,所以在我们自定义的页面中也需要提供这两个请求参数。

五、前后端分离项目中使用使用SpringSecurity登录

SpringSecurity默认登录方式是页面跳转(重定向),但是在前后端分离项目中,登录成功或失败都应该返回json字符串。

自定登录成功返回Json字符串

我们需要修改SpringSecurity登录成功后的默认行为,首先要自定义类来实现AuthenticationSuccessHandler接口,并且实现 public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
方法,在这个方法中,我们来处理登录成功后的行为:

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
    // authentication这个对象封装了登录用户的一些信息
    // 修改默认行为页面跳转为 通过response响应json数据
        httpServletResponse.setHeader("Content-Type", "application/json;characterEncoding=utf8");
        httpServletResponse.getWriter().write(        objectMapper.writeValueAsString(authentication));
    }
}

自定义成功处理后要进行配置:

 protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/loginpage.html")
                .loginProcessingUrl("/login")
                // 设置成功处理器
                .successHandler(successHandler)
                .and().
                authorizeRequests()
                .antMatchers("/loginpage.html").permitAll().
                anyRequest().
                authenticated()
                .and().csrf().disable();
    }

打印登录信息authentication:

{
	authorities:  [
	 {
		authority: "admin"
		}
	],
	details: {
		remoteAddress: "0:0:0:0:0:0:0:1",
		sessionId: "828BF85C0966A6FBD785ECA5A6827C49"
	},
	authenticated: true,
	principal: {
		password: null,
		username: "xiaoming",
		authorities: [
			{
				authority: "admin"
			}
		],
		accountNonExpired: true,
		accountNonLocked: true,
		credentialsNonExpired: true,
		enabled: true
	},
	credentials: null,
	name: "xiaoming"
}

自定义登录失败返回Json字符串

如果是登录失败的情况,我们需要自定义类实现AuthenticationFailureHandler接口,并实现onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException 方法:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    	// 登录失败的处理,通过Response返回, e所有异常信息
        httpServletResponse.setHeader("Content-Type", "application/json;characterEncoding=utf8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(e));
    }
}


自定义失败处理后要进行配置:

 protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/loginpage.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                // 配置失败处理器
                .failureHandler(failureHandler)
                .and().
                authorizeRequests()
                .antMatchers("/loginpage.html").permitAll().
                anyRequest().
                authenticated()
                .and().csrf().disable();
    }

返回部份结果:

{
	cause: null,
	stackTrace: [
	{
	methodName: "additionalAuthenticationChecks",
	fileName: "DaoAuthenticationProvider.java",
	lineNumber: 93,
	className: "org.springframework.security.authentication.dao.DaoAuthenticationProvider",
	nativeMethod: false
	},
	{
	methodName: "authenticate",
	fileName: "AbstractUserDetailsAuthenticationProvider.java",
	lineNumber: 166,
	className: "org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider",
	nativeMethod: false
	},
	{
	methodName: "authenticate",
	fileName: "ProviderManager.java",
	lineNumber: 174,
	className: "org.springframework.security.authentication.ProviderManager",
	nativeMethod: false
	},
	{
	methodName: "authenticate",
	fileName: "ProviderManager.java",
	lineNumber: 199,
	className: "org.springframework.security.authentication.ProviderManager",
	nativeMethod: false
	}
	//....
}

六、获取用户登录信息

SpringSecurity登录 的用户信息是存在SecurityContext中的,我们可以从中获取 到当前登录用户的信息:

	@GetMapping("/current")
    public Object getCurrentUser() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

结果是:

{
    "authorities":[
        {
            "authority":"admin"
        }
    ],
    "details":{
        "remoteAddress":"0:0:0:0:0:0:0:1",
        "sessionId":"A85D2A9654293E0402856AA1C53C0E54"
    },
    "authenticated":true,
    "principal":{
        "password":null,
        "username":"xiaoming",
        "authorities":[
            {
                "authority":"admin"
            }
        ],
        "accountNonExpired":true,
        "accountNonLocked":true,
        "credentialsNonExpired":true,
        "enabled":true
    },
    "credentials":null,
    "name":"xiaoming"
}

也可以直接注入的方式来获取:

	@GetMapping("/current")
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }

返回结果中principal是登录返回的UserDetails对象,我们也可以只获取这个对象
SpringSecurity_第3张图片
只获取principal的方式 :

	@GetMapping("/current")
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
        return userDetails;
    }

七、Spring Security登录流程解析

SpringSecurity登录时首先使用的是UsernamePasswordAuthenticationFilter进行登录请求的拦截:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
 	// 登录请求会调这个方法
    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 {
        	// 获取表单中的用户信息username和password,如果前台提交的是Json数据,需要重写该方法
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            // 构建UsernamePasswordAuthenticationToken 信息
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            // 交给AuthenticationManager去执行
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
	//......
}

我们看看这构建UsernamePasswordAuthenticationToken 信息的时候做了什么:

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		// super 对应的父类的构造函数中接收的是一组权限,这里还没有权限所以设置为null
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		// 是否认证也设置为false
		setAuthenticated(false);
	}

在UsernamePasswordAuthenticationFilter中最终会给AuthenticationManager去执行 :

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
	
	// 在UsernamePasswordAuthentication中调用了这个方法,这里接收的authentication就是之前的UsernamePasswordAuthenticationToken 这个对象
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
			// 获取对象类类型
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();
		// SpringSecurity提供了多组AuthenticationProvider 用于登录
		for (AuthenticationProvider provider : getProviders()) {
		// 获取每一个AuthenticationProvider ,通过provider.supports(toTest)判断当前这个provider是否支持username password方式登录
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				// 如果支持的话,就调用这个provider的authenticate(authentication);方法
				// DaoAuthenticationProvider 是AbstractUserDetailsAuthenticationProvider子类,authenticate是执行的父类中的方法
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}

}

上面的类调用了AbstractUserDetailsAuthenticationProvider这个类的authenticate方法做登录认证:

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
//........
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// 从传过来的authentication获取登录用户名
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		// 使用缓存
		boolean cacheWasUsed = true;
		// 从缓存中获取用户
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
		// 没有获取到缓存中的用户
			cacheWasUsed = false;

			try {
			// 从authentication封装一个user对象 这里的用户已经包含了用户名、密码和权限了
			// 这个地方的retrieveUser是由其子类DaoAuthenticationProvider实现的 这个方法也很重要,下面有分析
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			//...

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			//preAuthenticationChecks登录前检查,check方法中做的是isAccountNonLocked、isEnabled、isAccountNonExpired的检查
			preAuthenticationChecks.check(user);
			// 认证-> 检查用户密码是否匹配,如果不匹配就抛出异常
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				// 这里在上面的检查密码是否匹配中已经抛出了异常后又执行了一次检查
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}
		// 检查后的后置检查用来检查用户密码是否过期
		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			// 把用户信息放入缓存
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
	
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	//........
	//认证成功后
	protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		// 这里的设置用户已认证
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		/**
		上面调的这个 方法
		public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
					Collection authorities) {
				super(authorities);
				this.principal = principal;
				this.credentials = credentials;
				super.setAuthenticated(true); // must use super, as we override
			}
		**/
			result.setDetails(authentication.getDetails());
	
			return result;
		}
	// 后置检查
	private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
		public void check(UserDetails user) {
			if (!user.isCredentialsNonExpired()) {
				logger.debug("User account credentials have expired");

				throw new CredentialsExpiredException(messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.credentialsExpired",
						"User credentials have expired"));
			}
		}
	}

}

DaoAuthenticationProvider:


protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			// 获取UserDetailsService,并通过loadUserByUsername方法获取UserDetails对象
			// 这里的UserDetailsService就是我们自定义的MyUserDetailsService,在setUserDetailsService方法中加载的
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		//.......
}

protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		//.....
		// 获取登录信息中的密码
		String presentedPassword = authentication.getCredentials().toString();
		// 用passwordEncoder去检查密码是否匹配,用户名已经匹配了才得到userDetial对象
		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
		// 如果不配置就会抛出异常
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

认证成功过后,就会调用AbstractAuthenticationProcessingFilter里的doFilter方法执行认证成功的处理:

 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
            // 调用自定义认证失败处理
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
			// 调用自定义认证成功处理
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }
		// 登录成功后存储登录信息
        SecurityContextHolder.getContext().setAuthentication(authResult);
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
        }

        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }    

八、自定义过滤器

登录时可能要用到图片验证,所以会用到自定义的过滤器,用于验证图片验证码是否正确,只需要自定义类继承OncePerRequestFilter即可:

public class BeforeUsernamePasswordFilter extends OncePerRequestFilter {

    private AuthenticationFailureHandler authenticationFailureHandler ;
     public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    // 处理校验逻辑
        System.out.println(httpServletRequest.getRequestURI());
        if (false) {
        	// 如果失败了,用authenticationFailureHandler处理,要返回AuthenticationException的子类异常
            // AuthenticationException
            authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, null);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

设置这个BeforeUsernamePasswordFilter :

  protected void configure(HttpSecurity http) throws Exception {
  	// 实例化
        BeforeUsernamePasswordFilter beforeUsernamePasswordFilter = new BeforeUsernamePasswordFilter();
        // 这里只是给实例对象属性赋值
        beforeUsernamePasswordFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
        // 设置这个filter生效位置
        http.addFilterBefore(beforeUsernamePasswordFilter, UsernamePasswordAuthenticationFilter.class).
                formLogin()
                .loginPage("/loginpage.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .and().
                authorizeRequests()
                .antMatchers("/loginpage.html").permitAll().
                anyRequest().
                authenticated()
                .and().csrf().disable();
    }

九、RememberMe功能

在WebSecurityConfigurerAdapter的实现类的protected void configure(HttpSecurity http) throws Exception方法中设置RemeberMe功能:

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .and()
                // 开启rememberMe功能
                .rememberMe()
                // 设置持久化token的接口
                .tokenRepository(persistentTokenRepository())
                // 设置token有效时间
                .tokenValiditySeconds(20)
                .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }

设置token持久化接口:

 @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        // 自动创建使用的数据库表
        repository.setCreateTableOnStartup(false);
        return repository;
    }

PersistentTokenRepository接口有默认有两个子类,分别是InMemoryTokenRepositoryImpl、JdbcTokenRepositoryImpl,他们的功能分别是在内存中操作token和在数据库中操作token。

十、登录认证源码详细分析

UsernamePasswordAuthenticationFilter.java中的主要流程:
SpringSecurity_第4张图片
注意上面获取了AuthenticationManager,并且调用了它的authenticate方法,传入的参数authRequest是UsernamePasswordAuthenticationToken的实例,下面是ProviderManager.java中的只要流程:
SpringSecurity_第5张图片
ProviderManager.java中的主要认证逻辑还是交由其抽象父类执行的:result = provider.authenticate(authentication);
SpringSecurity_第6张图片
SpringSecurity_第7张图片
登录成功处理:
SpringSecurity_第8张图片

你可能感兴趣的:(服务器,JAVA,Spring,框架)