spring boot 集成 spring security 实现json串登录和短信验证码登录(1)

问题:用spring security 实现json串登录方式一般用来解决前后端分离的登录问题的处理,前端通过输入用户名密码json串发送后端由security验证登录,登录成功返回登录成功标识token。以后请求只需带token即可通过验证。这其中涉及到几个问题:

    1.如何让spring security 校验我们自定义的json串登录过滤器

    2.登录成功后,后续请求如何让spring security 验证token来实现自动认证

那么,解决这两个问题,首先得看spring security登录的实现方式,spring security实现登录是通过一系列过滤器链来最终来完成登录,所以我们需要自定义一个json登录和校验过滤器加入到security的过滤器链。而且我们通过spring security 的UsernamePasswordAuthenticationFilter.class源码 

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
        String username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

发现我们只需将用户名密码传给UsernamePasswordAuthenticationToken类并调用UsernamePasswordAuthenticationFilter的this.getAuthenticationManager().authenticate(authRequest)方法即可实现框架的自动认证

首先我们需要定义一个Json 用户名密码登录配置器

/**
 * Json 用户名密码登录配置文件(配置器)
 *
 * @author liaofuxing
 * @date 2020/02/18 11:50
 */
@Configuration
public class JsonAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler defaultAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler defaultAuthenticationFailureHandler;


    @Override
    public void configure(HttpSecurity http) throws Exception {

        JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter();
        jsonAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        jsonAuthenticationFilter.setAuthenticationSuccessHandler(defaultAuthenticationSuccessHandler);
        jsonAuthenticationFilter.setAuthenticationFailureHandler(defaultAuthenticationFailureHandler);

        http.addFilterAfter(jsonAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

设置登录成功失败的Handler,登录成功Handler里面实现成功表示token的返回,具体代码稍后在gitee中查看,这里就不一一列出,和验证登录的过滤器jsonAuthenticationFilter,

并模仿UsernamePasswordAuthenticationFilter自定义JsonAuthenticationFilter过滤器

/**
 * Json 用户名密码登录过滤器
 *
 * @author liaofuxing
 * @date 2020/02/18 11:50
 */
public class JsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   private boolean postOnly = true;

   public JsonAuthenticationFilter() {
      super(new AntPathRequestMatcher("/user/login", "POST"));
   }

   @Override
   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {

         if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
         }

         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         dbf.setExpandEntityReferences(false);
         StringBuffer sb = new StringBuffer();
         try (InputStream inputStream = request.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
            String str;
            while ((str = bufferedReader.readLine()) != null) {
               sb.append(str);
            }
         } catch (IOException ex) {
            throw new RuntimeException("获取请求内容异常", ex);
         }

         JSONObject jsonObject = JSON.parseObject(sb.toString());
         String username = jsonObject.getString("username");
         String password = jsonObject.getString("password");

         UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);

         return this.getAuthenticationManager().authenticate(authenticationToken);
      }

}

与UsernamePasswordAuthenticationFilter一样继承AbstractAuthenticationProcessingFilter重写attemptAuthentication方法实现框架的自动认证

ps:UsernamePasswordAuthenticationFilter的自动登录认证是通过定义的UserDetailServiceImpl来实现用户名密码校验的,所以先要定义好UserDetailServiceImpl 和User实体类。这些都是先决条件。

将我们定义的JsonAuthenticationConfigurer 添加到spring security的配置链中去,

@Override
protected void configure(HttpSecurity http) throws Exception {
    //处理跨域请求
    http.cors().and().csrf().disable()
            .apply(jsonAuthenticationConfigurer)
            .and()
            .apply(springSocialConfigurer)
            .and()
            .apply(smsCodeAuthenticationConfigurer)
            .and()
            //权限不足结果处理
            .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)
            .and()
            //设置登出url
            .logout().logoutUrl("/user/logout")
            //设置登出成功处理器(下面介绍)
            .logoutSuccessHandler(logoutSuccessHandler).and()
            .authorizeRequests()
            .antMatchers("/authentication/require",
                    "/sms/*",
                    "/user/regist").permitAll()
            .antMatchers("/user/lala/**").hasRole("ADMIN")
            .anyRequest()
            .authenticated();

    /*  authorizationFilter是用来拦截登录请求判断请求中是否带有token,并且token是否有对应的已经登录的用户,如果有应该直接授权通过
     *  所以这个过滤器应该在UsernamePasswordAuthenticationFilter过滤器之前执行,所以放在LogoutFilter之后
     */
    http.addFilterAfter(authorizationFilter, LogoutFilter.class);


}

这个是完整配置,jsonAuthenticationConfigurer是我们添加进去的。

这样就实现了json串形式的登录,解决了问题1,

问题2,实现后续请求的token校验,同样是定义过滤器,添加到security 过滤器链

代码

@Component
public class TokenAuthorizationFilter extends OncePerRequestFilter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private UserDetailServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //从请求头中取出token
        String token = request.getHeader("token");
        if(!StringUtils.isEmpty(token)) {
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                //redis中获取用户名
                String username = redisTemplate.opsForValue().get("SECURITY_TOKEN:"+ token);
                //从数据库中根据用户名获取用户
                UserDetails systemUser = userDetailsService.loadUserByUsername(username);
                if (systemUser != null) {
                    //解析并设置认证信息
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(systemUser, null, systemUser.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

以上只是部分代码,主要是梳理流程,具体代码:spring cloud学习实例代码

代码在  api-gateway 这个项目中。

项目过滤器示意图。