前后端完全分离实现登录和退出

前后端分离的整合

使用springsecurity+前端项目+redis完成认证授权的代码

1. 搭建一个前端工程

使用 vue ui搭建,使用webstrom操作

2. 创建一个登录页面

<template>
  <div class="login_container">
    <!-- 登录盒子  -->
    <div class="login_box">
      <!-- 头像 -->
      <div class="avatar_box">
        <img src="../assets/3.jpg" alt="">
      </div>
      <!-- 登录表单 -->
      <el-form :model="loginForm" ref="LoginFormRef"  label-width="0px" class="login_form">
        <!-- 用户名 -->
        <el-form-item prop="username">
          <el-input v-model="loginForm.username" prefix-icon="el-icon-user-solid" ></el-input>
        </el-form-item>
        <!-- 密码 -->
        <el-form-item prop="password">
          <el-input type="password" v-model="loginForm.password" prefix-icon="el-icon-s-grid"></el-input>
        </el-form-item>
        <!-- 按钮 -->
        <el-form-item class="btns">
          <el-button type="primary" >登录</el-button>
          <el-button type="info">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>
<script>
export default {
  name: "Login",
  data() {
    return {
      //数据绑定
      loginForm: {
        username: '张三',
        password: '123456'
      },

    }
  },
  methods:{

  }
}
</script>

<style  scoped>
.login_container {
  background-color: #2b5b6b;
  height: 100%;
}
.login_box {
  width: 450px;
  height: 300px;
  background: #fff;
  border: 1px solid #42b983;
  border-radius: 3px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);

}
.login_box>.avatar_box {
  height: 130px;
  width: 130px;
  border: 1px solid #eee;
  border-radius: 50%;
  padding: 10px;
  box-shadow: 0 0 10px #ddd;
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;

}
.login_box>.avatar_box>img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background-color: #eee;
}
.login_form {
  position: absolute;
  bottom: 0;
  width: 100%;
  padding: 0 20px;
  box-sizing: border-box;
}
.btns {
  display: flex;
  justify-content: flex-end;
}
</style>
  1. 在views中创建视图——登录页面

  2. 在router的index.js中配置路由

{
    path: '/',
    name: 'home',
    //重定向,使之打开页面就跳转到登录页面
    redirect: '/login',
  },
    //配置登录页面的路由
  {
    path: '/login',
    name:'login',
    component: () => import('../views/Login.vue'),
  },
  1. 在App.vue默认组件中进行路由渲染
<template>
  <div id="app">
   <router-view/>
  </div>
</template>

3. 登录按钮的点击事件

1. 首先,需要在main.js全局配置中全局配置axios

  1. 导入axios
  2. 设置axios的基础路径:端口号为后端项目的端口号
  3. 将axios挂载到vue对象中
import axios form 'axios'//导入
axios.dafaults.baseURL="基础路径http://ip:端口号" //配置axios的基础路径,便于在进行axios请求时将其省略
Vue.prototype.$名称=axios//将axios挂载到vue对象中。axios要与导入时的import后的名称一致。

2. 然后,在login页面的登录页面添加点击事件

  1. 在loginForm表单中添加表单验证——>:rules关键字和ref
  2. 在data中添加表单验证的规则
  3. 实现点击事件
  • 表单验证

    在这里插入图片描述

  • 验证规则

    在这里插入图片描述

  • 登录点击事件

    myLogin(){
          this.$refs.LoginFormRef.validate(valid => {
            if(valid){
              this.$axios.post('/login?username='+this.loginForm.username+'&password='+this.loginForm.password).then(res=>{
                if(res.data.code == 200){
                  this.$message.success("登录成功")
                  //将token保存到sessionStorage,类似于cookie
                    sessionStorage.setItem("token",res.data.data)
                  //跳转到后台
                  this.$router.push("/home")
                }else{
                  this.$message.error("登录失败")
                }
              })
            }
          })
        }
    

在该请求中,将获取到的token存放到sessionStorage中,通过setItem方法

解决跨域问题

此时允许点击“登录”按钮,会报错,出现跨域问题

在这里插入图片描述

跨域问题:通过ajax从一个服务访问另一个服务时,出现跨域问题

服务:只要ip或端口或协议不同,都称为不同的域

如何解决:

