可参考的资料:
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);
- }
- }
- 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();
- }
- }
- <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
- 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;
- }
- }
3.kaptcha的依赖如下
- <dependency>
- <groupId>com.github.penggle</groupId>
- <artifactId>kaptcha</artifactId>
- <version>2.3.2</version>
- </dependency>
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"/>