前后端分离的整合
使用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>
在views中创建视图——登录页面
在router的index.js中配置路由
{path: '/',name: 'home',//重定向,使之打开页面就跳转到登录页面redirect: '/login',},//配置登录页面的路由{path: '/login',name:'login',component: () => import('../views/Login.vue'),},
- 在App.vue默认组件中进行路由渲染
<template><div id="app"><router-view/></div> </template>
3. 登录按钮的点击事件
1. 首先,需要在main.js全局配置中全局配置axios
- 导入axios
- 设置axios的基础路径:端口号为后端项目的端口号
- 将axios挂载到vue对象中
import axios form 'axios'//导入
axios.dafaults.baseURL="基础路径http://ip:端口号" //配置axios的基础路径,便于在进行axios请求时将其省略
Vue.prototype.$名称=axios//将axios挂载到vue对象中。axios要与导入时的import后的名称一致。
2. 然后,在login页面的登录页面添加点击事件
- 在loginForm表单中添加表单验证——>:rules关键字和ref
- 在data中添加表单验证的规则
- 实现点击事件
-
表单验证
-
验证规则
-
登录点击事件
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,类似于cookiesessionStorage.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 {@Overridepublic 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();
@Overrideprotected 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中
在设置前置路由守卫之前,在没有登录的情况下,可以直接访问登录后的页面——不符合现实,所以使用前置路由守卫,来判断有没有登录
- 先查看路径,若用户访问的是登录页面
/login
,直接放行- 若不是,就获取sessionStorage中保存的token值
- 若token不存在,或者token为undefined,就强制跳转到
/login
登录页面- 如果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完成。
- 在登录时,登录成功后,将token存入到redis中
- 在配置时,自定义实现退出接口,使其在退出登录后,删除redis中的缓存信息,并返回json数据,而不是页面
- 在登录过滤器中,验证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,"");
@Autowiredprivate StringRedisTemplate redisTemplate;//登录成功需要返回的json数据private AuthenticationSuccessHandler successHandler(){return new AuthenticationSuccessHandler() {@Overridepublic 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);
@Overrideprotected 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. 验证tokenif(!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中的tokensessionStorage.removeItem("token")//跳转到登录页面this.$router.push("/login")})}