最近一直在研究关于如何将Shiro权限框架和SpringBoot框架的集成,然后自己根据一些大佬博客文章和自己项目中使用的Shiro框架来写一篇自己理解的文章,可能在编写的过程中有不正确的地方,欢迎大家给予指正,谢谢大家。
Shiro介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
— 百度百科
在具体了解框架搭建注意事项之前我们要先了解 Realm 是什么?
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现
Realm能做的工作主要有以下几个方面:
身份验证(getAuthenticationInfo 方法)验证账户和密码,并返回相关信息
权限获取(getAuthorizationInfo 方法) 获取指定身份的权限,并返回相关信息
令牌支持(supports方法)判断该令牌(Token)是否被支持
令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌, 这边我们使用token
那么我们开始集成Shiro框架
我们在写变使用maven来管理项目 添加有关Shiro的依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
第一步:我们需要自定义关于OAuth2Realm的 Realm ;
AuthorizingRealm 提供了安全的访问应用的相关实体类,比如用户、角色、权限,对其中的访问应用相应的认证或者授权操作
所以我们在自定义 Realm 要继承 AuthorizingRealm 这个抽象类并重写getAuthenticationInfo和getAuthorizationInfo方法,来完成登录和权限的验证。
getAuthenticationInfo:
该方法主要是用于当前登录用户授权。
getAuthorizationInfo:
该方法是进行用户验证的。调用currUser.login(token)方法时会调用doGetAuthenticationInfo方法。
代码演示
package com.hwrest.jeadmin.modules.oauth2;
import com.hwrest.jeadmin.modules.sys.entity.SysUserEntity;
import com.hwrest.jeadmin.modules.sys.entity.SysUserTokenEntity;
import com.hwrest.jeadmin.modules.sys.service.ShiroService;
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;
/**
* @author queiter.ex
* @title: OAuth2Realm
* @projectName je_admin
* @description: 自定义 Realm 认证
* @date 2019/12/2514:35
*/
@Component // 注册到bean域中
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService; // 这个是我们通过数据库查询一些用户或用户权限资源接口方法
/**
* 授权(验证权限的调用)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 得到用户的身份
SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
// 获取到用户的Id
Long userId = sysUserEntity.getUserId();
// 然后获取用户的权限列表
Set<String> userPermissions = shiroService.getUserPermissions(userId);
// 开始对获取的权限列表进行授权 http://shiro.apache.org/static/1.3.2/apidocs/org/apache/shiro/authz/SimpleAuthorizationInfo.html
// AuthorizationInfo接口的简单POJO实现,当身份验证成功 其目的就是聚合授权信息,将角色和权限存储为内部属性。
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(userPermissions);
return simpleAuthorizationInfo;
}
/**
* 认证(登录时调用)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 返回在身份验证过程中提交的帐户标识。 这边我们使用的标识就是token
String accessToken = (String) authenticationToken.getPrincipal(); // 获取token
// 根据accessToken, 查询用户信息
SysUserTokenEntity byToken = shiroService.getByToken(accessToken);
if (byToken == null || byToken.getExpireTime().getTime() < System.currentTimeMillis()) {
throw new IncorrectCredentialsException("token失效, 请重新登录");
}
// 查询用户信息
SysUserEntity user = shiroService.queryUser(byToken.getUserId());
if (user.getStatus() == 0) {
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
// 存储的是主体(Subject)的身份认证信息 http://shiro.apache.org/static/1.3.2/apidocs/org/apache/shiro/authc/SimpleAuthenticationInfo.html
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, accessToken, getName());
return simpleAuthenticationInfo;
}
}
上述方法中 SimpleAuthorizationInfo 这个类就是讲认证成功后的角色和权限作为内容属性进行存储
public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //获取角色字符串信息
Collection<String> getStringPermissions(); //获取权限字符串信息
Collection<Permission> getObjectPermissions(); //获取 Permission 对象信息
}
AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)
SimpleAuthenticationInfo 将认证成功的主体和身份凭证存入共shiro框架的调用和使用这个主体(对象)和身份凭证(token)比如可以做单点登录或者过期之后再次进入登录或者登出的操作
第二步:我们要配置Shiro的配置类在springmvc框架总通常我们是使用xml进行配置,在springBoot中呢我们使用配置类来配置Shiro框架
在理解这一块的时候我们先上两个图,通过图来理解这一块
从上图看到,我们在定义安全管理器SecurityManager 是需要从Realm中获取用户进行比较来确定用户的身份是否合法,也就是说,SecurityManager 是在Subject进行登录进入到SecurityManager ,然后SecurityManager 在通过Realm域获取用户角色权限来去验证Subject是否合法,我们再看个图
看看SecurityManager 的运行原理
package com.hwrest.jeadmin.config;
import com.hwrest.jeadmin.modules.oauth2.OAuth2Filter;
import com.hwrest.jeadmin.modules.oauth2.OAuth2Realm;
import org.apache.commons.lang.ObjectUtils;
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;
/**
* @author: queiter.ex
* @title: ShiroConfig
* @projectName je_admin
* @description: Shiro配置
* @date 2019/12/2517:08
*/
@Configuration // 代表这个是一个配置类
public class ShiroConfig {
/**
* 配置安全管理器 注入自定义Realm SecurityManager 要用apache shiro中 安全管理器
* @param oAuth2Realm
* @return
*/
@Bean("securityManager") // 注册的bean名称 securityManager
public SecurityManager securityManager(OAuth2Realm oAuth2Realm){
// 主要定义了登录的操作创建Subject的一个方法 http://shiro.apache.org/static/1.3.0/apidocs/org/apache/shiro/web/mgt/DefaultWebSecurityManager.html
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 登录的时候使用什么样的身份验证来验证subject的用于认证
defaultWebSecurityManager.setRealm(oAuth2Realm);
defaultWebSecurityManager.setRememberMeManager(null);
return defaultWebSecurityManager;
}
/**
* 配置过滤规则,因为在登录的时候我们需要验证那些可以让它不登录就可以进行访问url
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
// 创建ShiroFilterFactoryBean对象 http://shiro.apache.org/static/1.2.1/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置 安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// oauth 过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", new OAuth2Filter());
// 过滤 oauth2认证的路径
shiroFilterFactoryBean.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("/aaa.txt", "anon");
filterMap.put("/**", "oauth2");
// urls设置明白
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean("lifecycleBeanPostProcessor") // 管理shiro的生命周期
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// 开启注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
自定义OAuth2Filter
package com.hwrest.jeadmin.modules.oauth2;
import com.hwrest.jeadmin.common.utils.HttpContextUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author quiter.ex
* @title: OAuth2Filter
* @projectName je_admin
* @description: oauth2过滤器
* @date 2019/12/2517:41
*/
// http://shiro.apache.org/static/1.2.1/apidocs/org/apache/shiro/web/filter/authc/AuthenticatingFilter.html
// 对所的请求进行认证的尝试 自定义这个过滤规则
public class OAuth2Filter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取 token 获取一下这个token一般过滤的路径也会携带token进入页面的
String token = getRequestToken((HttpServletRequest) servletRequest);
if (StringUtils.isBlank(token)) {
return null;
}
// 收集的token传给OAuth2Token 然后交给Shiro这个框架
return new OAuth2Token(token);
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取请求token, 如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) servletRequest);
if (StringUtils.isBlank(token)) {
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
// String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
//
// httpResponse.getWriter().print(json);
return false;
}
return executeLogin(servletRequest, servletResponse);
}
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
return false;
}
// 登录失败返回的请求头
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
// try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
// R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
//
// String json = new Gson().toJson(r);
// httpResponse.getWriter().print(json);
// } catch (IOException e2) {
//
// }
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;
}
}
自定义 OAuth2Token
package com.hwrest.jeadmin.modules.oauth2;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author liuhongwei.ex
* @title: OAuth2Token
* @projectName je_admin
* @description:
* @date 2019/12/2517:53
*/
//http://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/authc/AuthenticationToken.html
//用于收集用户提交的身份(如用户名)及凭据(如密码) 收到这些信息交给Shiro框架
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;
}
}
ShiroUtils
/**
* @author liuhongwei.ex
* @title: ShiroUtils
* @projectName je_admin
* @description: 有关Shiro的工具类
* @date 2019/12/2613:47
*/
public class ShiroUtils {
// 获取 Shiro管理的Session
public static Session getSession(){
return SecurityUtils.getSubject().getSession();
}
public static Subject getSubject(){
return SecurityUtils.getSubject();
}
public static SysUserEntity getUserEntity() {
return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
}
public static Long getUserId() {
return getUserEntity().getUserId();
}
// 创建session缓存
public static void setSessionAttribute(Object key, Object value) {
getSession().setAttribute(key, value);
}
public static Object getSessionAttribute(Object key) {
return getSession().getAttribute(key);
}
public static boolean isLogin() {
return SecurityUtils.getSubject().getPrincipal() != null;
}
}