0
点赞
收藏
分享

微信扫一扫

【SpringBoot】Shiro实现无状态登录

悲催博士僧 2021-10-09 阅读 70

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

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

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

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

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

认证缓存和权限缓存

引入依赖

<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());
}
举报

相关推荐

0 条评论