Security实现多方式登录

【实现思路】

自定义过滤器继承UsernamePasswordAuthenticationFilter,可以配置一个需要身份验证的请求匹配器,它使用AntPathRequestMatcher来匹配请求的URL路径。(如果请求的URL路径与"/user/login"相匹配,并且HTTP方法为POST,那么该请求将被要求进行身份验证)

【代码实现】

1. 编写Security配置类WebSecurityConfig

java 复制代码
@Configuration  
@EnableWebSecurity  
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级别的权限认证  
@Slf4j  
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
  
    @Value("${spring.security.concurrent-session-control.maximum-sessions}")  
    private int maximumSessions;  

    @Value("${spring.security.concurrent-session-control.exception-if-maximum-exceeded:false}")  
    private boolean exceptionIfMaximumExceeded;  

    @Value("${server.servlet.session.cookie.name:}")  
    private String cookieId;  

    @Autowired  
    private JsonMapper jsonMapper;  

    @Autowired  
    @Qualifier("customUserDetailsServiceImpl")  
    private UserDetailsService userDetailsService;  

    @Autowired  
    private SysUserService userMgrService;  

    @Autowired  
    private SysLogService sysLogService;  

    @Autowired  
    private SipacUidService sipacUidService;  

    @Autowired  
    private SipacZwxtService sipacZwxtService;  

    @Autowired  
    private UserLoginPwdErrorService userLoginPwdErrorService;  

    /**  
    * 会话存储库  
    */  
    @Autowired  
    private FindByIndexNameSessionRepository<?> sessionRepository;  

    /**  
    * 定义无需权限的接口列表(requestMethod空格requestUrl),在CustomUserDetailsService中使用  
    *  
    * @return  
    */  
    @Bean  
    public Set<GrantedAuthority> publicAuthority() {  
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();  
        grantedAuthorities.add(new SimpleGrantedAuthority("GET /user/info"));  
        grantedAuthorities.add(new SimpleGrantedAuthority("GET /common/bell"));  
        grantedAuthorities.add(new SimpleGrantedAuthority("POST /user/changePwd"));  
        return grantedAuthorities;  
    }  

    /**  
    * 身份验证管理生成器  
    *  
    * @param auth  
    * @throws Exception  
    */  
    @Override  
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {  
        // 自定义customUserDetailsService必须单独创建并注入Spring,否则Spring检查不到此bean,仍然会启动inMemoryAuthentication  
        auth.userDetailsService(userDetailsService)  
        // 使用 BCryptPasswordEncoder 加盐加密  
        .passwordEncoder(new BCryptPasswordEncoder());  
        /// //不删除凭据,以便记住用户  
        // auth.eraseCredentials(false);  
    }  

    /**  
    * HTTP请求安全处理  
    *  
    * @param http  
    * @throws Exception  
    */  
    @Override  
    protected void configure(HttpSecurity http) throws Exception { //HTTP请求安全处理  
        http  
            .authorizeRequests() // 授权  
            //允许直接访问的地址(其中根路径/需要放开,不然将vue打包后放入java项目路径一起发布后将无法访问根路径)  
            .antMatchers( "/app/**","/common/**", "/zwxt/api/**", "/zwxtLogin.html", "/mobile/", "/manualTask/**").permitAll()  
            //将PreflightRequest不做拦截, 放过 option 请求  
            .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()  
            //使用Spring EL表达式规则 将自定义权限规则传入,所有url必须走我们写的权限规则方法,EL表达式为true时才能访问  
            .anyRequest().access("@rbcaService.hasPermission(request,authentication)")  
            //异常处理  
            .and()  
            .exceptionHandling()  
            //没有权限,返回json  
            .accessDeniedHandler(new AccessDeniedHandler() {  
            @Override  
            public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException ex) throws IOException, ServletException {  
            handleAccessDenied(req, resp, ex);  
            }  
            })  
            // 设置登录失效处理程序  
            .authenticationEntryPoint(new AuthenticationEntryPoint() {  
                @Override  
                public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException ex) throws IOException, ServletException {  
                    handleAuthenticationEntryPointCommence(req, resp, ex);  
                }  
            })  
            // 退出登录  
            .and()  
            .logout()  
            // 退出登录的url, 默认为/logout  
            .logoutUrl("/user/logout")  
            // .logoutSuccessUrl("/logout/success").permitAll() // 退出成功跳转URL,注意该URL不需要权限验证  
            .addLogoutHandler(new LogoutHandler() {  
                @Override  
                public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) {  
                    handleLogout(req, resp, auth);  
                    // 退出时需要移除,否则下次登录时会提示超过设定的最大session会话数  
                    sessionRegistry().removeSessionInformation(req.getSession().getId());  
                }  
            })  
            .logoutSuccessHandler(new LogoutSuccessHandler() {  
                @Override  
                public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException {  
                    handleLogoutSuccess(req, resp, auth);  
                }  
            })  
            // 退出登录删除指定的cookie  
            .deleteCookies(cookieId)  
            .and()  
            .csrf().disable() //取消csrf防护,否则登录报错403,权限不足  
            .cors().disable(); //开启跨域访问  

        // 同源允许iframe嵌套  
        http.headers().frameOptions().sameOrigin();  
        // 禁用缓存  
        http.headers().cacheControl();  

        // // 以下设置无效,因为已经用添加自定义ConcurrentSessionFilter代替  
        // //session相关的控制  
        // http.sessionManagement()  
        // //指定最大的session并发数量---即一个用户只能同时在一处登陆(腾讯视频的账号好像就只能同时允许2-3个手机同时登陆)  
        // .maximumSessions(1)  
        // //当超过指定的最大session并发数量时,阻止后面的登陆(感觉貌似很少会用到这种策略)  
        // .maxSessionsPreventsLogin(true)  
        // ;  

        //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter,从JSON中获取用户名密码进行验证  
        http.addFilterAt(jsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);  
        http.addFilterAt(zwxtLoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);  

        /*  
        ConcurrentSessionFilter:判断session是否过期,如果过期就执行过期方法。  
        本来这个过滤器是不需要我们管的,但是这个过滤器中也用到了SessionRegistry,而SessionRegistry现在是由我们自己来定义的,所以该过滤器我们也要重新配置一下  
        */  
        http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), new JsonSessionInformationExpiredStrategy(jsonMapper)), ConcurrentSessionFilter.class);  
    }  

    /**  
    * spring session 会话注册表  
    *  
    * @return  
    */  
    @Bean  
    public SpringSessionBackedSessionRegistry<?> sessionRegistry() {  
        return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);  
    }  


    @Bean  
    JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {  
        // 自定义Filter,从JSON中获取用户名密码进行验证  
        JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(jsonMapper, userLoginPwdErrorService);  
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", HttpMethod.POST.name()));  

        // // 将存有的身份信息传进去, 重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager  
        // filter.setAuthenticationManager(super.authenticationManagerBean());  
        // // 自定义AuthenticationManager,实现认证方法  
        filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(  
        new JsonUsernamePasswordAuthenticationProvider(userDetailsService, passwordEncoder(), sipacUidService, userLoginPwdErrorService))));  

        // session并发控制,因为默认的并发控制方法是空方法.这里必须自己配置一个,并且设置setMaximumSessions和setExceptionIfMaximumExceeded  
        // 这两个设置值和http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)作用一样,  
        // 但在自定义Filter中不会取http.sessionManagement()中设置的值,需要在这里设置  
        // 如果需要根据用户某个状态来判断,如果用户处在空闲状态,就允许同一个账号后登录的踢掉前面登录的,如果是繁忙状态就不允许登录,请参考  
        // https://blog.csdn.net/huangmingleiluo/article/details/78546502 参照ConcurrentSessionControlAuthenticationStrategy自定义类实现SessionAuthenticationStrategy接口即可  
        ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy =  
        new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());  
        //设置单账号最大并发登录数  
        concurrentSessionControlAuthenticationStrategy.setMaximumSessions(maximumSessions);  
        /*  
        为true时,当超过指定的最大session并发数量时,阻止后面的登陆;  
        为false时,将最近使用最少的若干(当前会话总数(含当前会话)-允许的最大数+1)会话其标记为无效  
        具体见org.springframework.security.web.authentication.session.allowableSessionsExceeded  
        */  
        concurrentSessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(exceptionIfMaximumExceeded);  
        //组合多个SessionAuthenticationStrategy,  
        CompositeSessionAuthenticationStrategy delegateStrategies = new CompositeSessionAuthenticationStrategy(  
            //要注意这些实例在集合中的顺序  
            Arrays.asList(  
                concurrentSessionControlAuthenticationStrategy,  
                /*  
                    固定会话攻击保护策略  
                    ChangeSessionIdAuthenticationStrategy仍使用原来的session的SessionAuthenticationStrategy,仅修改下session id;  
                    SessionFixationProtectionStrategy则是创建一个新的session,将老session的属性迁移到新的session中。  
                    只有当请求中已经存在session,并且是有效的时候才会进行实际的处理,具体见这两个类的父类AbstractSessionFixationProtectionStrategy的onAuthentication方法。  
                */  
                new SessionFixationProtectionStrategy(),  
                /*  
                    注册新session信息到SessionRegistry,在这里注册了,在JsonUsernamePasswordAuthenticationFilter中就不需要  
                    sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal());了。  
                    在这注册是验证成功后注册的,而JsonUsernamePasswordAuthenticationFilter则是在验证之前  
                */  
                    new RegisterSessionAuthenticationStrategy(sessionRegistry()  
                )  
        ));  
        filter.setSessionAuthenticationStrategy(delegateStrategies);  

        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {  
        @Override  
        public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {  
            handleAuthenticationSuccess(req, resp, auth);  
        }  
        });  
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {  
        @Override  
        public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {  
            handleAuthenticationFailure(req, resp, e);  
        }  
        });  
        return filter;  
    }  


    @Bean  
    ZwxtLoginAuthenticationFilter zwxtLoginAuthenticationFilter() throws Exception {  
        // 自定义Filter,从JSON中获取Token进行验证  
        ZwxtLoginAuthenticationFilter filter = new ZwxtLoginAuthenticationFilter(jsonMapper, sipacZwxtService);  
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/zwxtLogin", HttpMethod.POST.name()));  

        //自定义AuthenticationManager,实现认证方法  
        filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(  
        new ZwxtAuthenticationProvider(userDetailsService, passwordEncoder(), sipacZwxtService))));  

        // session并发控制,因为默认的并发控制方法是空方法.这里必须自己配置一个,并且设置setMaximumSessions和setExceptionIfMaximumExceeded  
        // 这两个设置值和http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true)作用一样,  
        // 但在自定义Filter中不会取http.sessionManagement()中设置的值,需要在这里设置  
        // 如果需要根据用户某个状态来判断,如果用户处在空闲状态,就允许同一个账号后登录的踢掉前面登录的,如果是繁忙状态就不允许登录,请参考  
        // https://blog.csdn.net/huangmingleiluo/article/details/78546502 参照ConcurrentSessionControlAuthenticationStrategy自定义类实现SessionAuthenticationStrategy接口即可  
        ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy =  
        new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());  
        //设置单账号最大并发登录数  
        concurrentSessionControlAuthenticationStrategy.setMaximumSessions(maximumSessions);  
        /*  
            为true时,当超过指定的最大session并发数量时,阻止后面的登陆;  
            为false时,将最近使用最少的若干(当前会话总数(含当前会话)-允许的最大数+1)会话其标记为无效  
            具体见org.springframework.security.web.authentication.session.allowableSessionsExceeded  
        */  
        concurrentSessionControlAuthenticationStrategy.setExceptionIfMaximumExceeded(exceptionIfMaximumExceeded);  
        //组合多个SessionAuthenticationStrategy,  
        CompositeSessionAuthenticationStrategy delegateStrategies = new CompositeSessionAuthenticationStrategy(  
            //要注意这些实例在集合中的顺序  
            Arrays.asList(  
                concurrentSessionControlAuthenticationStrategy,  
                /*  
                固定会话攻击保护策略  
                ChangeSessionIdAuthenticationStrategy仍使用原来的session的SessionAuthenticationStrategy,仅修改下session id;  
                SessionFixationProtectionStrategy则是创建一个新的session,将老session的属性迁移到新的session中。  
                只有当请求中已经存在session,并且是有效的时候才会进行实际的处理,具体见这两个类的父类AbstractSessionFixationProtectionStrategy的onAuthentication方法。  
                */  
                new SessionFixationProtectionStrategy(),  
                /*  
                注册新session信息到SessionRegistry,在这里注册了,在JsonUsernamePasswordAuthenticationFilter中就不需要  
                sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal());了。  
                在这注册是验证成功后注册的,而JsonUsernamePasswordAuthenticationFilter则是在验证之前  
                */  
                new RegisterSessionAuthenticationStrategy(sessionRegistry()  
                )  
        ));  
        filter.setSessionAuthenticationStrategy(delegateStrategies);  

        filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {  
            @Override  
            public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {  
                handleAuthenticationSuccess(req, resp, auth);  
            }  
        });  
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {  
            @Override  
            public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {  
                handleAuthenticationFailure(req, resp, e);  
            }  
        });  
        return filter;  
    }  

    @Override  
    public void configure(WebSecurity web) { //WEB安全  
        //设置静态资源不要拦截  
        web.ignoring().antMatchers("/static/**");  
        //对于在header里面增加token等类似情况,放行所有OPTIONS请求。  
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");  
    }  

    /**  
    * 密码生成策略.  
    */  
    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
    
    /*
        用户成功进行身份验证后的处理器。当用户成功登录并通过身份验证时,身份验证成功处理器将被调用,以执行特定的操作或逻辑。通常用于在用户登录成功后,跳转到指定页面、记录日志、发送通知等操作。
    */
    private void handleAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {  
        UserDetails userDetails = (UserDetails) auth.getPrincipal();  
        String userId = userDetails.getUsername();  
        HttpSession session = req.getSession();  
        session.setAttribute(ConstantUtil.SESSION_USER_ID, userId);  

        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);  
        resp.setCharacterEncoding("UTF-8");  
        Map<String, Object> map = new HashMap<>(1);  
        map.put("token", req.getSession().getId());  

        PrintWriter out = resp.getWriter();  
        SysUser sysUser = userMgrService.get(userId);  
        String message;  
        if (sysUser.getAccountType() == AccountTypeEnum.PT && passwordEncoder().matches("123456", sysUser.getPwd())) {  
            message = "登录成功! 当前密码为初始密码,请尽快修改!";  
        } else {  
            message = "登录成功!";  
        }  
        out.write(jsonMapper.writeValueAsString(Response.ok(message, map)));  
        out.flush();  
        out.close();  

        //获取请求IP  
        String ip = StringUtils.getIpAddr(req);  
        //获取请求Method  
        String method = req.getMethod();  
        //获取请求Url  
        String url = req.getRequestURI();  
        LocalDateTime now = LocalDateTime.now();  
        //更新用户最新登录时间和IP  
        try {  
            userMgrService.updateLastLogin(userId, now, ip);  
        } catch (Exception e) {  
            log.error("更新用户最新登录时间和IP失败", e);  
        }  
        //记录日志到数据库  
        try {  
            sysLogService.save(new SysLog(userId, now, ip, "登录", "登录", method, url, null, ConstantUtil.SUCCESS,  
        null, null, null));  
        } catch (Exception e) {  
            log.error("记录操作日志失败", e);  
        }  

    }  
       
    /*
        用户身份验证失败时的处理器。当用户尝试进行身份验证但失败时,身份验证失败处理器将被调用,以执行特定的操作或逻辑。通常用于在用户登录失败后,记录日志、显示错误消息、重定向到登录页面等操作。
    */
    private void handleAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {  
        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);  
        resp.setCharacterEncoding("UTF-8");  
        Response<?> response;  
        if (e instanceof PasswordErrorBeforeAccountLockException) {  
            PasswordErrorBeforeAccountLockException o = (PasswordErrorBeforeAccountLockException) e;  
            response = Response.error("密码错误,再输错" + o.getErrorRemainNum() + "次,账号将被临时锁定" + o.getLockTime() + "分钟");  
        } else if (e instanceof PasswordErrorOverLimitAccountLockException) {  
            PasswordErrorOverLimitAccountLockException o = (PasswordErrorOverLimitAccountLockException) e;  
            response = Response.error("密码错误次数超过限制,账号临时锁定" + o.getLockTime() + "分钟");  
        } else if (e instanceof AccountLockException) {  
            AccountLockException o = (AccountLockException) e;  
            response = Response.error("账号已被临时锁定,请" + o.getLockRemainTime() + "分钟后再试");  
        } else if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {  
            response = Response.error("用户名或者密码输入错误!");  
        } else if (e instanceof LockedException) {  
            response = Response.error("账户被锁定,请联系管理员!");  
        } else if (e instanceof CredentialsExpiredException) {  
            response = Response.error("密码过期,请联系管理员!");  
        } else if (e instanceof AccountExpiredException) {  
            response = Response.error("账户过期,请联系管理员!");  
        } else if (e instanceof DisabledException) {  
            response = Response.error("账户被禁用,请联系管理员!");  
        } else if (e instanceof SessionAuthenticationException) {  
            response = Response.error("登录失败! 当前用户超过最大并发登录数!");  
        } else if (e instanceof SipacUidValidateException) {  
            response = Response.error("zw用户体系接口:" + e.getMessage());  
        } else if (e instanceof SipacZwxtValidateException) {  
            response = Response.error("zw系统认证中心接口:" + e.getMessage());  
        } else if (e instanceof AuthenticationServiceException) {  
            response = Response.error("登录失败! " + e.getMessage());  
        } else {  
            response = Response.error("登录失败! " + e.getMessage());  
        }  

        PrintWriter out = resp.getWriter();  
        out.write(jsonMapper.writeValueAsString(response));  
        out.flush();  
        out.close();  
    }  

    private void handleAccessDenied(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException {  
        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);  
        resp.setCharacterEncoding("UTF-8");  
        PrintWriter out = resp.getWriter();  
        out.write(jsonMapper.writeValueAsString(Response.error(ResultCode.NotAuthorized)));  
        out.flush();  
        out.close();  
    }  

    private void handleAuthenticationEntryPointCommence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException {  
        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);  
        resp.setCharacterEncoding("UTF-8");  
        PrintWriter out = resp.getWriter();  
        out.write(jsonMapper.writeValueAsString(Response.error(ResultCode.NoLoginOrSessionExpired)));  
        out.flush();  
        out.close();  
    }  

    private void handleLogout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) {  
        //当session过期或者用户清除浏览器缓存后,点击退出按钮,此处无法获取租户和用户标识信息,也就无法记录日志了  
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  
        if (authentication == null) {  
            return;  
        }  
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();  
        String userId = userDetails.getUsername();  
        //获取请求IP  
        String ip = StringUtils.getIpAddr(req);  
        //获取请求Method  
        String method = req.getMethod();  
        //获取请求Url  
        String url = req.getRequestURI();  
        LocalDateTime now = LocalDateTime.now();  
        //记录日志到数据库  
        try {  
            sysLogService.save(new SysLog(userId, now, ip, "退出", "退出", method, url, null, ConstantUtil.SUCCESS,  
        null, null, null));  
        } catch (Exception e) {  
            log.error("记录操作日志失败", e);  
        }  
    }  

    private void handleLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {  
        resp.setContentType(MediaType.APPLICATION_JSON_VALUE);  
        resp.setCharacterEncoding("UTF-8");  
        PrintWriter out = resp.getWriter();  
        out.write(jsonMapper.writeValueAsString(Response.ok("退出成功! ")));  
        out.flush();  
        out.close();  
    }  
  
}
  1. 在Security配置类configure(HttpSecurity http)方法中重写Filter替换UsernamePasswordAuthenticationFilter,从JSON中获取用户名密码进行验证。这里我们添加两种验证方式:jsonUsernamePasswordAuthenticationFilter()zwxtLoginAuthenticationFilter()
  2. 在自定义Filter中为Spring Security配置一个需要身份验证的请求匹配器,它使用AntPathRequestMatcher来匹配请求的URL路径,如果请求的URL路径与"/user/login"相匹配,并且HTTP方法为POST,那么该请求将被要求进行身份验证。
  3. 设置自定义AuthenticationManager自定义认证方法。

