基于SpringSecurity的登录(SpringSecurity+Vue+ElementUI+axios前后端分离)

背景

考虑到Vue本身是一个渐进式的前端框架,在被案例中并没有使用Vue的页面组件化和路由,而是采用html页面+iframe(代替路由)。

同时,为了省去手写可复用Vue组件的工作量,所以使用ElementUI作为UI组件。

最后,ajax采用的是axios组件。

注意:即便后端采用的不是springsecurity进行登录,也有很多知识是相通的。

阳了三天,基本不烧了,只是听觉丧失50%,味觉丧失70%,献给CJ104那些爱卷的猴崽子们吧:)

全景代码

先不解释概念直接上代码

后端实现

从数据库中查询用户和密码信息,用于替换SpringSecurity自动产生密码

@Component
public class OAUserDetailService implements UserDetailsService {

    // 鉴于篇幅的原因,就不贴UserService的代码了,具体实现就是根据用户名查用户记录并返回
    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userService.getById(s);
        if (user == null) throw new UsernameNotFoundException(String.format("不存在%s用户", s));
        return new OAUserDetail(user);
    }

    public class OAUserDetail implements UserDetails{
        private User user;

        public OAUserDetail(User user){
            this.user = user;
        }

        @Override
        public Collection getAuthorities() {
            return new ArrayList<>();
        }

        @Override
        public String getPassword() {
            return user.getPassword();
        }

        @Override
        public String getUsername() {
            return user.getId();
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}

SpringSecurity配置

@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {
    // 临时方案,实战中不可取
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                        .successForwardUrl("/login/success") // 登录成功跳转到controller处理
                        .failureForwardUrl("/login/failure");
        http.cors();
        http.csrf().disable();
        http.authorizeRequests().anyRequest().authenticated(); // 没有这一句,将会直接绕过登录可以访问到controller
        http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());


    }
    
    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域访问的站点
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5500"));
        //允许跨域访问的methods
        corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST"));
        // 允许携带凭证
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //对所有URL生效
        source.registerCorsConfiguration("/**",corsConfiguration);
        return source;
    }
}

登录处理Controller

省略了演示过很多遍的R对象的代码,就是一个类包含了code、message、data三个属性的前后端分离响应类

@RestController
@RequestMapping("/login")
public class LoginController {
    @RequestMapping(value = "/success", method = {RequestMethod.GET, RequestMethod.POST})
    public R loginSuccess(){
        return R.error(401, "登录成功");
    }

    @RequestMapping(value = "/failure", method = {RequestMethod.GET, RequestMethod.POST})
    public R loginFailure(){
        return R.ok("登录失败");
    }
}

未登录异常处理类

public class OAAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        R r = R.error(401, "未登录");
        out.write(new ObjectMapper().writeValueAsString(r));
        out.flush();
        out.close();
    }
}

前端实现

登录页




    
    
    
    
    
    
    
    
    登录奇豆OA


    
登录

首页




    
    
    
    
    
    
    
    
    奇豆OA


    
{{sitem.name}}

前后端分离

前后端分离的的具体表现有两点

1、前端采用html技术来开发页面

2、前端页面与服务端Servlet分开部署在不同的web容器中

好处很明显:前端工程师不需要学习Servlet技术、前后端并行开发、减轻服务端Servlet容器的访问压力...

但带来的问题就包括:

跨域访问、登录session、Json响应,接下来将基于之前的代码针对这几个问题进行探讨

跨域描述

跨域是指在不同的域之间进行访问存在安全隐患,所以对请求进行了限制。跨域分为以下三种情况

http://127.0.0.1:8080 --> https://127.0.0.1:8080   协议跨域

http://127.0.0.1:8080 --> http://127.0.0.2:8080    IP跨域

http://127.0.0.1:8080 --> http://127.0.0.1:8081    端口跨域 

注意,如下两种情况不涉及跨域

1 如果是前后端不分离,即将页面和服务端代码运行在同一个Servlet容器中自然不涉及跨域

2 直接在浏览器中输入服务端Controller的URL,相当于local --> server 也谈不上跨域

解决跨域

解决跨域的办法有好几种,基于之前的案例代码,我们采用服务端解决跨域

非SpringSecurity的场景

  1. 最简单粗暴的做法为Controller添加 @CrossOrigin注解,意味着允许跨域访问。 缺点很明显,服务端接口完全暴露在了危险的网络环境中
  2. 精细化控制跨域访问,可以通过自定义Cors过滤器,具体实现如下
import org.springframework.http.HttpHeaders;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 只允许127.0.0.1:5500跨域访问
        corsConfiguration.addAllowedOrigin("http://127.0.0.1:5500");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader(HttpHeaders.COOKIE);
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://127.0.0.1:5500")
                .allowedMethods("GET","POST")
                .allowCredentials(true)
                .allowedHeaders("*");
    }
}

