前后端分离的整合
使用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,类似于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中
在设置前置路由守卫之前,在没有登录的情况下,可以直接访问登录后的页面——不符合现实,所以使用前置路由守卫,来判断有没有登录
- 先查看路径,若用户访问的是登录页面
/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,"");
@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") }) }