使用vue集成spring security进行安全登陆

在前后端分离的状态下,传统的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

核心配置类

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这个类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容