一、shiro中session的共享问题🍉
1.演示问题🥝
(1)启动shiro-springboot的集群项目🍓
(2)修改nginx的配置🍓
(3)测试🍓
使用swagger测试需要在过滤器中放行
//测试路径
http://localhost:8080/doc.html
登录成功后访问某些资源时,出现了未登录的json提示
2.如何解决session共享问题🥝
默认session存储再各自服务的内存中,可以让session统一存储再redis中。
疯狂的蛋糕的依赖。—提供了redis存储session的类。
修改shiro的配置类。
package com.lzq.config;import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.lzq.filter.LoginFilter;
import com.lzq.filter.MyWebSessionManagerextends;
import com.lzq.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;@Configuration
public class ShiroConfig {@Beanpublic DefaultWebSecurityManager securityManager(){DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();securityManager.setRealm(myRealm());//设置session管理器securityManager.setSessionManager(sessionManager());securityManager.setCacheManager(redisCacheManager());return securityManager;}@Beanpublic SessionManager sessionManager(){MyWebSessionManagerextends sessionManager = new MyWebSessionManagerextends();//SessionDao用于操作session对象,在容器中对session对象进行CRUD操作sessionManager.setSessionDAO(sessionDAO());return sessionManager;}@Beanpublic SessionDAO sessionDAO(){//该类会对session对象进行crud操作RedisSessionDAO sessionDAO = new RedisSessionDAO();sessionDAO.setRedisManager(redisManager());return sessionDAO;}/*** RedisSessionDAO shiro sessionDao层的实现 通过redis* 使用的是shiro-redis开源插件*/@Beanpublic MyRealm myRealm(){MyRealm myRealm=new MyRealm();//设置密码加密器myRealm.setCredentialsMatcher(credentialsMatcher());return myRealm;}@Value("${shiro.hashAlgorithmName}")private String hashAlgorithmName;@Value("${shiro.hashIterations}")private int hashIterations;@Beanpublic HashedCredentialsMatcher credentialsMatcher(){HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();credentialsMatcher.setHashAlgorithmName(hashAlgorithmName);credentialsMatcher.setHashIterations(hashIterations);return credentialsMatcher;}@Bean(name = "shiroFilter")public ShiroFilterFactoryBean factoryBean(){ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager());shiroFilterFactoryBean.setLoginUrl("/login.html");//设置过滤规则Map<String,String> map=new HashMap<>();map.put("/login","anon");map.put("/doc.html","anon");map.put("/webjars/**","anon");map.put("/swagger-resources/**","anon");map.put("/v2/**","anon");map.put("/**","authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(map);Map<String,Filter> filterMap=new HashMap<>();filterMap.put("authc",new LoginFilter());shiroFilterFactoryBean.setFilters(filterMap);return shiroFilterFactoryBean;}//springboot如何注册web三大组件。@Beanpublic FilterRegistrationBean<Filter> filterRegistrationBean(){FilterRegistrationBean<Filter> filterRegistrationBean=new FilterRegistrationBean<>();filterRegistrationBean.setFilter(new DelegatingFilterProxy());filterRegistrationBean.setName("shiroFilter");filterRegistrationBean.addUrlPatterns("/*");return filterRegistrationBean;}/*** 这里是为了能在html页面引用shiro标签,*/@Bean(name = "shiroDialect")public ShiroDialect shiroDialect() {return new ShiroDialect();}@Beanpublic RedisCacheManager redisCacheManager(){RedisCacheManager redisCacheManager = new RedisCacheManager();redisCacheManager.setRedisManager(redisManager());return redisCacheManager;}@Beanpublic RedisManager redisManager(){RedisManager redisManager = new RedisManager();redisManager.setHost("192.168.179.129");redisManager.setDatabase(1);return redisManager;}
}
二、解决前端不支持cookie的效果🍉
原因: 默认DefaultWebSessionManager它只接受Cookie中存储的JsessionId. 查询发现再redis中不存在对应的key.
客户发送请求时,再请求头中携带sessionId, 然后重写DefaultWebSessionManager中getSessionId()的方法。
思考:1. 如何把sessionId放入请求头。
2. 重写getSessionId方法如何获取请求头的sessionID。
1.如何把sessionId放入请求头🥝
修改登录的接口
@PostMapping("/login")@ResponseBodypublic Result login(@RequestBody LoginVo loginVo){Subject subject = SecurityUtils.getSubject();//判斷当前用户是否登陆过if (subject.isAuthenticated()){return new Result(200,"登陸成功",subject.getSession().getId());}UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getUsername(),loginVo.getPassword());try {subject.login(token);System.out.println("登录成功");return new Result(200,"登录成功",subject.getSession().getId());}catch (Exception e){e.printStackTrace();System.out.println("登录失败");return new Result(500,"登陆失败",null);}
修改前端登录方法
lzq(){this.$http.post("http://localhost:8080/login",this.formLabelAlign).then(requs=>{console.log(requs)if (requs.data.code==200){this.$message.success("登陆成功")sessionStorage.setItem("token",requs.data.data)this.$router.push("/aaa")}else {this.$message.error("账户密码错误")}})},
修改main.js文件
设置axios请求拦截器 进行拦截请求 将后端传过来的session id 赋值给 sessionStorage中 赋给请求头中
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/element.js'
import axios from "axios"
import it from "element-ui/src/locale/lang/it";router.beforeEach(((to, from, next) =>{//to:到哪去 from:从哪来 next:下一站var path = to.path;if (path=="/login"){return next();}//设置变量token 用来存储session idvar token = sessionStorage.getItem("token");if (token){return next();}return next("/login");
} ))
//设置axios的请求拦截器
axios.interceptors.request.use(config=>{//从sessionStorage中获取token token存储的session idvar item=sessionStorage.getItem("token");if (item){config.headers.token=item;}return config;
})Vue.prototype.$http=axiosVue.config.productionTip = falsenew Vue({router,render: h => h(App)
}).$mount('#app')
2.重写DefaultWebSessionManager的方法🥝
package com.lzq.filter;import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;public class MyWebSessionManagerextends extends DefaultWebSessionManager {private static final String AUTHORIZATION = "token";private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";protected Serializable getSessionId(ServletRequest request, ServletResponse response) {//获取请求头中名称为token的内容String id = WebUtils.toHttp(request).getHeader("token");if (!StringUtils.isEmpty(id)) { //如果存在该tokenrequest.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);return id;} else {//从cookie中获取sessionId.return super.getSessionId(request, response);}}}
重写DefaultWebSessionManager的方法是为了获取前端传过来的session id
重写后需要将配置文件交给ioc容器进行管理
@Bean(name = "shiroFilter")public ShiroFilterFactoryBean factoryBean(){ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager());shiroFilterFactoryBean.setLoginUrl("/login.html");//设置过滤规则Map<String,String> map=new HashMap<>();map.put("/login","anon");map.put("/doc.html","anon");map.put("/webjars/**","anon");map.put("/swagger-resources/**","anon");map.put("/v2/**","anon");map.put("/**","authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(map);Map<String,Filter> filterMap=new HashMap<>();filterMap.put("authc",new LoginFilter());shiroFilterFactoryBean.setFilters(filterMap);return shiroFilterFactoryBean;}
修改shiro配置类
@Beanpublic SessionManager sessionManager(){MyWebSessionManagerextends sessionManager = new MyWebSessionManagerextends();//SessionDao用于操作session对象,在容器中对session对象进行CRUD操作sessionManager.setSessionDAO(sessionDAO());return sessionManager;}
修改shiroFilter过滤器
我们发现跨域请求,会发送两个请求:第一个OPTIONS请求,第二个请求是真实的请求。
OPTIONS请求:先头部队。
所以我们对OPTIONS请求都要放行。
package com.lzq.filter;import com.alibaba.fastjson.JSON;
import com.lzq.vo.Result;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;public class LoginFilter extends FormAuthenticationFilter {//当未登录账号访问接口时,会触发该方法。//默认内容是重定向到登录页面---重写改为返回json数据@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {//解决响应数据的中文乱码问题response.setContentType("application/json;charset=utf-8");//通过io流的方式将响应返还给前端PrintWriter writer = response.getWriter();Result result = new Result(401, "请先登录", null);//把java对象转换为json字符串 ----JSON java封装的工具类String jsonstring = JSON.toJSONString(result);writer.print(jsonstring);//刷新io流writer.flush();//关闭io流资源writer.close();return false;}@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {//将请求转换格式 获取其中内容HttpServletRequest request1 = (HttpServletRequest) request;//获取请求方式String method = request1.getMethod();//判断将options访问类型进行放行if ("OPTIONS".equals(method)){return true;}return super.isAccessAllowed(request, response, mappedValue);}
}
三、设置前端前置路由守卫🍉
router.beforeEach(((to, from, next) =>{//to:到哪去 from:从哪来 next:下一站var path = to.path;if (path=="/login"){return next();}//设置变量token 用来存储session idvar token = sessionStorage.getItem("token");if (token){return next();}return next("/login");
} ))
四、如何防止恶意重复登录🍉
package com.lzq.controller;import com.lzq.vo.LoginVo;
import com.lzq.vo.Result;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;@Controller
//@CrossOrigin//origins:允许那些跨域访问该接口 allowedHeaders: 允许携带那些头信息的请求访问 methods: 允许那些请求跨域请求该接口
public class LoginController {@PostMapping("/login")@ResponseBodypublic Result login(@RequestBody LoginVo loginVo){Subject subject = SecurityUtils.getSubject();//判斷当前用户是否登陆过if (subject.isAuthenticated()){return new Result(200,"登陸成功",subject.getSession().getId());}UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getUsername(),loginVo.getPassword());try {subject.login(token);System.out.println("登录成功");return new Result(200,"登录成功",subject.getSession().getId());}catch (Exception e){e.printStackTrace();System.out.println("登录失败");return new Result(500,"登陆失败",null);}}@PostMapping("/logout")@ResponseBodypublic Result logout(){Subject subject = SecurityUtils.getSubject();//清空redissubject.logout();return new Result(200,"退出成功",null);}@GetMapping("/unlogin")@ResponseBodypublic Result unlogin(){return new Result(401,"请先登录",null);}}
五、退出功能🍉
编辑退出接口🥝
@PostMapping("/logout")@ResponseBodypublic Result logout(){Subject subject = SecurityUtils.getSubject();//清空redissubject.logout();return new Result(200,"退出成功",null);}
编辑前端退出按钮🥝
<template><div><el-button type="primary" @click="logout">退出</el-button><el-button @click="info" type="primary">获取用户信息</el-button><el-button @click="query" type="primary">查询</el-button><el-button @click="daochu" type="primary">导出</el-button><span v-text="userinfo.username"></span></div>
</template><script>export default {name: "Aaa",data(){return{userinfo:{}}},created() {},methods: {logout() {this.$http.post("http://localhost:8080/logout").then(requs=>{if (requs.data.code==200){this.$message.success("退出成功")sessionStorage.clear()this.$router.push("/login")}})},info(){this.$http.get("http://localhost:8080/user/info").then(requs=>{this.userinfo=requs.data.data;})},query(){this.$http.get("http://localhost:8080/user/query").then(requs=>{if (requs.data.code==200){this.$message.success("查询成功")}})},daochu(){}}}</script><style scoped></style>
六、获取当前登录用户的信息🍉
package com.lzq.controller;import com.lzq.vo.Result;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/info")public Result inf(){Object principal = SecurityUtils.getSubject().getPrincipal();return new Result(200,"查询成功",principal);}@GetMapping("/query")@ResponseBody
// @RequiresPermissions(value = {"user:query","user:delete"},logical = Logical.OR)@RequiresPermissions(value = "user:query")public Result query(){return new Result(200,"查詢成功",null);}@GetMapping("/update")@RequiresPermissions(value = "user:update")public String update(){return "user:update------------------------";}@GetMapping("/delete")@RequiresPermissions(value = "user:delete")public String delete(){return "user:delete------------------------";}@GetMapping("/insert")@RequiresPermissions(value = "user:insert")public String insert(){return "user:insert------------------------";}@GetMapping("/export")@RequiresPermissions(value = "user:export") //该注解不能别识别public String export(){return "user:export------------------------";}
}
<template><div><el-button type="primary" @click="logout">退出</el-button><el-button @click="info" type="primary">获取用户信息</el-button><el-button @click="query" type="primary">查询</el-button><el-button @click="daochu" type="primary">导出</el-button><span v-text="userinfo.username"></span></div>
</template><script>export default {name: "Aaa",data(){return{userinfo:{}}},created() {},methods: {logout() {this.$http.post("http://localhost:8080/logout").then(requs=>{if (requs.data.code==200){this.$message.success("退出成功")sessionStorage.clear()this.$router.push("/login")}})},info(){this.$http.get("http://localhost:8080/user/info").then(requs=>{this.userinfo=requs.data.data;})},query(){this.$http.get("http://localhost:8080/user/query").then(requs=>{if (requs.data.code==200){this.$message.success("查询成功")}})},daochu(){}}}</script><style scoped></style>