基于shiro搭建分布式会话管理附shiro会话源码流程

在这里插入图片描述

搭建:三个项目相互之间都是独立的,zuul-demo1、zuul-demo2作为两个服务,zuul-gateway根据路径转发请求给这两个服务,须携带请求头Authorization。
项目地址:https://gitee.com/zzhua195/zuul-shiro

一、zuul-demo1项目

1. 导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zzhua</groupId>
    <artifactId>zuul-demo1</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.8.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--shiro与spring整合 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--shiro与redis整合实现sessionDao -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>

    </dependencies>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

2. ShiroConfiguration

package com.zzhua.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;
import org.crazycake.shiro.IRedisManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;

@Configuration
public class ShiroConfiguration {
    
    

    @Value("${spring.redis.host:127.0.0.1}")
    private String host;

    @Value("${spring.redis.port:6379}")
    private int port;

    @Value("${spring.redis.timeout:6379}")
    private int timeout;

    @Bean
    public Realm realm() {
    
    
        return new MyRealm();
    }

    @Bean
    public IRedisManager redisManager() {
    
    
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        return redisManager;
    }

    @Bean
    @Primary
    public RedisSessionDAO redisSessionDAO(IRedisManager redisManager) {
    
    
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager);
        // redisSessionDAO.setValueSerializer();
        return redisSessionDAO;
    }

    // 自定义会话管理器
    @Bean
    public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO,
                                                   @Autowired(required = false) SessionListener sessionListener) {
    
    
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager() {
    
    

            private static final String HEADER = "Authorization";

            /**
             * 此方法会被调用2次,
             *        第一次是: 请求刚进来,正在resolveContextSession(subjectContext)
             *        第二次是: 使用session存东西setAttribute(key,val),
             *                                DelegatingSession会使用保存的sessionManager
             *                                                 并且会把key给传过来
             *
             * @param key
             * @return
             */
            @Override
            public Serializable getSessionId(SessionKey key) {
    
    
                if (WebUtils.isWeb(key)) {
    
    
                    WebSessionKey webSessionKey = (WebSessionKey) key;
                    HttpServletRequest req = (HttpServletRequest) webSessionKey.getServletRequest();
                    String header = req.getHeader(HEADER);
                    return StringUtils.isEmpty(header)?key.getSessionId():header;
                }
                // return super.getSessionId(key)
                // 返回key的sessionId(不支持父类其它获取sessionId的方式,只允许请求头方式),
                // 这里如果返回了不为null的结果,那么必须要保证根据这个结果能找到Session,否则Shiro默认会抛出异常,见(DefaultSessionManager#retrieveSession)
                return key.getSessionId();
            }
        };
        // 即不会给客户端写回sessionId
        sessionManager.setSessionIdCookieEnabled(false);
        // 即不会重写url路径(如果浏览器不支持cookie,JSESSIONID拼接在url上)
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        // 设置sessionDao
        sessionManager.setSessionDAO(redisSessionDAO);
        // 每隔半分钟检测一次session
        sessionManager.setSessionValidationInterval(30000);
        // 设置自定义的对应的会话监听器
        sessionManager.setSessionListeners(Arrays.asList(sessionListener));
        return sessionManager;
    }

    @Bean
    public SecurityManager securityManager(Realm realm, SessionManager sessionManager) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        // 注册自定义filter
        HashMap<String, Filter> filters = new HashMap<>();
        // 在springboot中,不能把这个filter交给spring管理,
        // 否则它会直接被嵌入到web,有可能先于shiroFilter执行,而导致不能拿到Subject
        MyFilter myFilter = new MyFilter();
        filters.put("myFilter", myFilter);
        shiroFilter.setFilters(filters);
        LinkedHashMap<String, String> chainDefMap = new LinkedHashMap<>();
        chainDefMap.put("/free", "anon"); // 匿名filter
        chainDefMap.put("/login", "anon"); // 匿名filter
        chainDefMap.put("/**", "myFilter"); // 自定义filter
        // 设置拦截器链
        shiroFilter.setFilterChainDefinitionMap(chainDefMap);
        // 设置安全管理器
        shiroFilter.setSecurityManager(securityManager);
        return shiroFilter;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        return advisor;
    }



}

3. MyFilter

