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

 

posted @ 2021-11-15 17:43  意犹未尽  阅读(492)  评论(0编辑  收藏  举报