由两种方式:前端解决和后端解决,这里仅讲解后端的解决方式

  • 后端解决

    在config中创建一个用于解决跨域问题的配置类

    @Configuration
    public class AllowOriginConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**") // 所有接口
                    .allowCredentials(true) // 是否发送 Cookie
                    .allowedOrigins("*")//支持域
                   // .allowedOriginPatterns("*") // 支持域
                    .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) // 支持方法
                    .allowedHeaders("*")
                    .exposedHeaders("*");
        }
    }
    

    在security配置类中添加登录允许跨域的设置

    因为其他需求可以,而login不行,因为login是security写的,不止自己写的

    http.cors();
    
     @Override
        protected void configure(HttpSecurity http) throws Exception {
            //把自定义的过滤器放在之前
            http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
            http.formLogin()
                    //登录页面;
                    //登录的处理路径 默认 /login
                    .loginProcessingUrl("/login")
                    .successHandler(successHandler())//登录成功
                    .failureHandler(failureHandler()) //登录失败转发的路径 必须为post请求
                    .permitAll(); //上面的请求路径无需认证
            //指定权限不足跳转的页面
            http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    
            http.csrf().disable();//禁用跨域伪造请求的过滤器
            //security登录允许跨域
            http.cors();
            //除了上的请求,其他请求都需要认证
            http.authorizeRequests().anyRequest().authenticated();
        }
    

4. 前置路由守卫

前置路由守卫用于判断有没有登录

放在main.js或/router/index.js中

在设置前置路由守卫之前,在没有登录的情况下,可以直接访问登录后的页面——不符合现实,所以使用前置路由守卫,来判断有没有登录

  1. 先查看路径,若用户访问的是登录页面/login,直接放行
  2. 若不是,就获取sessionStorage中保存的token值
  3. 若token不存在,或者token为undefined,就强制跳转到/login登录页面
  4. 如果token存在,直接放行
//前置路由守卫
//to:即将要访问的路径
//from:从哪里来
//next:放行函数
router.beforeEach((to,from,next)=>{
      //如果用户访问的是登录页面,直接放行
  if(to.path === '/login'){
    //放行
    return next();
  }
  //获取sessionStorage中保存的token值
  const token = window.sessionStorage.getItem('token');
  //如果token不存在,强制跳转到登录页面
  if(!token){
    return next("/login");
  }
  //如果token存在,直接放行
  next();
})

5. 设置携带token令牌:请求拦截器

在未设置请求拦截器之前,成功登录后,点击按钮均显示“未登录”,是因为请求没有携带token令牌,后端判断为未登录

放在main.js中

//设置请求拦截器——携带token令牌
axios.interceptors.request.use(config=>{
  var token = sessionStorage.getItem("token");
  if(token){
    config.headers.token = token;
  }
  return config;
})

6. 设置响应拦截器

确保后端,返回类型为R类型

main.js中设置响应拦截器

  • 后端示例
@PreAuthorize("hasAuthority('user:query')")
    @GetMapping("/select")
    public R select(){
        System.out.println("查询用户");
        return new R(200,"查询用户","查询用户");
    }

  • 前端响应拦截器
//设置响应拦截器
axios.interceptors.response.use(response=>{
  //如果返回的code为200,为成功,显示返回的信息
  if(response.data.code===200){
    Vue.prototype.$message.success(response.data.msg);
    return response;
  }else {
    //否则,为失败,显示返回的信息
    Vue.prototype.$message.error(response.data.msg);
    return response;
  }
})

设置响应拦截器之后,就可以通过响应拦截器将后端的信息显示到前端页面,无需在axios请求中再次处理相关msg

  • 示例

    methods:{
        query(){
          this.$axios.get('/user/select').then(res=>{})
        },
        insert(){
          this.$axios.get('/user/add').then(res=>{})
        },
        del(){
          this.$axios.get('/user/delete').then(res=>{})
        },
        update(){
          this.$axios.get('/user/update').then(res=>{})
        },
        myExport(){
          this.$axios.get('/user/export').then(res=>{})
        }
      },
    
    

7. 退出

后端:借助redis完成。

  1. 在登录时,登录成功后,将token存入到redis中
  2. 在配置时,自定义实现退出接口,使其在退出登录后,删除redis中的缓存信息,并返回json数据,而不是页面
  3. 在登录过滤器中,验证token时加判断,判断redis中是否存在登录的token