2.创建自定义FilterJsonUsernamePasswordAuthenticationFilter拦截器

java 复制代码
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {  
  
    private static final String USER_PARAMETER = "user";  

    private final JsonMapper jsonMapper;  

    private final UserLoginPwdErrorService userLoginPwdErrorService;  

    public JsonUsernamePasswordAuthenticationFilter(JsonMapper jsonMapper, UserLoginPwdErrorService userLoginPwdErrorService) {  
        this.jsonMapper = jsonMapper;  
        this.userLoginPwdErrorService = userLoginPwdErrorService;  
    }  

    @Override  
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {  
        if (!"POST".equals(request.getMethod())) {  
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());  
        }  
        // 从JSON中读取用户名和密码  
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {  
            String username = null;  
            String password = null;  
            try (InputStream is = request.getInputStream()) {  
                // 将传入的JSON数据转换成Map,然后就可以通过get("key")获取  
                Map<String, String> authenticationBean = jsonMapper.readValue(is, new TypeReference<Map<String, String>>() {  
                });  
                AES aes = SecureUtil.aes("publicinstitutio".getBytes());  
                String usernameAndPassword = aes.decryptStr(authenticationBean.get(USER_PARAMETER));  
                int index = usernameAndPassword.indexOf(",");  
                username = usernameAndPassword.substring(0, index);  
                password = usernameAndPassword.substring(index + 1);  
            } catch (IOException e) {  
                logger.error("", e);  
            }  
            username = (username != null) ? username : "";  
            username = username.trim();  
            password = (password != null) ? password : "";  

            //根据用户名检查最近密码连续输入错误次数是否超过配置最大值  
            userLoginPwdErrorService.check(username);  

            SysUserDetails sysUserDetails = new SysUserDetails(username);  
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUserDetails, password);  
            //sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal())  
            // Allow subclasses to set the "details" property  
            setDetails(request, authRequest);  
            return this.getAuthenticationManager().authenticate(authRequest);  
        }  
        // 不是,就使用原来父类的key-value形式获取  
        return super.attemptAuthentication(request, response);  
    }  
  
}

