在前后端分离的状态下,传统的spring security认证模式也需要做一点改造,以适应ajax的前端访问模式
现在前后端分离的开发模式已经成为主流,好处不多说了,说说碰到的问题和坑。首先要解决的肯定是跨域问题,这个问题之前已经有讨论,请移步这里查看。另外一个问题是传统的spring security安全机制是基于页面跳转的,使用302重定向(认证成功跳转至之前访问的页面,认证失败或未认证跳转至系统设置的默认登陆页面)。传统应用这么弄没问题,但现在vue一般都是基于axios进行ajax访问,ajax请求是没法直接处理302跳转的(浏览器会直接处理跳转请求,ajax的callback拿到的是跳转后的返回页面,在spring security中就是登陆首页,不符合需求)。幸好spring security所有的流程都是可以自定义的,我们可以扩展一下各个环节的流程。spring boot版本为2.1.4.RELEASE,对应的spring security版本为5.1.5.RELEASE
推荐阅读:
Java 面试常见问题总结(2024 最新版)文章浏览阅读131次,点赞2次,收藏7次。内容概览:涵盖了 Java 基础、集合框架、并发编程、JVM 、操作系统、MySQL、Redis、ES、系统设计、分布式、高并发等知识点的面试题,全部都是面试常问的高频面试题。并且,会给出相应的参考答案(文章链接)。准备技术面试的过程中,可以对照着这份 PDF 来进行自测,这是一种非常不错的学习和复习方式。https://blog.csdn.net/x1ao_fe1/article/details/144120328?spm=1001.2014.3001.5501
核心配置类
WebSecurityConfig
类配置如下
import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import com.bocsh.mer.security.MyUserDetailsService; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MyUserDetailsService myDetailService; @Autowired private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource; protected Log log = LogFactory.getLog(this.getClass()); @Autowired private AuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } //定义登陆成功返回信息 private class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal(); log.info("商户[" + SecurityContextHolder.getContext().getAuthentication().getPrincipal() +"]登陆成功!"); //登陆成功后移除session中验证码信息 request.getSession().removeAttribute("codeValue"); request.getSession().removeAttribute("codeTime"); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("{\"status\":\"ok\",\"msg\":\"登录成功\"}"); out.flush(); out.close(); } } //定义登陆失败返回信息 private class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //登陆失败后移除session中验证码信息 request.getSession().removeAttribute("codeValue"); request.getSession().removeAttribute("codeTime"); response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); PrintWriter out = response.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"请检查用户名、密码或验证码是否正确\"}"); out.flush(); out.close(); } } //定义异常返回信息 public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpStatus.UNAUTHORIZED.value(),authException.getMessage()); } } //定义登出成功返回信息 private class AjaxLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("{\"status\":\"ok\",\"msg\":\"登出成功\"}"); out.flush(); out.close(); } } @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint()) .and() .csrf().disable() .authorizeRequests() .antMatchers("/users/login_page","/users/captcha").permitAll() .anyRequest().authenticated() .and().formLogin().loginPage("/users/login_page") .successHandler(new AjaxAuthSuccessHandler()) .failureHandler(new AjaxAuthFailHandler()) .loginProcessingUrl("/login") .authenticationDetailsSource(authenticationDetailsSource) .and() .logout().logoutSuccessHandler(new AjaxLogoutSuccessHandler()) .logoutUrl("/logout"); } }
复制
这里分别说明:configure
方法配置整体的认证规则,即除了/users/login_page
和/users/captcha
这两个方式之外全部需要认证,同时在formLogin
方法中设置了两个自定义的handler分别处理认证成功、认证失败的情况,这里我们设置直接返回json字符串,content-type设置为application/json;charset=utf-8
,这样在认证完毕之后就不用自动302跳转了,而是交由axios框架来进行处理。
UnauthorizedEntryPoint
定义了异常处理的逻辑,同理我们也是直接返回ajax信息而不做跳转。
axios相关配置
instance.interceptors.response.use(res => { let { data } = res this.destroy(url) if (this.shade) { Spin.hide() Modal.success({ title: '操作成功' }) } console.log(res) return data }, error => { console.log(error) var code = error.response.status if (code === 401) { Cookies.remove(TOKEN_KEY) window.location.href = '/login' Message.error('未登录,或登录失效,请登录') }
复制
这里面我们定义了一个响应拦截器,在error情况下,判断若返回码为401(就是我们在spring security中自定义的handler的错误状态码),则自动跳转至登陆页面。这样实现了在会话失效的情况下,点击前端任意需要访问后端api的按钮,均会触发跳转登录首页的效果,符合我们的预期。实际情况中一般前端框架都会自己带一套基于cookies的认证机制,这里我们把cookies的失效时间可以设置的长一点(一般可以设为一天),以保障还是以后端会话的失效时间为准。
验证码处理
一般我们在处理用户登录,为安全考虑需要加入图形验证码。这里需要注意的一点是验证码也必须作为login这个action的处理参数传入,否则的话这个验证码只是做了前端页面验证,实际在处理login事件的时候是没有验证码的,这样就没有意义了。而spring security框架默认只能帮助我们处理用户名+密码的这样验证方式,这样就需要对认证方式进行扩展。
WebAuthenticationDetails
import javax.servlet.http.HttpServletRequest; import org.springframework.security.web.authentication.WebAuthenticationDetails; public class MyWebAuthenticationDetails extends WebAuthenticationDetails { /** * */ private static final long serialVersionUID = 6975601077710753878L; private String username; private String password; private String validcode; private String sessionCodeValue; private long sessionCodeTime; public String getSessionCodeValue() { return sessionCodeValue; } public void setSessionCodeValue(String sessionCodeValue) { this.sessionCodeValue = sessionCodeValue; } public long getSessionCodeTime() { return sessionCodeTime; } public void setSessionCodeTime(long sessionCodeTime) { this.sessionCodeTime = sessionCodeTime; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getValidcode() { return validcode; } public void setValidcode(String validcode) { this.validcode = validcode; } public MyWebAuthenticationDetails(HttpServletRequest request) { super(request); username = request.getParameter("username"); password = request.getParameter("password"); validcode = request.getParameter("validateCode"); sessionCodeValue = (String)request.getSession().getAttribute("codeValue"); sessionCodeTime = (Long)request.getSession().getAttribute("codeTime"); } }
复制
这边前三个参数是从页面的login请求中传过来的,后面两个参数是在页面上请求生成图片验证码的时候我们设置到session里面去,以便于后续的验证
AuthenticationDetailsSource
import javax.servlet.http.HttpServletRequest; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.stereotype.Component; @Component public class MyAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> { @Override public WebAuthenticationDetails buildDetails(HttpServletRequest context) { return new MyWebAuthenticationDetails(context); } }
复制
这个类实现了一个自定义的接口,返回我们刚才定义的MyWebAuthenticationDetails
资源
AuthenticationProvider
import java.util.ArrayList; import java.util.Collection; import java.util.Date; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; import com.bocsh.mer.service.MerchantService; @Component public class MyAuthenticationProvider implements AuthenticationProvider { protected Log log = LogFactory.getLog(this.getClass()); @Autowired MerchantService ms; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { log.info("now start custom authenticate process!"); MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails(); //校验码判断 if(!details.getValidcode().equals(details.getSessionCodeValue())) { log.info("validate code error"); throw new BadCredentialsException("authenticate fail!"); } //校验码有效期 if((new Date()).getTime() - details.getSessionCodeTime() > 60000) { log.info("validate code expired!"); throw new BadCredentialsException("authenticate fail!"); } //验密 String inpass=""; try { inpass = ms.getPass(details.getUsername()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } if(!inpass.equals(details.getPassword())) { log.info("password error"); throw new BadCredentialsException("authenticate fail!"); } Collection<GrantedAuthority> auths=new ArrayList<GrantedAuthority>(); auths.add(new SimpleGrantedAuthority("ROLE_ADMIN")); return new UsernamePasswordAuthenticationToken(details.getUsername(),details.getPassword(),auths); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
复制
这里自定义了一个AuthenticationProvider
来处理实际的认证业务逻辑,在这里可以方便的根据我们需要来进行自定义,我这边分别做了验证码校验、效期校验和验密,大家可以根据需要定制。认证成功就返回一个UsernamePasswordAuthenticationToken
对象并配置好合适的权限(权限基于标准的RBAC模型,我这里为了讲解方便设置为ROLE_ADMIN,这里不展开讲了)。如果认证失败,只需要抛出一个异常(AuthenticationException的子类),这样spring security会自动找到失败页面进行返回,对应我们上面定义的AjaxAuthFailHandler
这个类。
推荐阅读:
Java 面试常见问题总结(2024 最新版)文章浏览阅读131次,点赞2次,收藏7次。内容概览:涵盖了 Java 基础、集合框架、并发编程、JVM 、操作系统、MySQL、Redis、ES、系统设计、分布式、高并发等知识点的面试题,全部都是面试常问的高频面试题。并且,会给出相应的参考答案(文章链接)。准备技术面试的过程中,可以对照着这份 PDF 来进行自测,这是一种非常不错的学习和复习方式。https://blog.csdn.net/x1ao_fe1/article/details/144120328?spm=1001.2014.3001.5501