因为要借助redis,所以需要导入redis,并在配置文件中进行配置,并且开启装有redis的虚拟机,开启redis服务

  • 引入依赖

     <!--引入redis依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
    
  • 在配置文件中添加redis相关配置

    #Redis服务器连接端口
    spring.redis.port=6379
    #Redis服务器地址
    spring.redis.host=172.16.7.112
    
    
  • 在登录成功后将token添加到redis中

    //将token存入redis缓存中
    redisTemplate.opsForValue().set("login:"+token,"");

    @Autowired
        private StringRedisTemplate redisTemplate;
        //登录成功需要返回的json数据
        private   AuthenticationSuccessHandler successHandler(){
          return new AuthenticationSuccessHandler() {
              @Override
              public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                  //设置响应编码
                  httpServletResponse.setContentType("application/json;charset=utf-8");
                  //获取输出对象
                  PrintWriter writer = httpServletResponse.getWriter();
                  //返回json数据
                  Map<String,Object> map=new HashMap<>();
    
                  map.put("username",authentication.getName());
                  //获取权限信息列表
                  Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                 //获取权限标识码
                  List<String> collect = authorities.stream().map(item -> item.getAuthority()).collect(Collectors.toList());
                  map.put("permission",collect);
                  String token = JWTUtil.createToken(map);
                  //将token存入redis缓存中
                    redisTemplate.opsForValue().set("login:"+token,"");
                  //返回一个统一的json对象
                  R r=new R(200,"登录成功!",token);
                  //转换未json字符串
                  String s = JSON.toJSONString(r);
                  //servlet
                  //发送响应到客户端
                  writer.println(s);//将JSON字符串写入到HTTP响应中
                  writer.flush();//确保所有缓冲的输出都被发送到客户端
                  writer.close();//关闭PrintWriter
    
              }
          };
        }
    
    
  • 在security配置文件中重写退出方法

    //退出登录后,删除redis中的缓存信息
    redisTemplate.delete("login:"+token);

     @Override
        protected void configure(HttpSecurity http) throws Exception {
            //把自定义的过滤器放在之前
            http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
            http.formLogin()
                    //登录页面;
                    //登录的处理路径 默认 /login
                    .loginProcessingUrl("/login")
                    .successHandler(successHandler())//登录成功
                    .failureHandler(failureHandler()) //登录失败转发的路径 必须为post请求
                    .permitAll(); //上面的请求路径无需认证
            //指定权限不足跳转的页面
            http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    
            http.csrf().disable();//禁用跨域伪造请求的过滤器
            //退出,重写使其返回json数据,而不是网页
            http.logout(item->{
                item.logoutSuccessHandler((httpServletRequest, httpServletResponse, e) -> {
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    PrintWriter writer = httpServletResponse.getWriter();
                    String token = httpServletRequest.getHeader("token");
                    //退出登录后,删除redis中的缓存信息
                    redisTemplate.delete("login:"+token);
                    R r=new R(200,"退出成功",null);
                    String jsonString =JSON.toJSONString(r);
                    writer.println(jsonString);
                    writer.flush();
                    writer.close();
                });
            });
            //security登录允许跨域
            http.cors();
            //除了上的请求,其他请求都需要认证
            http.authorizeRequests().anyRequest().authenticated();
        }
    
    
  • 在登录过滤器中添加判断条件

    !redisTemplate.hasKey("login:"+token

      //3. 验证token
         if(!JWTUtil.verify(token)||!redisTemplate.hasKey("login:"+token)){
             PrintWriter writer = httpServletResponse.getWriter();
             //返回一个token失效的json数据
             R r=new R(500,"token失效!",null);
             String s = JSON.toJSONString(r);
             writer.write(s);
             writer.flush();
             writer.close();
             return;
         }
    
    
  • 前端,添加退出点击事件,退出后删除sessionStorage中的token信息,并跳转返回登录页面

     logout(){
          this.$axios.post("/logout").then(res=>{
            //移除sessionStorage中的token
            sessionStorage.removeItem("token")
            //跳转到登录页面
            this.$router.push("/login")
          })
        }
    
    
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值