注意:需要引入的依赖包org.springframework.web.cors.xxx,所以特意贴了import代码

SpringSecurity的场景

在使用了springSecurity的情况下你会发现之前的@CrossOrigin注解也解决不了跨域问题了,原因是springsecurity默认情况下并不允许跨域访问,通过如下设置可以解决该问题

@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启允许跨域访问
        http.cors();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域访问的站点
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5500"));
        //允许跨域访问的methods
        corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST"));
        // 允许携带凭证
        corsConfiguration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //对所有URL生效
        source.registerCorsConfiguration("/**",corsConfiguration);
        return source;
    }
}

至此,跨域问题基本可以解决了

那么接下来就会发现一个问题,登录成功后index页面仍然无法发起请求(根据ParentID获取菜单列表)

 该问题就是没有session ID导致的

session ID的描述

session是一种用来解决http协议无状态的重要技术。具体是如何解决无状态的呢?

首先,当浏览器访问服务端,且使用的session时,servlet容器(Tomcat)会为其创建一个Session对象,并为其生存一个session id(JSESSIONID),并通过写cookie的方式将sesssion id写入到浏览器的cookie中,如下图

基于SpringSecurity的登录(SpringSecurity+Vue+ElementUI+axios前后端分离)_第1张图片

 然后,浏览器将自动保存session ID 

如下图

最后,当浏览器再次发起其他请求时,将会自动的携带该session ID,如下图(总算把线划直了)

 结论:session + cookie解决http无状态的问题

嗯.... 这和登不登录有和关系???

session的创建并不是用来解决登录问题的,但是,我们可以通过这一技术来简单的实现登录,这取决于登录时创建session,并将用户信息存入session,来点伪代码吧

// 由于是伪代码所以就没有写注解了,方法的返回值就随意些一下,不要照抄
public class LoginController{
    
    public String login(HttpServletRequest request, String username, String password){
        // 执行这句将会创建Session
        Session session = request.getSession()
        // 根据用户名获取用户信息
        User use = getUser(username)
        // 验证密码
        boolean result = verifyLogin(use, password)
        // 保存用户信息到session,注意不要保存密码等敏感信息到Session
        session.setAttribute("user", username)
    }
}

这样一来,我们就可以在拦截器中取检查对于session id对于的session中是否有user属性

解决Session ID未保存的问题

经过上一小节的说明,相信大家对session id与登录的关系了。但是,在跨域的情况下浏览器并不能正常的保存session id。

注意:session id的话题与springsecurity无关,无论采用什么方式实现登录都一样。除非采用Token记录登录的方式

浏览器在跨域的情景下不能正常保存session id,其根本原因是ajax的异步请求,需要通过在异步回调中为浏览器保存session id。

在全景代码中通过Axios发起ajax请求有如下一段代码

new Vue({
        el: '#app',
        data: {...},
        methods: {
            login(){
                axios({
                    method: 'post',
                    url: 'http://127.0.0.1:8080/login',
                    //  就是他啦~~~~~~~~~~~~
                    withCredentials: true,
                    data: Qs.stringify({
                        username: this.form.username,
                        password: this.form.password
                    })
                }).then((response) => {...})
            }
        }
    })

withCredentials: true,的选项则意味着浏览器保存认证信息(即保存session id),同时,也会在发起请求时自动带上seesion id。所以,所有的请求的需要加上该选项。

至此,session id的问题可以解决

但是,前后端分离的一大特性就是,前后端之间通过JSON来传递响应结果,那么,在spring security接管登录处理、登录失败、登录成功等处理之后如何替换掉用默认的页面跳转,而改为返回Json响应呢。

Json响应

  • 首先,Spring Security在用户未登录时访问资源会将请求重定向到登录页,解决该问题可以如下:

对于全局代码中Security配置类关键代码为:

@Configuration
@Slf4j
public class OASecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 就他啦~~~~
        http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());
    }
}
http.exceptionHandling().authenticationEntryPoint(new OAAuthenticationEntryPoint());

意味着,当未登录时会触发异常,并触发EntryPoint(即,跳转登录页),只需要自己定义该EntryPoint便可以接管默认行为,改为返回Json响应了。

至于代码中的OAAuthenticationEntryPoint,在全景代码部分已经有贴过,就不重复了

  • 其次Spring Security登录成功与失败的处理可以设置返回结果为Json响应
@Override
protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                        .successForwardUrl("/login/success")
                        .failureForwardUrl("/login/failure");
}
successForwardUrl与failureForwardUrl

可以指定Controller来处理登录成功与失败。至于该Controller也在全景代码部分已经有贴过,就不重复了

至此,前后端分离的登录的问题已经解决。

注意:前端部分axios代码,需自行优化

你可能感兴趣的:(系统登录,vue.js,elementui,ajax)