springboot整合shiro+redis单点登录-登录踢人-未认证请求返回JSON数据


需要实现的功能如题,本文将分块记录每一个关键环节。
闲言少叙,直接上干货↓↓

核心依赖

这个shiro-all看上去有点非主流,但是在maven库中确实是存在的,不想麻烦的直接依赖这个就行了

	<dependency>
	    <groupId>org.apache.shiro</groupId>
	    <artifactId>shiro-all</artifactId>
	    <version>1.3.2</version>
	</dependency>
	
	<dependency>
	    <groupId>org.crazycake</groupId>
	    <artifactId>shiro-redis</artifactId>
	    <version>3.2.3</version>
	</dependency>

返回结果实体类

没啥说的

package com.example.demo.shiro;

public class RestResult<T> {
    int code;
    String msg;
    T data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

用户信息实体类

这个虽然看上去也没啥说的,但是我经历过一个异常堆栈,原因是在realm实现类里实现认证方法new SimpleAuthenticationInfo的时候,principal参数传的是UserInfo类的对象,而最开始的UserInfo类没有id字段,所以在执行subject.logout()方法的时候抛出异常,提示principal的对象没有id字段。然后我加上了id字段,但是登陆的时候没有向其中传值,所以又抛出id字段值为null的异常。
所以解决这个问题的方法是要么在实体类中加id字段并有值,要么principal参数不要传对象,直接传个username或者其他什么的标识的字符串


2021-07-20补充说明
如果实体类存在父类,则登录后,父类字段的信息不会被保存,现象就是在非登录线程中,使用subject.getPrincipal()方法获取出实体对象,属于父类的字段内容为null


package com.example.demo.shiro;


import java.io.Serializable;

public class UserInfo implements Serializable {
    private String id;

    private String userName;

    private String password;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

实现realm

realm是shiro的核心组件,实现的这两个方法名字看上去很像。
doGetAuthenticationInfo方法作用是认证,根据传入的账户和密码验证用户是否存在,整合了redis实现单点登录情况下,在执行过此方法后,会以当前session的ID加shiro固定的前缀为key在redis中保存此次登录的session,以此实现共享session,避免重复认证;
doGetAuthorizationInfo方法作用是赋予角色和资源(也可以理解为权限标识)。

package com.example.demo.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class SupRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet<>();
        roles.add("role1");
        roles.add("role2");
        simpleAuthorizationInfo.addRoles(roles);
        
        Set<String> perms = new HashSet<>();
        perms.add("perms1");
        perms.add("perms2");
        simpleAuthorizationInfo.addStringPermissions(perms);
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String userName = token.getUsername();
        String pwd = String.valueOf(token.getPassword());

        UserInfo userInfo = new UserInfo();
        userInfo.setPassword(pwd);
        userInfo.setUserName(userName);

        return new SimpleAuthenticationInfo(userInfo.getUserName(), userInfo.getPassword(), "SupRealm");
    }
}

未认证的请求返回JSON数据

对于未登录的请求,shiro默认的实现是重定向至一个url,如果在配置ShiroFilterFactoryBean没有指定url则默认重定向至/login.jsp,但是对于前后端分离的情况下,最好还是在判断为未登录的情况返回一个状态码或是一个JSON数据。
对于未登录的请求发生后,shiro也还是会在redis中缓存一个session,对于这种无效的session显然是我们不希望存在的,所以在返回状态码或JSON数据前我们将这个无效session删除掉。
ShiroAuthcFilter 的配置会在最后看到

package com.example.demo.shiro;

import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;

public class ShiroAuthcFilter extends FormAuthenticationFilter {
	@Autowired
    @Qualifier("sessionDAO")
    private RedisSessionDAO redisSessionDAO;
    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        // 删除无效session
        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession();
        redisSessionDAO.delete(session);
        
        RestResult restResult = new RestResult();
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        HttpServletRequest httpRequest = (HttpServletRequest)request;
        String device = httpRequest.getHeader("device");
        PrintWriter writer = response.getWriter();
        try {
            restResult.setCode(-1);
            restResult.setMsg("请登录");
            writer.print(JSONObject.toJSON(restResult).toString());
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }finally {
            writer.flush();
            writer.close();
        }
    }
}