public class MyFilter extends AccessControlFilter {
    
    

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    
    
        Subject subject = getSubject(request, response);
        return subject.isAuthenticated();
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    
    
        ObjectMapper mapper = new ObjectMapper();
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(mapper.writeValueAsString("未登录,不能访问"));
        return false;
    }
}

4. MyRealm

public class MyRealm extends AuthorizingRealm {
    
    

    /**
     * 获取授权信息
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    
        SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
        User user = (User) principals.getPrimaryPrincipal();
        if ("zhangsan".equals(user.getName())) {
    
    
            authInfo.addStringPermission("get:a");
        }
        if ("lisi".equals(user.getName())) {
    
    
            authInfo.addStringPermission("get:b");
        }
        if ("wangwu".equals(user.getName())) {
    
    
            authInfo.addStringPermission("get:a");
            authInfo.addStringPermission("get:b");
            authInfo.addRoles(Collections.singletonList("role:admin"));
        }
        return authInfo;
    }

    /**
     * 获取认证信息
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        System.out.println("登录名:" + upToken.getUsername());
        System.out.println("密码:" + Arrays.toString(upToken.getPassword()));

        if ("zhangsan".equals(upToken.getUsername())) {
    
    
            return new SimpleAuthenticationInfo(new User("zhangsan", 23), "123456", this.getName());
        } else if ("lisi".equals(upToken.getUsername())) {
    
    
            return new SimpleAuthenticationInfo(new User("lisi", 23), "123456", this.getName());
        } else if ("wangwu".equals(upToken.getUsername())) {
    
    
            return new SimpleAuthenticationInfo(new User("wangwu", 23), "123456", this.getName());
        }

        return null;
    }
}

@Primary
@Component
public class MySessionListener implements SessionListener {
    
    

    private static CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    public static CopyOnWriteArrayList<Session> getSessions() {
    
    
        return sessions;
    }

    // 拥有身份,创建session
    @Override
    public void onStart(Session session) {
    
    
        sessions.add(session);
    }

    // ================检测session周期:默认一个小时=====================

    // 检测session valid
    @Override
    public void onStop(Session session) {
    
    
        sessions.removeIf(s -> {
    
    
            boolean flag = Objects.equals(session.getId(), s.getId());
            Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principals != null) {
    
    
                Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
                User user = (User) primaryPrincipal;
                System.out.println(user.getName() + " 账号已被停用");
            }
            return flag;
        });
    }

    // 检测session过期
    @Override
    public void onExpiration(Session session) {
    
    
        sessions.removeIf(s -> {
    
    
            boolean flag = Objects.equals(session.getId(), s.getId());
            Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principals != null) {
    
    
                Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
                User user = (User) primaryPrincipal;
                System.out.println(user.getName() + " 账号已过期");
            }
            return flag;
        });
    }
}

5. MySessionListener

@Primary
@Component
public class MySessionListener implements SessionListener {
    
    

    private static CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    public static CopyOnWriteArrayList<Session> getSessions() {
    
    
        return sessions;
    }

    // 拥有身份,创建session
    @Override
    public void onStart(Session session) {
    
    
        sessions.add(session);
    }

    // ================检测session周期:默认一个小时=====================

    // 检测session valid
    @Override
    public void onStop(Session session) {
    
    
        sessions.removeIf(s -> {
    
    
            boolean flag = Objects.equals(session.getId(), s.getId());
            Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principals != null) {
    
    
                Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
                User user = (User) primaryPrincipal;
                System.out.println(user.getName() + " 账号已被停用");
            }
            return flag;
        });
    }

    // 检测session过期
    @Override
    public void onExpiration(Session session) {
    
    
        sessions.removeIf(s -> {
    
    
            boolean flag = Objects.equals(session.getId(), s.getId());
            Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principals != null) {
    
    
                Object primaryPrincipal = ((PrincipalCollection) principals).getPrimaryPrincipal();
                User user = (User) primaryPrincipal;
                System.out.println(user.getName() + " 账号已过期");
            }
            return flag;
        });
    }
}

6. SessionController

package com.zzhua.config;

import com.zzhua.pojo.User;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@RestController
public class SessionController {
    
    

    @Autowired
    SessionDAO sessionDAO;

    @RequiresRoles("role:admin")
    @RequestMapping("stopSession/{sessionId}")
    public String stopSession(@PathVariable("sessionId") String sessionId) {
    
    
        Session session = sessionDAO.readSession(sessionId);
        if (session == null) {
    
    
            return "未找到该会话";
        }
        Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
        String uname = "";
        if (principals != null) {
    
    
            Object user = ((PrincipalCollection) principals).getPrimaryPrincipal();
            uname = ((User) user).getName();
        }
        sessionDAO.delete(session);
        return "停用会话id:"+session.getId()+",用户为: " + uname;
    }

    @RequiresRoles("role:admin")
    @RequestMapping("getAllSessions")
    public List<String> getAllSessions() {
    
    
        Collection<Session> sessions = sessionDAO.getActiveSessions();
        List<String> nameList = new ArrayList<>();
        sessions.stream().forEach(session -> {
    
    
            Object principals = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (principals != null) {
    
    
                Object user = ((PrincipalCollection) principals).getPrimaryPrincipal();
                String uname = ((User) user).getName();
                nameList.add(uname);
            }
        });
        return nameList;
    }

}

7. TestController

@Validated
@RestController
public class Test1Controller {
    
    

    @RequestMapping("test")
    public Object test1(@RequestParam(value = "name",required = false) String name,
                        @RequestHeader(value = "token1",required = false) String token1,
                        @CookieValue(name = "ck",required = false) String ck,
                        @RequestBody(required = false) Map<String, Object> map) {
    
    
        System.out.println("请求参数: " + name);
        System.out.println("请求头: " + token1);
        System.out.println("请求体: " + map);
        System.out.println("cookie: " + ck);
        return "test1";
    }

    @RequestMapping("free")
    public Object free() {
    
    
        return "free";
    }


    @RequiresPermissions("get:a")
    @RequestMapping("a")
    public Object a() {
    
    
        return "a";
    }

    @RequiresPermissions("get:b")
    @RequestMapping("b")
    public Object b() {
    
    
        return "b";
    }

}

8.User

import java.io.Serializable;

public class User implements Serializable {
    
    

    private String name;
    private Integer age;

    public User(String name, Integer age) {
    
    
        this.name = name;
        this.age = age;
    }

    public User() {
    
    
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public Integer getAge() {
    
    
        return age;
    }

    public void setAge(Integer age) {
    
    
        this.age = age;
    }
}

9. MyControllerAdvice

@RestControllerAdvice
public class MyControllerAdvice {
    
    

    @ExceptionHandler(Exception.class)
    public Object object(Exception ex) {
    
    
        return ex.getMessage();
    }

}

二、zuul-demo2

将zuul-demo1复制一遍改个端口号,即可(后面抽取下shiro到公共模块)

三、zuul-gateway

将zuul-demo1的shiro配置拷一份过来

1.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zzhua</groupId>
    <artifactId>zuul-gateway</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

        <!--shiro与spring整合 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--shiro与redis整合实现sessionDao -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

2. application.yml

server:
  port: 8080
zuul:
  routes:
    zuul-demo1:
      path: /zuul-demo1/**
      url: http://127.0.0.1:9091
    zuul-demo2:
      path: /zuul-demo2/**
      url: http://127.0.0.1:9092
  sensitive-headers: #这里须为空,会将请求头都携带过去

3.启动类

@EnableZuulProxy
@SpringBootApplication
public class ZuulGatewayApp {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(ZuulGatewayApp.class, args);
    }

}

4.LoginController

package com.zzhua.controller;

import com.zzhua.pojo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.Map;

@Validated
@RestController
public class LoginController {
    
    

    @RequestMapping("login")
    public Object login(@NotBlank(message = "uname can't be blank") String uname,
                        @NotBlank(message = "pword can't be blank")String pword) {
    
    
        Subject subject = SecurityUtils.getSubject();
        subject.login(new UsernamePasswordToken(uname, pword));
        // 登录成功之后,返回sessionId给前端
        Serializable sessionId = subject.getSession().getId();
        return sessionId;
    }

    @RequestMapping("getCurrUser")
    public User getCurrUser() {
    
    
        Object principal = SecurityUtils.getSubject().getPrincipal();
        System.out.println(principal);
        return ((User) principal);
    }

}

猜你喜欢

转载自blog.csdn.net/qq_16992475/article/details/118614802