SpringSecurity

2,260 阅读4分钟

SpringSecurity

一、框架的基础使用

当pom文件中加入Spring Security依赖时,默认开启权限认证

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

自定义SecurityConfig配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SuccessHandler successHandler;

    @Autowired
    private FailureHandler failureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //basic和表单两种方式,formLogin的拦截器UsernamePasswordAuthenticationFilter
                .formLogin()
                .loginPage("/login")
            //自定义登录url(覆盖AbstractAuthenticationProcessingFilter中的RequestMatcher),post方式,
                .loginProcessingUrl("/login")
            //覆盖默认成功处理器,默认实现跳转上一url,先返回登录用户信息json
                .successHandler(successHandler)
            //覆盖默认失败处理器,默认实现跳转登录页面loginPage,先返回失败信息
                .failureHandler(failureHandler)
                .and()
            //对授权的路径进行配置,
                .authorizeRequests()
                .antMatchers("/login.html","/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }

    //spring security内置的加密器,加密中的字符串中有salt,每次加密的salt均不一样,所以对同一内容加密后的结果是不同的。
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

SuccessHandler.java

@Component
public class SuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();

        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

FailureHandler.java

@Component
public class FailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(500);
        ObjectMapper objectMapper = new ObjectMapper();
        String content = objectMapper.writeValueAsString(exception.getMessage());
        response.getWriter().write(content);
    }
}

UserService.java

//为方便,模拟从数据库查询用户信息
@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Set<GrantedAuthority> authorities = new HashSet<>();

        authorities.add(new SimpleGrantedAuthority("admin"));
        return new User("fly",passwordEncoder.encode("123"),authorities);
    }
}

效果:

访问http://localhost:8080/test,返回{"result":"0","message":"请登录","data":null}

访问http://localhost:8080/login.html,输入错误用户名或密码,返回{"result":"0","message":"用户名或密码错误","data":null}//具体错误信息可通过覆盖DaoAuthenticationProvider或者在UserDetailService中添加逻辑代码实现。输入正确用户名 密码,返回

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

自定义格式在SuccessHandler中实现。

过程分析

一张经典的SpringSecurity执行图

SpringSecurity的执行是依赖一套FilterChain来实现的

SecurityContextPersistenceFilter是第二个Filter,通过断点方式来看这条Filter Chain

1571972343156

比较重要的几个Filter:

SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,用于FilterSecurityInterceptor判断是否已经认证,SecurityContextHolder中存储。
ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();SecurityContextHolder中的SecurityContext是线程级变量

UsernamePasswordAuthenticationFilter:拦截请求(看其构造函数中配置)。这里采用了模板的方式,具体拦截过程在AbstractAuthenticationProcessingFilter中的doFilter方法。

FilterSecurityInterceptor:根据配置,即那些请求需要permitAll ,哪些请求需要authenticated。

主要的验证逻辑是在UsernamePasswordAuthenticationFilter中实现的

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#UsernamePasswordAuthenticationFilter(构造函数)


	public UsernamePasswordAuthenticationFilter() {
	//这个类继承了一个抽象类,这里的构造函数设置了父类的requiresAuthenticationRequestMatcher属性(在判断是否拦截请求的时候用到)
		super(new AntPathRequestMatcher("/login", "POST"));
	}

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter

		...
		//用上面的requiresAuthenticationRequestMatcher进行匹配判断,即拦截路径/login POST请求
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}
		...
		
		try {
		//尝试进行验证 子类实现(关键方法)
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch{
		...
		}
		...
		//调用登录成功处理器
		successfulAuthentication(request, response, chain, authResult);

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

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

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

		username = username.trim();

        //封装用于验证的token
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property sessionID,封装请求的一些信息
		setDetails(request, authRequest);

        //得到AuthenticationManager对象(全局唯一)的实现类ProviderManager进行登录验证
		return this.getAuthenticationManager().authenticate(authRequest);
	}

​ org.springframework.security.authentication.ProviderManager#authenticate

...
//通过循环所注册的provider,通过provider的support(token)方法查找能处理该类型token的provider
for (AuthenticationProvider provider : getProviders()) {
	if (!provider.supports(toTest)) {
				continue;
			}
}
try {
    		//支持UsernamePasswordAuthenticationToken的是DaoAuthenticationProvider
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}

org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate

preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user),

org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser

//利用userDetailService查找用户,security自动配置
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;

org.springframework.security.authentication.dao.DaoAuthenticationProvider#additionalAuthenticationChecks

...
//这个方法对密码进行了校验
	String presentedPassword = authentication.getCredentials().toString();

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

整体的流程是这样的,

一张经典的图

1571974749801

所以可以通过添加自定义filter provider token可以实现自定义登录流程,

添加验证码

简要实现:在UsernamePasswordAuthenticationFilter前添加一个checkCodeFilter即可.

实现: