1. 程式人生 > >基於java config的springSecurity(二)--自定義認證

基於java config的springSecurity(二)--自定義認證

可參考的資料:

http://blog.csdn.net/xiejx618/article/details/42523337

http://blog.csdn.net/xiejx618/article/details/22902343

本文在前文的基礎上進行修改.
一.根據傳過來的使用者名稱和密碼實現自定義的認證邏輯.將基於記憶體的AuthenticationProvider改為自定義的AuthenticationProvider,實現認證(Authentication),還沒有實現授權(Authorization)
1.修改實體User類實現org.springframework.security.core.userdetails.UserDetails作為spring security管理的使用者.修改實體Role類實現org.springframework.security.core.GrantedAuthority作為spring security管理的簡單授權許可權

2.先自定義UserDetailsService,以供AuthenticationProvider使用.使用構造方法注入UserRepository,呼叫org.exam.repository.UserRepositoryCustom#findByUsernameWithAuthorities,根據使用者名稱來返回spring security管理的使用者.因為org.exam.domain.User#authorities是懶載入的,可以參考http://blog.csdn.net/xiejx618/article/details/21794337來解決懶載入問題.

package org.exam.auth;
import org.exam.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
 * Created by xin on 15/1/10.
 */
public class UserDetailsServiceCustom implements UserDetailsService {
	private final UserRepository userRepository;
	public UserDetailsServiceCustom(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userRepository.findByUsernameWithAuthorities(username);
	}
}
3.再自定義AuthenticationProvider.
package org.exam.auth;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
 * Created by xin on 15/1/10.
 */
public class AuthenticationProviderCustom implements AuthenticationProvider {
	private final UserDetailsService userDetailsService;
	public AuthenticationProviderCustom(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
		String username = token.getName();
		//從資料庫找到的使用者
		UserDetails userDetails = null;
		if(username != null) {
			userDetails = userDetailsService.loadUserByUsername(username);
		}
		//
		if(userDetails == null) {
			throw new UsernameNotFoundException("使用者名稱/密碼無效");
		}else if (!userDetails.isEnabled()){
			throw new DisabledException("使用者已被禁用");
		}else if (!userDetails.isAccountNonExpired()) {
			throw new AccountExpiredException("賬號已過期");
		}else if (!userDetails.isAccountNonLocked()) {
			throw new LockedException("賬號已被鎖定");
		}else if (!userDetails.isCredentialsNonExpired()) {
			throw new LockedException("憑證已過期");
		}
		//資料庫使用者的密碼
		String password = userDetails.getPassword();
		//與authentication裡面的credentials相比較
		if(!password.equals(token.getCredentials())) {
			throw new BadCredentialsException("Invalid username/password");
		}
		//授權
		return new UsernamePasswordAuthenticationToken(userDetails, password,userDetails.getAuthorities());
	}

	@Override
	public boolean supports(Class<?> authentication) {
		//返回true後才會執行上面的authenticate方法,這步能確保authentication能正確轉換型別
		return UsernamePasswordAuthenticationToken.class.equals(authentication);
	}
}

4.配置自定義的AuthenticationProvider

package org.exam.config;
import org.exam.auth.AuthenticationProviderCustom;
import org.exam.auth.UserDetailsServiceCustom;
import org.exam.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
 * Created by xin on 15/1/7.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private UserRepository userRepository;
	@Bean
	public UserDetailsService userDetailsService(){
		UserDetailsService userDetailsService=new UserDetailsServiceCustom(userRepository);
		return userDetailsService;
	}
	@Bean
	public AuthenticationProvider authenticationProvider(){
		AuthenticationProvider authenticationProvider=new AuthenticationProviderCustom(userDetailsService());
		return authenticationProvider;
	}
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//暫時使用基於記憶體的AuthenticationProvider
		//auth.inMemoryAuthentication().withUser("username").password("password").roles("USER");
		//自定義AuthenticationProvider
		auth.authenticationProvider(authenticationProvider());
	}
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/static/**");
	}
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//暫時禁用csrf,並自定義登入頁和登出URL
		http.csrf().disable()
				.authorizeRequests().anyRequest().authenticated()
				.and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()
				.and().logout().logoutUrl("/logout").permitAll();
	}
}
5.在頁面上可以使用如下的程式碼獲得丟擲的異常資訊.
 <c:if test="${SPRING_SECURITY_LAST_EXCEPTION.message != null}">
        <p>
           ${SPRING_SECURITY_LAST_EXCEPTION.message}
        </p>
    </c:if>

原始碼:http://download.csdn.net/detail/xiejx618/8349649

二.加入驗證碼功能.看過Spring Security 3.x Cookbook的Spring Security with Captcha integration,覺得驗證碼附加到使用者名稱這種方式非常醜陋,其實驗證碼驗證邏輯也不應在UserDetailsService.loadUserByUsername方法,因為這個方法不止在輸入使用者碼密碼登入時呼叫,比如記住我自動登入功能,也會呼叫此方法.
基於xml的方式,可以使用<custom-filter position="FORM_LOGIN_FILTER" ref="multipleInputAuthenticationFilter" />來替換預設的UsernamePasswordAuthenticationFilter,但基於javaConfig的方式似乎沒有等效的配置,所以替換預設的UsernamePasswordAuthenticationFilter的路不通.因為驗證驗證碼邏輯比使用者名稱密碼的邏輯要先,我的思路在UsernamePasswordAuthenticationFilter之前再新增一個KaptchaAuthenticationFilter.
1.修改配置org.exam.config.WebSecurityConfig#configure
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(new KaptchaAuthenticationFilter("/login", "/login?error"), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable()
            .authorizeRequests().anyRequest().authenticated()
            .and().formLogin().loginPage("/login").failureUrl("/login?error").usernameParameter("username").passwordParameter("password").permitAll()
            .and().logout().logoutUrl("/logout").permitAll();

}

HttpSecurity有addFilterBefore,addFilterAfter,就沒有replaceFilter,不然第一行配置都省了,所以思路只能這麼來.先看看KaptchaAuthenticationFilter,

注:http://docs.spring.io/spring-security/site/docs/4.1.0.RC2/reference/htmlsingle/#new開始提供HttpSecurity.addFilterAt

2.繼承AbstractAuthenticationProcessingFilter或者UsernamePasswordAuthenticationFilter,是為了利用驗證失敗時的處理(跳到failureUrl顯示異常資訊)
package org.exam.config;
import com.google.code.kaptcha.Constants;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * Created by xin on 15/1/7.
 */

public class KaptchaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private String servletPath;
    public KaptchaAuthenticationFilter(String servletPath,String failureUrl) {
        super(servletPath);
        this.servletPath=servletPath;
        setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl));

    }

    @Override

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res=(HttpServletResponse)response;
        if ("POST".equalsIgnoreCase(req.getMethod())&&servletPath.equals(req.getServletPath())){
            String expect = (String) req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
            if(expect!=null&&!expect.equalsIgnoreCase(req.getParameter("kaptcha"))){
                unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("輸入的驗證碼不正確"));
                return;
            }
        }
        chain.doFilter(request,response);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        return null;

    }

}
attemptAuthentication回撥不用理,重寫了doFilter方法,它不會被呼叫,從以上程式碼可知使用了google的kaptcha生成驗證碼,下面看看如何配置.
3.kaptcha的依賴如下
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>
然後在DispatcherServletInitializer新增一個kaptcha servlet,
org.exam.config.DispatcherServletInitializer#onStartup
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encoding-filter", CharacterEncodingFilter.class);
    encodingFilter.setInitParameter("encoding", "UTF-8");
    encodingFilter.setInitParameter("forceEncoding", "true");
    encodingFilter.setAsyncSupported(true);
    encodingFilter.addMappingForUrlPatterns(null, false, "/*");
    ServletRegistration.Dynamic kaptchaServlet = servletContext.addServlet("kaptcha-servlet", KaptchaServlet.class);
    kaptchaServlet.addMapping("/except/kaptcha");
}
4.不要忘了/except/kaptcha的請求被攔截了,所以要忽略掉
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/static/**", "/except/**");
}
頁面加入驗證輸入域,然後測試
<input type="text" id="kaptcha" name="kaptcha"/><img src="/testweb/except/kaptcha" width="80" height="25"/>