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 这个项目中。
项目过滤器示意图。