实现登录和踢人

sessionManager和redisCacheManager之所以能被注入是因为在配置类中配置了这两个Bean(在后面能看到)。
登录方法前三行代码是shiro的登录操作,基本上是固定搭配。主要说一下踢人的逻辑,创建一个Deque集合来保存登录时shiro生成的sessionid,然后将这个集合以username为key保存到redis中,如果集合中保存的sessionid多于一个,则删除集合中其他的sessionid,并根据被删除的sessionid获取对应的session(即被踢掉的session),设置kickout属性值为true。
这样,被踢掉的session再发请求的时候,校验属性kickout的属性值为true,则不允许继续操作,并返回状态

package com.example.demo.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/shiroController")
public class ShiroController {

    @Autowired
    @Qualifier("sessionManager")
    private DefaultWebSessionManager sessionManager;

    @Autowired
    @Qualifier("redisCacheManager")
    private RedisCacheManager redisCacheManager;

    @GetMapping("/sys/login")
    public RestResult login(String username, String pwd){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, pwd);
        subject.login(token);

        Cache<String, Deque<Serializable>> cache = this.redisCacheManager.getCache("kickout-session");
        Deque<Serializable> deque = cache.get(username);
        if(deque == null){
            deque = new ArrayDeque<>();
        }

        Session session = subject.getSession();
        if(!deque.contains(session.getId())){
            deque.add(session.getId());
            cache.put(username, deque);
        }

        while(1 < deque.size()){
            Serializable serializable = deque.removeFirst();
            if(!serializable.equals(session.getId())){
                Session kickoutSession = this.sessionManager.getSession(new DefaultSessionKey(serializable));
                kickoutSession.setAttribute("kickout", true);
                kickoutSession.setAttribute("kickoutMsg", "让人踢了");
                cache.put(username, deque);
            }
        }

        RestResult restResult = new RestResult();
        restResult.setMsg("登录成功");
        restResult.setCode(0);
        return restResult;
    }

    @GetMapping("/static/testM1")
    public Map<String, Object> testM1(){
        Map<String,Object> map = new HashMap<>();
        map.put("field1","value1");
        return map;
    }

    @GetMapping("/bus/testM2")
    public String testM2(){

        return "testM2";
    }

    @GetMapping("/bus/testM3")
    public String testM3(){
        return "testM3";
    }

    @GetMapping("/bus/testM4")
    @RequiresPermissions("test41")
    public String testM4(){
        return "testM4";
    }
}

实现被踢

实现一个拦截器,需继承AccessControlFilter 类,并实现isAccessAllowed和onAccessDenied方法。
这里isAccessAllowed方法返回值不能设置为true,如果设置为true则onAccessDenied方法不能被执行。
我们在onAccessDenied方法中获取当前请求的session,并校验kickout属性是否为true,如果是true则登出允许继续请求,并返回状态。
在网上查阅资料的时候,发现有不少人喜欢将已登录的有效session保存起来并标记被踢session的方法和识别session被踢并且登出的方法写在一起,如果这样写的话用户在登录的时候就不能第一时间保存有效sessionid,而是在登录后的第一个请求中保存,感觉这样怪怪的,而且每次请求都要走一遍是否需要保存sessionid的逻辑,更奇怪。
KickoutSessionFilter 的配置会在最后看到

package com.example.demo.shiro;

import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;

public class KickoutSessionFilter extends AccessControlFilter {

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;
    private RedisCacheManager redisCacheManager;

    public void setSessionManager(SessionManager sessionManager){
        this.sessionManager = sessionManager;
    }

    public void setCache(RedisCacheManager redisCacheManager){
        this.cache = redisCacheManager.getCache("kickout-session");
        this.redisCacheManager = redisCacheManager;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request,response);
        if (subject == null || !subject.isAuthenticated()) {
            return true;
        }
        Session session = subject.getSession();

