SpringBoot集成Shiro实现认证和授权

一、概念篇

(一)关于Shiro

关于Shiro的了解,推荐一门课程:Shiro知识精讲,和一篇文章:https://zhuanlan.zhihu.com/p/54176956
在这里插入图片描述

Shiro由三大部分组成,分别是Subject, SecurityManagerRealms.

Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。

SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。其中,Authenticator完成认证,Authorizer则用于授权。

Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

(二)SpringBoot中使用Shiro实现自定义的授权与认证

在SpringBoot中使用Shiro主要包括四个部分,如下图所示。
在这里插入图片描述

  • ShiroConfig: 将OAuth2Filter和OAuth2Realm配置应用到Shiro框架
  • AuthenticatingFilter:拦截HTTP请求,对请求进行放行和其他处理
  • AnthenrizingRealm:实现具体的授权和认证的业务逻辑
  • AnthenticationToken:通过JwtUtil类可以生成Token,这个Token我们是要返回给客户端的。客户端提交的Token不能直接交给Shiro框架,需要先封装成AuthenticationToken类型的对象,所以我们我们需要先创建AuthenticationToken的实现类。

二、源码篇

以下源代码是在SpringBoot即成Shrio + JWT的实践。关于JWT的部分可以参考https://blog.csdn.net/loongkingwhat/article/details/119322423。

(一)依赖库

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-web</artifactId>
	<version>1.5.3</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.5.3</version>
</dependency>
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.10.3</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.11</version>
</dependency>
<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpcore</artifactId>
	<version>4.4.13</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(二)封装AuthenticationToken类型

String类型的token在Shiro中无法直接使用,必须封装成为Shiro框架可识别和处理的AuthenticationToken类型。

package com.example.demo.config.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class OAuth2Token implements AuthenticationToken {
    
    
    private String token;

    public OAuth2Token(String token){
    
    
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
    
    
        return token;
    }

    @Override
    public Object getCredentials() {
    
    
        return token;
    }
}


(三)创建自定义Filter类

package com.example.emos.wx.config.shiro;

import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
//因为在OAuth2Filter类中要读写ThreadLocal中的数据,所以OAuth2Filter类必须要设置成多例的,否则ThreadLocal将无法使用。
@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
    
    
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
	 * 拦截请求之后,用于把令牌字符串封装成令牌对象
	 */
	@Override
    protected AuthenticationToken createToken(ServletRequest request, 
		ServletResponse response) throws Exception {
    
    
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);

        if (StringUtils.isBlank(token)) {
    
    
            return null;
        }

        return new OAuth2Token(token);
    }

    /**
	 * 拦截请求,判断请求是否需要被Shiro处理
	 */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, 
		ServletResponse response, Object mappedValue) {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        // Ajax提交application/json数据的时候,会先发出Options请求
		// 这里要放行Options请求,不需要Shiro处理
		if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
    
    
            return true;
        }
		// 除了Options请求之外,所有请求都要被Shiro处理
        return false;
    }

    /**
	 * 该方法用于处理所有应该被Shiro处理的请求
	 */
    @Override
    protected boolean onAccessDenied(ServletRequest request, 
		ServletResponse response) throws Exception {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;

		resp.setHeader("Content-Type", "text/html;charset=UTF-8");
		//允许跨域请求
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

		threadLocalToken.clear();
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
    
    
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
		
        try {
    
    
            jwtUtil.verifierToken(token); //检查令牌是否过期
        } catch (TokenExpiredException e) {
    
    
            //客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
            if (redisTemplate.hasKey(token)) {
    
    
                redisTemplate.delete(token);//删除令牌
                int userId = jwtUtil.getUserId(token);
                token = jwtUtil.createToken(userId);  //生成新的令牌
                //把新的令牌保存到Redis中
                redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
                //把新令牌绑定到线程
                threadLocalToken.setToken(token);
            } else {
    
    
                //如果Redis不存在令牌,让用户重新登录
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已经过期");
                return false;
            }

        } catch (JWTDecodeException e) {
    
    
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }

        boolean bool = executeLogin(request, response);
        return bool;
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token,
		AuthenticationException e, ServletRequest request, ServletResponse response) {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        resp.setContentType("application/json;charset=utf-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        try {
    
    
            resp.getWriter().print(e.getMessage());
        } catch (IOException exception) {
    
    

        }
        return false;
    }

    /**
     * 获取请求头里面的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
    
    
        //从header中获取token
        String token = httpRequest.getHeader("token");

        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
    
    
            token = httpRequest.getParameter("token");
        }
        return token;

    }

    @Override
    public void doFilterInternal(ServletRequest request, 
		ServletResponse response, FilterChain chain) throws ServletException, IOException {
    
    
        super.doFilterInternal(request, response, chain);
    }
}

(四)实现自定义Realm类

实际进行权限信息验证的是我们的 Realm,Shiro 框架内部默认提供了两种实现,一种是查询.ini文件的IniRealm,另一种是查询数据库的JdbcRealm,这两种来说都相对简单。但是在具体使用Shiro的时候,一般都会使用更易定制的自定义的Realm。

自定义的Realm继承 Shirot 框架的 AuthorizingRealm类,并实现默认的两个方法,分别是用于授权的doGetAuthorizationInfo,和用于认证的doGetAuthenticationInfo

package com.example.demo.config.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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
public class OAuth2Realm extends AuthorizingRealm {
    
    

    @Autowired
    private JwtUtil jwtUtil;


    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //TODO 查询用户的权限列表
        //TODO 把权限列表添加到info对象中
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
        //TODO 从令牌中获取userId,然后检测该账户是否被冻结。
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
        //TODO 往info对象中添加用户信息、Token字符串
        return info;
    }
}

Controller中的方法如果不是Shiro直接放行请求的Web方法,客户端的对应的请求一定会被Shiro拦截下来,先由OAuth2Filter检查请求头的Token是否合法。如果没问题,接下来就要由OAuth2Realm中的doGetAuthenticationInfo()方法来颁发认证对象。请求被赋予了认证对象,那么请求才会被发送到Web方法来执行。
在这里插入图片描述

Shiro每次验证权限之前,都要执行授权方法,把用户具有的权限封装成权限对象AuthenticationInfo,然后放行请求。接下来Web方法的@RequiresPermissions注解,会从权限对象中提取权限数据,跟要求的权限作比较。如果用户具有该Web方法要求的权限,那么Web方法就会正常执行。反之则返回异常消息。

//创建Web方法的时候,如果希望只有满足相关权限的用户才能调用这个Web方法,我们只需要给Web方法添加上@RequiresPermissions注解即可。
@PostMapping("/addUser")
@ApiOperation("添加用户")
@RequiresPermissions(value = {
    
    "ROOT", "USER:ADD"}, logical = Logical.OR)
public R addUser() {
    
    
	return R.ok("用户添加成功");
}

(五)配置自定义Reaml和Filter到Shiro

把OAuth2Filter和OAuth2Realm配置到Shiro框架,这样自定义的Shiro才生效

package com.example.emos.wx.config.shiro;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class ShiroConfig {
    
    

    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(oAuth2Realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter oAuth2Filter) {
    
    
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //oauth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", oAuth2Filter);
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/user/register", "anon");
        filterMap.put("/user/login", "anon");
        filterMap.put("/test/**", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

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

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

}




猜你喜欢

转载自blog.csdn.net/loongkingwhat/article/details/119487014