Spring-Security基于源码扩展-自定义登录(二十三)
比如我们有的业务场景需要走outh2 或者短信验证码登录 下面以短信验证码为例
首先梳理默认的登录流程
1.http.formLogin() 会在WebSecurityConfigurerAdapter创建一个FormLoginConfigurer
2.WebSecurityConfigurerAdapter是WebSecurity的confugures 详见源码:spring-security源码-初始化(九) 搜索"5."
3.WebSecurity的build方法会触发WebSecurityConfigurerAdapter 的int和configure spring-security源码-初始化(九) 搜索"6."
4,WebSecurityConfigurerAdapter的build 就会遍历内部的init confgures 则对应FormLoginConfigurer 会添加一个UsernamePasswordAuthenticationFilter执行登录处理 参考Spring-Security源码-Filter之UsernamePasswordAuthenticationFilter(十四)
5.UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter UsernamePasswordAuthenticationFilter会负责解析入参封装成token交给AuthenticationManager处理
AuthenticationManager内部会匹配当前类型的AuthenticationProvider Token的处理器
Filter处理详见源码<2> AuthenticationManager初始化处参考<8> AuthenticationManager处理参考<跳转>
因为UsernamePasswordAuthenticationFilter和FormLoginConfigurer只适合用户名和密码这种形式,所以验证码打算从Configure 到Fitler 到AuthenticationManager 的AuthenticationProvider整套自定义
1.定义一个验证码的Token 参考UsernamePasswordAuthenticationToken实现
ublic class MessageAuthenticationToken extends AbstractAuthenticationToken { private String messageCode; private String phoneNumber; private Object principal; private Object credentials; public MessageAuthenticationToken(String messageCode,String phoneNumber) { super(null); this.messageCode=messageCode; this.phoneNumber=phoneNumber; super.setAuthenticated(true); // must use super, as we override } /** * The credentials that prove the principal is correct. This is usually a password, * but could be anything relevant to the <code>AuthenticationManager</code>. Callers * are expected to populate the credentials. * * @return the credentials that prove the identity of the <code>Principal</code> */ @Override public Object getCredentials() { return credentials; } /** * The identity of the principal being authenticated. In the case of an authentication * request with username and password, this would be the username. Callers are * expected to populate the principal for an authentication request. * <p> * The <tt>AuthenticationManager</tt> implementation will often return an * <tt>Authentication</tt> containing richer information as the principal for use by * the application. Many of the authentication providers will create a * {@code UserDetails} object as the principal. * * @return the <code>Principal</code> being authenticated or the authenticated * principal after authentication. */ @Override public Object getPrincipal() { return principal; } public void setCredentials(Object credentials) { this.credentials = credentials; } public void setPrincipal(Object principal) { this.principal = principal; } public String getMessageCode() { return messageCode; } public void setMessageCode(String messageCode) { this.messageCode = messageCode; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } }
2.自定义验证码登录请求Filter
@Order(1600) public class MessageAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //默认的登录认证请求 登录commit private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); public MessageAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } /** * 允许调用方改变 * @param loginProcessingUrl */ public MessageAuthenticationFilter(String loginProcessingUrl ) { super(new AntPathRequestMatcher(loginProcessingUrl, "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { //获取验证码 String messageCode = obtainMessageCode(request); messageCode = (messageCode != null) ? messageCode : ""; messageCode = messageCode.trim(); //获取手机号 String phoneNumber = obtainPhoneNumber(request); phoneNumber = (phoneNumber != null) ? phoneNumber : ""; //验证码登录的token MessageAuthenticationToken authRequest = new MessageAuthenticationToken(messageCode, phoneNumber); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMessageCode(HttpServletRequest request) { return request.getParameter("messageCode"); } protected String obtainPhoneNumber(HttpServletRequest request) { return request.getParameter("phoneNumber"); } }
3.自定义一个UserDetail主要根据手机号获取用户信息
public class MessageCodeUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //模拟查库 MyUserDetail userDetail= new MyUserDetail(); userDetail.setUsername("liqiang"); userDetail.setPassword("liqiang"); return userDetail; } }
3.自定义AuthenticationManager的AuthenticationProvider 处理器
public class MessageAuthenticationProvider implements AuthenticationProvider { public static Map<String,String> redisCache=new HashMap<>(); private UserDetailsService userDetailsService; public MessageAuthenticationProvider(UserDetailsService userDetailsService){ this.userDetailsService=userDetailsService; } static { redisCache.put("13128273410","1111"); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String code= getMessageCode(authentication); if(!StringUtils.hasLength(code)){ throw new BadCredentialsException("验证码已失效"); } MessageAuthenticationToken messageAuthenticationToken=(MessageAuthenticationToken)authentication; if(!messageAuthenticationToken.getMessageCode().equals(code)){ throw new BadCredentialsException("验证码错误"); } UserDetails userDetails= userDetailsService.loadUserByUsername(((MessageAuthenticationToken) authentication).getPhoneNumber()); messageAuthenticationToken.setCredentials(userDetails.getAuthorities()); messageAuthenticationToken.setPrincipal(userDetails.getUsername()); return messageAuthenticationToken; } private String getMessageCode(Authentication authentication) { MessageAuthenticationToken messageAuthenticationToken=(MessageAuthenticationToken)authentication; return redisCache.get(messageAuthenticationToken.getPhoneNumber()); } /** * 匹配token处理器 * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return (MessageAuthenticationToken.class.isAssignableFrom(authentication)); } }
4.自定义一个Configure替换默认的
public class MessageLoginConfig<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<MessageLoginConfig<H>, H>{ //登录页面 用于登录失败 跳回的登录页面 private String loginPage; //登录请求处理页面 private String loginProcessingUrl; //登录成功跳转页面 private String defaultSuccessUrl; public MessageLoginConfig<H> loginPage(String loginPage) { this.loginPage = loginPage; return this; } public MessageLoginConfig<H> loginProcessingUrl(String loginProcessUrl) { this.loginProcessingUrl=loginProcessUrl; return this; } public MessageLoginConfig<H> defaultSuccessUrl(String defaultSuccessUrl) { this.defaultSuccessUrl = defaultSuccessUrl; return this; } private MessageAuthenticationFilter authFilter; @Override public void init(H builder) throws Exception { //初始化一个Filter if(loginProcessingUrl==null) { authFilter = new MessageAuthenticationFilter(); }else{ authFilter = new MessageAuthenticationFilter(loginProcessingUrl); } //登录验证成功的处理器 authFilter.setAuthenticationSuccessHandler(new MessageCodeAuthenticationSuccessHandler(defaultSuccessUrl)); //登录验证失败的处理器 authFilter.setAuthenticationFailureHandler(new MessageCodeAuthenticationFailureHandler(loginPage)); //登录认证失败的处理器 未登录访问需要登录的页面 的异常处理器参考<> registerAuthenticationEntryPoint(builder, new AuthenticationEntryPoint() { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private PortResolver portResolver = new PortResolverImpl(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { this.redirectStrategy.sendRedirect(request, response, buildRedirectUrlToLoginPage(request,response,loginPage)); } protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, String url) { if (UrlUtils.isAbsoluteUrl(url)) { return url; } int serverPort = this.portResolver.getServerPort(request); String scheme = request.getScheme(); RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setScheme(scheme); urlBuilder.setServerName(request.getServerName()); urlBuilder.setPort(serverPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setPathInfo(url); return urlBuilder.getUrl(); } }); } @Override public void configure(H builder) throws Exception { //替换默认的UsernamePasswordAuthenticationFilter builder.addFilterAfter(authFilter, UsernamePasswordAuthenticationFilter.class); //设置AuthenticationManager 用于登录认证 this.authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class)); } @SuppressWarnings("unchecked") protected final void registerAuthenticationEntryPoint(H http, AuthenticationEntryPoint authenticationEntryPoint) { ExceptionHandlingConfigurer<H> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), getAuthenticationEntryPointMatcher(http)); } protected final RequestMatcher getAuthenticationEntryPointMatcher(H http) { ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); } MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher)); } class MessageCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private PortResolver portResolver = new PortResolverImpl(); public MessageCodeAuthenticationSuccessHandler(String url){ this.url=url; } private String url; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { this.redirectStrategy.sendRedirect(request, response,buildRedirectUrlToLoginPage(request,response,url)); } protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, String url) { if (UrlUtils.isAbsoluteUrl(url)) { return url; } int serverPort = this.portResolver.getServerPort(request); String scheme = request.getScheme(); RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setScheme(scheme); urlBuilder.setServerName(request.getServerName()); urlBuilder.setPort(serverPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setPathInfo(url); return urlBuilder.getUrl(); } } class MessageCodeAuthenticationFailureHandler implements AuthenticationFailureHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private PortResolver portResolver = new PortResolverImpl(); public MessageCodeAuthenticationFailureHandler(String url){ this.url=url; } private String url; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { this.redirectStrategy.sendRedirect(request, response, buildRedirectUrlToLoginPage(request,response,url)); } protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, String url) { if (UrlUtils.isAbsoluteUrl(url)) { return url; } int serverPort = this.portResolver.getServerPort(request); String scheme = request.getScheme(); RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder(); urlBuilder.setScheme(scheme); urlBuilder.setServerName(request.getServerName()); urlBuilder.setPort(serverPort); urlBuilder.setContextPath(request.getContextPath()); urlBuilder.setPathInfo(url); return urlBuilder.getUrl(); } } }
WebAdapter配置
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 对于不需要授权的静态文件放行 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); } /** * 对密码进行加密的实例 * @return */ @Bean PasswordEncoder passwordEncoder() { /** * 不加密所以使用NoOpPasswordEncoder * 更多可以参考PasswordEncoder 的默认实现官方推荐使用: BCryptPasswordEncoder,BCryptPasswordEncoder */ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { MessageCodeUserDetailService messageCodeUserDetailService=new MessageCodeUserDetailService(); /** * * 多个用户通过and隔开 * 添加自定义的messageCodeUserDetailService */ auth.userDetailsService(messageCodeUserDetailService) .and() //添加自定义的MessageAuthenticationProvider .authenticationProvider(new MessageAuthenticationProvider(messageCodeUserDetailService)); } @Override protected void configure(HttpSecurity http) throws Exception { // http.authorizeRequests() // .anyRequest() // .authenticated() // .and().rememberMe()//记住登录 // .tokenRepository(new InMemoryTokenRepositoryImpl()) // .and() // .formLogin()// rm表单的方式 // .loginPage("/login")//登录页面路径 // .loginProcessingUrl("/doLogin") // //自定义登录请求地址 // .defaultSuccessUrl("/hello") // .usernameParameter("loginName") // .passwordParameter("loginPassword") // .permitAll(true)//不拦截 // .and() // .csrf()//记得关闭 // .disable() // .sessionManagement(). // maximumSessions(1) // .maxSessionsPreventsLogin(true); // ("/login","doLogin")主要是在最后一个过滤器校验是否登录和授权处增加一个Permiall的Attribute 实现登录页面和doLogin的放心 参考<> http.authorizeRequests().antMatchers("/login","/doLogin").permitAll() .anyRequest() .authenticated() .and().rememberMe()//记住登录 .tokenRepository(new InMemoryTokenRepositoryImpl()) .and() //使用自定义的Configure .apply(new MessageLoginConfig<HttpSecurity>()) .loginPage("/login")//登录页面路径 .loginProcessingUrl("/doLogin") //自定义登录请求地址 .defaultSuccessUrl("/hello") .and() .csrf()//记得关闭 .disable() .sessionManagement(). maximumSessions(1) .maxSessionsPreventsLogin(true); }