        if(session.getAttribute("kickout") != null && (boolean)session.getAttribute("kickout")){
            subject.logout();
            saveRequest(request);
            RestResult restResult = new RestResult();
            restResult.setCode(1);
            restResult.setMsg("被踢了");
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html");
            ((DefaultWebSessionManager)this.sessionManager).getSessionDAO().delete(session);
            PrintWriter writer = response.getWriter();
            try{
                writer.print(JSONObject.toJSON(restResult).toString());
                return false;
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                writer.close();
            }
        }
        return true;
    }
}

shiro的配置类

配置是重中之重,没有这部分,上边的活全都白干。
挨个说一下吧。
getRealm()、securityManager()和shiroFilterFactoryBean()的配置是shiro配置的核心,realm负责认证和授权、securityManager是shiro的安全管理器,创建subject就是由它完成的、shiroFilterFactoryBean用来定义拦截规则。

lifecycleBeanPostProcessor()和defaultAdvisorAutoProxyCreator()配置用来开启shiro的注解功能。

redisManager()、sessionDAO()、cookie()、sessionManager()、redisCacheManager()和sessionIdGenerator()配置是实现单点登录的关键配置。
redisManager()用来配置redis的配置信息。
redisCacheManager()用来操作redis缓存。
sessionDAO()用来执行session的增删改查。
sessionManager()用来管理session的生命周期。
cookie()用来配置sessionid以何key保存在前端。
sessionIdGenerator()sessionid生成器。

kickoutSessionFilter()和shiroAuthcFilter()都是自定义的过滤器。
kickoutSessionFilter()配置的功能是session被踢时的逻辑。
shiroAuthcFilter()配置的是对未登录请求的处理逻辑。
有一个容易被忽视的点需要注意一下,就是自定义的过滤器的配置要在shiroFilterFactoryBean的配置后面,否则会报如下的异常。
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.


2021-07-21更新
关于defaultAdvisorAutoProxyCreator配置,会将包含数据库事务的springBean的aop代理方式改为jdk动态代理,若仍想用cglib的方式代理,则需要设置defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

关于kickoutSessionFilter和shiroAuthcFilter以及一系列的自定义filter配置,如果将这些过滤器声明为springbean,则这些过滤器将过滤所有请求,所以在这里要将@Bean去掉

package com.example.demo.shiro;

import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;


@Configuration
public class ShiroConfigure {

    @Bean
    public SupRealm getRealm(){
        SupRealm realm = new SupRealm();
        return realm;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(getRealm());
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

    @Bean
    public RedisManager redisManager(){
        RedisManager manager = new RedisManager();
        manager.setHost("10.32.144.118:6379");
        manager.setTimeout(360000);
        manager.setDatabase(2);
        return manager;
    }

    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator(){
        return new JavaUuidSessionIdGenerator();
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("/shiroController/static/**","anon");
        map.put("/shiroController/sys/login","anon");
        map.put("/shiroController/bus/**","authc,kickout");
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        filter.setSecurityManager(securityManager);
        filter.setFilterChainDefinitionMap(map);
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("kickout", kickoutSessionFilter());
        filterMap.put("authc",shiroAuthcFilter());
        filter.setFilters(filterMap);
        return filter;
    }

    @Bean
    public RedisSessionDAO sessionDAO(){
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        sessionDAO.setRedisManager(redisManager());
        sessionDAO.setSessionIdGenerator(sessionIdGenerator());
        return sessionDAO;
    }

    @Bean
    public SimpleCookie cookie(){
        SimpleCookie cookie = new SimpleCookie("sid");
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        return cookie;
    }

    @Bean
    public DefaultWebSessionManager sessionManager(){
        DefaultWebSessionManager manager = new DefaultWebSessionManager();
        //manager.setGlobalSessionTimeout(-1000L);
        manager.setDeleteInvalidSessions(true);
        manager.setSessionIdCookie(cookie());
        manager.setSessionDAO(sessionDAO());
        return manager;
    }

    @Bean
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager manager = new RedisCacheManager();
        manager.setRedisManager(redisManager());
        return manager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        return new DefaultAdvisorAutoProxyCreator();
    }

    /*@Bean*/
    public KickoutSessionFilter kickoutSessionFilter(){
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        kickoutSessionFilter.setCache(redisCacheManager());
        kickoutSessionFilter.setSessionManager(sessionManager());
        return kickoutSessionFilter;
    }

