美文网首页SpringBoot
【SpringBoot】Shiro实现无状态登录

【SpringBoot】Shiro实现无状态登录

作者: 扮鬼之梦 | 来源:发表于2020-04-25 20:38 被阅读0次

    Shiro无状态登录介绍

    使用Shiro实现有状态登录,即用户登录状态存储在服务器Session中,使用Shiro实现比较简单,我这里暂不进行讨论。
    在一些环境中,可能需要把 Web 应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像Session这种东西,而是每次请求时带上token之类的进行用户判断。
    使用Shiro实现无状态登录的主要步骤有,禁用缓存、设置不创建session、关闭Session验证、关闭Session存储、注入自定义拦截器、开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)等。

    1.创建SpringBoot项目,并添加以下依赖

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.7.0</version>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.68</version>
    </dependency>
    

    2.添加登录接口

    获取token,这里token写死为admin

    @RestController
    @RequestMapping("/auth")
    public class LoginController {
    
        @GetMapping("/token")
        public Object token(String username, String password) {
            //登录并返回token
            String token = "admin";
            return ResponseEntity.ok(token);
        }
    
    }
    

    3.添加业务接口

    添加增删改查接口,并设置需要的访问权限

    @RestController
    @RequestMapping("/service")
    public class ServiceController {
    
        @RequestMapping("/create")
        @RequiresPermissions("service:create")
        public ResponseEntity add() {
            return ResponseEntity.ok("增加成功");
        }
    
        @RequestMapping("/delete")
        @RequiresPermissions("service:delete")
        public ResponseEntity delete() {
            return ResponseEntity.ok("删除成功");
        }
    
        @RequestMapping("/update")
        @RequiresPermissions("service:update")
        public ResponseEntity update() {
            return ResponseEntity.ok("修改成功");
        }
    
        @RequestMapping("/read")
        @RequiresPermissions("service:read")
        public ResponseEntity read() {
            return ResponseEntity.ok("查询成功");
        }
    
    }
    

    4.创建SubjectFactory

    关键是设置不创建Session

    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.subject.SubjectContext;
    import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
    
    public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
        public Subject createSubject(SubjectContext context) {
            //不创建session
            context.setSessionCreationEnabled(false);
            return super.createSubject(context);
        }
    }
    

    5.创建拦截器

    主要功能就是拦截http请求,进行认证授权,注意不要拦截登录请求

    import com.alibaba.fastjson.JSON;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.web.filter.AccessControlFilter;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.HashSet;
    import java.util.Set;
    
    public class StatelessAuthcFilter extends AccessControlFilter {
    
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            HttpServletRequest req = (HttpServletRequest) request;
            String uri = req.getRequestURI();
            Set<String> passUrl = new HashSet<>();
            //配置不需要认证即可访问的地址,如登录接口
            passUrl.add("/auth/token");
    
            if (passUrl.contains(uri)) {
                //允许直接访问
                return true;
            }
    
            //不允许访问,执行onAccessDenied的拦截内容
            return false;
        }
    
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest req = (HttpServletRequest) request;
            //获取请求头中的token值
            String token = req.getHeader("token");
    
            if (token == null) {
                returnLoginError(response);
                return false;
            }
    
            //将token传入AuthenticationToken
            AuthenticationToken authenticationToken = new AuthenticationToken() {
                @Override
                public Object getPrincipal() {
                    return token;
                }
    
                @Override
                public Object getCredentials() {
                    return token;
                }
            };
    
            try {
                //委托给Realm进行认证
                getSubject(request, response).login(authenticationToken);
            } catch (Exception e) {
                //登录失败
                returnLoginError(response);
                return false;
            }
            return true;
        }
    
        //登录失败时默认返回401状态码
        private void returnLoginError(ServletResponse response) throws IOException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            ResponseEntity err = ResponseEntity.err(ResultCode.UN_AUTHORIZED.getValue(), ResultCode.UN_AUTHORIZED.getDescription());
            writer.write(JSON.toJSONString(err));
        }
    }
    

    6.创建StatelessRealm

    在StatelessAuthcFilter拦截器中,会调用StatelessRealm进行token认证及用户授权。
    我这里设置token为admin时拥有增删改查的权限。
    token为user时,只拥有查的权限。
    其他token,无任何权限。

    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;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class StatelessRealm extends AuthorizingRealm {
    
        /**
         * 设置支持的AuthenticationToken
         */
        public boolean supports(AuthenticationToken token) {
            return true;
        }
    
        /**
         * 授权
         */
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
            Long userId = (Long) principals.getPrimaryPrincipal();
    
            //根据userId查询用户用户权限,并授权
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            List<String> permissions = new ArrayList();
            if (userId == 0) {
                //拥有所有权限
                permissions.add("service:create");
                permissions.add("service:delete");
                permissions.add("service:update");
                permissions.add("service:read");
            } else if (userId == 1) {
                //只有读的权限
                permissions.add("service:read");
            } else {
                //没有权限
            }
            authorizationInfo.addStringPermissions(permissions);
            return authorizationInfo;
        }
    
        /**
         * 认证
         */
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String t = (String) token.getPrincipal();
    
            //根据token获取userId
            Long userId = null;
            if ("admin".equals(t)) {
                userId = 0L;
            } else if ("user".equals(t)) {
                userId = 1L;
            } else {
                //未获取到userId,抛出异常,
                throw new UnknownAccountException();
            }
            return new SimpleAuthenticationInfo(userId, token.getCredentials(), getName());
        }
    }
    

    7.最后将以上创建的类注入到配置文件ShiroConfig即可

    主要是禁用缓存、不创建Session、关闭Session验证、关闭Session存储、注入配置拦截器、开启权限校验注解等。

    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.mgt.*;
    import org.apache.shiro.session.mgt.DefaultSessionManager;
    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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    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 javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    @Configuration
    public class ShiroConfig {
    
        /**
         * 注入自定义Realm,禁用缓存
         */
        @Bean
        public StatelessRealm statelessRealm() {
            StatelessRealm statelessRealm = new StatelessRealm();
            statelessRealm.setCachingEnabled(false);
            return statelessRealm;
        }
    
        /**
         * 注入SubjectFactory,不创建session
         */
        @Bean
        public SubjectFactory subjectFactory() {
            return new StatelessDefaultSubjectFactory();
        }
    
        /**
         * 注入SessionManager,关闭Session验证
         */
        @Bean
        public SessionManager sessionManager() {
            DefaultSessionManager defaultSessionManager = new DefaultSessionManager();
            defaultSessionManager.setSessionValidationSchedulerEnabled(false);
            return defaultSessionManager;
        }
    
        /**
         * 注入SessionStorageEvaluator,关闭Session存储
         */
        @Bean
        public SessionStorageEvaluator sessionStorageEvaluator() {
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            return defaultSessionStorageEvaluator;
        }
    
        /**
         * 注入SecurityManager
         */
        @Bean
        public SecurityManager securityManager(
                StatelessRealm statelessRealm, SessionStorageEvaluator sessionStorageEvaluator,
                SubjectFactory subjectFactory, SessionManager sessionManager) {
            DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    
            defaultSecurityManager.setRealm(statelessRealm);
    
            DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
            defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
            defaultSecurityManager.setSubjectDAO(defaultSubjectDAO);
    
            defaultSecurityManager.setSubjectFactory(subjectFactory);
    
            defaultSecurityManager.setSessionManager(sessionManager);
    
            return defaultSecurityManager;
        }
    
        /**
         * 拦截器配置
         */
        @Bean(name = "shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
    
            //添加自定义的拦截器
            Map<String, Filter> filters = new HashMap<>();
            filters.put("statelessAuthc", new StatelessAuthcFilter());
            shiroFilterFactoryBean.setFilters(filters);
    
            //设置自定义的拦截器,拦截所有请求,方法一
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
            // 登录接口不进行token校验
        filterChainDefinitionMap.put("/user/login", "anon");
            // 其他接口进行token校验
            filterChainDefinitionMap.put("/**", "statelessAuthc");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            //方法二:shiroFilterFactoryBean.setFilterChainDefinitions("/**=statelessAuthc");
    
            return shiroFilterFactoryBean;
        }
    
        /**
         * 拦截器注册
         */
        @Bean
        public FilterRegistrationBean delegatingFilterProxy() {
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            DelegatingFilterProxy proxy = new DelegatingFilterProxy();
            proxy.setTargetFilterLifecycle(true);
            proxy.setTargetBeanName("shiroFilter");
            filterRegistrationBean.setFilter(proxy);
            return filterRegistrationBean;
        }
    
        /**
         * *
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
         * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * *
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)
         * 和AuthorizationAttributeSourceAdvisor)即可实现此功能
         * *
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
    
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
    }
    

    8.测试

    8.1访问登录接口,获取token

    image

    8.2使用token=admin,访问任意接口

    image

    8.3使用token=user,访问create接口

    image

    8.4使用token=user,访问read接口

    image

    8.5使用token=didi(即无任何权限的token),访问read接口

    image

    认证缓存和权限缓存

    引入依赖

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.7.0</version>
    </dependency>
    

    shiro配置中开启缓存

        /**
         * 注入自定义Realm
         */
        @Bean
        public ShiroRealm ShiroRealm() {
            ShiroRealm ShiroRealm = new ShiroRealm();
            // 开启全局缓存
            ShiroRealm.setCachingEnabled(true);
            // 开启授权缓存
            ShiroRealm.setAuthorizationCachingEnabled(true);
        //设置授权缓存名字方便查找
            ShiroRealm.setAuthorizationCacheName("authorizationCache");
            // 关闭认证缓存
            ShiroRealm.setAuthenticationCachingEnabled(false);
        // ShiroRealm.setAuthenticationCacheName("authenticationCache");
    
            ShiroRealm.setCacheManager(new EhCacheManager());
            return ShiroRealm;
        }
    

    获取认证或授权信息

    @Autowired
    private ShiroRealm shiroRealm;
    
    public void test(){
        // 获取授权信息
        Cache<Object, AuthorizationInfo> cache = shiroRealm.getAuthorizationCache();
        AuthorizationInfo authorizationInfo = cache.get(SecurityUtils.getSubject().getPrincipals());
    }
    

    相关文章

      网友评论

        本文标题:【SpringBoot】Shiro实现无状态登录

        本文链接:https://www.haomeiwen.com/subject/mlhlwhtx.html