一、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 {
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
//设置session管理器
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
@Bean
public SessionManager sessionManager(){
MyWebSessionManagerextends sessionManager = new MyWebSessionManagerextends();
//SessionDao用于操作session对象,在容器中对session对象进行CRUD操作
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
@Bean
public SessionDAO sessionDAO(){
//该类会对session对象进行crud操作
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public MyRealm myRealm(){
MyRealm myRealm=new MyRealm();
//设置密码加密器
myRealm.setCredentialsMatcher(credentialsMatcher());
return myRealm;
}
@Value("${shiro.hashAlgorithmName}")
private String hashAlgorithmName;
@Value("${shiro.hashIterations}")
private int hashIterations;
@Bean
public 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三大组件。
@Bean
public 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();
}
@Bean
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
@Bean
public 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")
@ResponseBody
public 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 id
var token = sessionStorage.getItem("token");
if (token){
return next();
}
return next("/login");
} ))
//设置axios的请求拦截器
axios.interceptors.request.use(config=>{
//从sessionStorage中获取token token存储的session id
var item=sessionStorage.getItem("token");
if (item){
config.headers.token=item;
}
return config;
})
Vue.prototype.$http=axios
Vue.config.productionTip = false
new 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)) { //如果存在该token
request.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配置类
@Bean
public 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数据
@Override
protected 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;
}
@Override
protected 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 id
var 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")
@ResponseBody
public 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")
@ResponseBody
public Result logout(){
Subject subject = SecurityUtils.getSubject();
//清空redis
subject.logout();
return new Result(200,"退出成功",null);
}
@GetMapping("/unlogin")
@ResponseBody
public Result unlogin(){
return new Result(401,"请先登录",null);
}
}
五、退出功能🍉
编辑退出接口🥝
@PostMapping("/logout")
@ResponseBody
public Result logout(){
Subject subject = SecurityUtils.getSubject();
//清空redis
subject.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>