• SpringSecurity基于源码扩展自定义登录(二十三)


    比如我们有的业务场景需要走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);
        }
  • 相关阅读:
    SOLO: 按位置分割对象
    支付宝架构
    h264和h265多维度区别
    机器学习图解
    机器视觉系统性能
    APA自动泊车系统
    智能驾驶测距估计
    结构感知图像修复:ICCV2019论文解析
    Lambda表达式
    转:利用 T-sql 的从句 for xml path('') 实现多行合并到一行, 并带有分隔符
  • 原文地址:https://www.cnblogs.com/LQBlog/p/15557463.html
Copyright © 2020-2023  润新知