3.创建自定义FilterZwxtLoginAuthenticationFilter拦截器

java 复制代码
@Slf4j  
public class ZwxtLoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {  

    private static final String CODE_PARAMETER = "code";  
    private static final String TIMESTAMP_PARAMETER = "timestamp";  
    private static final String SIGN_PARAMETER = "sign";  

    private final JsonMapper jsonMapper;  

    private final SipacZwxtService sipacZwxtService;  

    public ZwxtLoginAuthenticationFilter(JsonMapper jsonMapper, SipacZwxtService sipacZwxtService) {  
        this.jsonMapper = jsonMapper;  
        this.sipacZwxtService = sipacZwxtService;  
    }  

    @Override  
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {  
        if (!"POST".equals(request.getMethod())) {  
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());  
        }  
        // 从JSON中读取code、timestamp、sign  
        if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {  
            String code = null;  
            String timestamp = null;  
            String sign = null;  
            try (InputStream is = request.getInputStream()) {  
            // 将传入的JSON数据转换成Map,然后就可以通过get("key")获取  
            Map<String, String> authenticationBean = jsonMapper.readValue(is, new TypeReference<Map<String, String>>() {  
            });  
            code = authenticationBean.get(CODE_PARAMETER);  
            timestamp = authenticationBean.get(TIMESTAMP_PARAMETER);  
            sign = authenticationBean.get(SIGN_PARAMETER);  
            } catch (IOException e) {  
            this.logger.error(e);  
            }  
            this.logger.info("zw系统传递的授权码code为:" + code + ",时间戳timestamp为:" + timestamp + ",签名sign为:" + sign);  
            if (!StringUtils.hasLength(code)) {  
            this.logger.error("zw系统传递的授权码code为空!");  
                throw new AuthenticationServiceException("zw系统传递的授权码code为空!");  
            }  
            if (!StringUtils.hasLength(timestamp)) {  
                this.logger.error("zw系统传递的时间戳timestamp为空!");  
                throw new AuthenticationServiceException("zw系统传递的时间戳timestamp为空!");  
            }  
            if (!StringUtils.hasLength(sign)) {  
                this.logger.error("zw系统传递的签名sign为空!");  
                throw new AuthenticationServiceException("zw系统传递的签名sign为空!");  
            }  
            if (!sipacZwxtService.validateSign(code, Long.parseLong(timestamp), sign)) {  
                this.logger.error("zw系统传递的签名sign验证未通过,授权码code无效!");  
                throw new AuthenticationServiceException("zw系统传递的签名sign验证未通过,授权码code无效!");  
            }  
            ThirdOauthResult thirdOauthResult;  
            try {  
            thirdOauthResult = sipacZwxtService.thirdOauth(code);  
            } catch (Exception e) {  
            this.logger.error("调用zw系统认证中心接口出现异常! " + e.getMessage(), e);  
            throw new AuthenticationServiceException("调用zw系统认证中心接口出现异常! " + e.getMessage(), e);  
            }  
            //认证失败  
            if (thirdOauthResult.getCode() != 0) {  
                this.logger.error("调用zw系统认证中心接口进行认证未通过,具体信息为: " + thirdOauthResult.getMsg());  
                throw new SipacZwxtValidateException(thirdOauthResult.getMsg());  
            }  
            if (thirdOauthResult.getObj() == null) {  
                this.logger.error("调用zw系统认证中心接口进行认证结果内容为空,无法取得accountName字段");  
                throw new SipacZwxtValidateException("认证结果内容为空,无法取得accountName字段");  
            }  
            String userId = thirdOauthResult.getObj().getAccountName();  
            if (!StringUtils.hasLength(userId)) {  
                this.logger.error("调用zw系统认证中心接口进行认证结果内容中accountName字段为空");  
                throw new SipacZwxtValidateException("认证结果内容中accountName字段为空");  
            }  
            SysUserDetails sysUserDetails = new SysUserDetails(userId);  
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUserDetails, null);  
            //sessionRegistry.registerNewSession(request.getSession().getId(), authRequest.getPrincipal())  
            // Allow subclasses to set the "details" property  
            setDetails(request, authRequest);  
            return this.getAuthenticationManager().authenticate(authRequest);  
        }  
        // 不是,就使用原来父类的key-value形式获取  
        return super.attemptAuthentication(request, response);  
    }  
}

补充

平台认证流程图

zw系统认证流程图

相关推荐
代码之光_198020 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi25 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇1 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生2 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料2 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck8 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风9 小时前
详解K8S--声明式API
后端