    /*@Bean*/
    public ShiroAuthcFilter shiroAuthcFilter(){
        ShiroAuthcFilter shiroAuthcFilter = new ShiroAuthcFilter();
        return shiroAuthcFilter;
    }
}

结尾

学而时习之不亦说乎。
之前几乎没有接触过shiro框架,之所以会研究它基本上是出于兴趣。从着手编写第一行代码到完成本文所展示的功能,共耗时三天,因为之前对这个框架了解不是很多,所以在学习的过程中遇到不少问题,通过翻阅各种资料(一顿百度),看了少部分源码,最终一一攻克。
很享受这种专注于研究某一件事的感觉,尤其是在期间虽然遇到各种问题,但最后修成正果的成就感。
社会竞争激烈,唯有不断的给自己积累筹码才能持续生存,保持学习,加油!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
大学生参加学科竞赛有着诸多好处,不仅有助于个人综合素质的提升,还能为来职业发展奠定良好基础。以下是一些分析: 首先,学科竞赛是提高专业知识和技能水平的有效途径。通过参与竞赛,学生不仅能够深入学习相关专业知识,还能够接触到最新的科研成果和技术发展趋势。这有助于拓展学生的学科视野,使其对专业领域有更深刻的理解。在竞赛过程中,学生通常需要解决实际问题,这锻炼了他们独立思考和解决问题的能力。 其次,学科竞赛培养了学生的团队合作精神。许多竞赛项目需要团队协作来完成,这促使学生学会有效地与他人合作、协调分工。在团队合作中,学生们能够学到如何有效沟通、共同制定目标和分工合作,这对于日后进入职场具有重要意义。 此外,学科竞赛是提高学生综合能力的一种途径。竞赛项目通常会涉及到理论知识、实际操作和创新思维等多个方面,要求参赛者具备全面的素质。在竞赛过程中,学生不仅需要展现自己的专业知识,还需要具备创新意识和解决问题的能力。这种全面的综合能力培养对于来从事各类职业都具有积极作用。 此外,学科竞赛可以为学生提供展示自我、树立信心的机会。通过比赛的舞台,学生有机会展现自己在专业领域的优势,得到他人的认可和赞誉。这对于培养学生的自信心和自我价值感非常重要,有助于他们更加积极主动地投入学习和来的职业生涯。 最后,学科竞赛对于个人职业发展具有积极的助推作用。在竞赛中脱颖而出的学生通常能够引起企业、研究机构等用人单位的关注。获得竞赛奖项不仅可以作为个人履历的亮点,还可以为进入理想的工作岗位提供有力的支持。
Spring Boot是一个用于快速开发Java应用程序的开源框架,Shiro是一个强大且易于使用的Java安全框架,Redis是一个开源的内存数据库。结合使用这些技术可以实现单点登录功能。 在Spring Boot中使用Shiro来处理认证和授权,可以通过配置Shiro的Realm来实现用户的登录认证和权限控制。将用户的信息存储在Redis中,利用Redis的持久化特性来实现用户登录状态的共享和存储。 首先,在Spring Boot项目的配置文件中配置Redis的连接信息,以便连接到Redis数据库。 然后,创建一个自定义的Shiro的Realm,在其中重写认证和授权的方法。在认证方法中,将用户的登录信息存储到Redis中,以便其他服务可以进行验证。在授权方法中,根据用户的角色和权限进行相应的授权操作。 接着,在Spring Boot项目的配置类中配置Shiro的相关设置,包括Realm、Session管理器、Cookie管理器等。 最后,可以在Controller层中使用Shiro的注解来标记需要进行认证和授权的接口,以确保只有登录后且具备相应权限的用户才能访问这些接口。 总的来说,通过使用Spring Boot、ShiroRedis的组合,可以实现单点登录的功能。用户在登录后,将登录信息存储到Redis中,其他服务可以通过验证Redis中的数据来判断用户的登录状态。同时,Shiro提供了强大的认证和授权功能,可以确保只有具备相应权限的用户才能访问受保护的接口。这些功能的具体实现可以通过深入研究Spring Boot、ShiroRedis的源码来了解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值