Apache Shiro是一个安全开源框架,可用于处理认证、授权、session管理和加解密。
Authentication(认证):用户身份识别,通常被称为用户“登录”。
Authorization(授权):访问控制,比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理,甚至在非web或EJB应用程序。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
Subject:代表当前用户,可以是一个人,也可以是第三方服务。
SecurityManager:Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。对于 Web 应用一般使用DefaultWebSecurityManager。
Realms:Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
org.apache.shiro
shiro-spring-boot-web-starter
1.7.1
继承AuthorizingRealm,实现其中的两个方法。
doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。
doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。
@Component
@RequiredArgsConstructor
@Slf4j
public class ShiroAuthorizeRealm extends AuthorizingRealm {
// tokenUtils 为自定义token生产和校验工具类
private final TokenUtils tokenUtils;
/**
* @description: 授权校验
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizeUser user = (SimpleAuthorizeUser) getAvailablePrincipal(principalCollection);
Set roles = user.getRoles();
Set perms = user.getPermissions();
// 将用户用户角色和权限赋值Shiro对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(perms);
return info;
}
/**
* @description: 认证校验
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
if (token == null) {
throw new UnauthorizedException("token非法无效");
}
// 校验token有效性
SimpleAuthorizationUser user = tokenUtils.getAuthorizationUser(token);
if (user == null) {
throw new UnauthorizedException("token已过期");
}
// 刷新token,(实现: 用户在线操作不掉线功能)
tokenUtils.refreshToken(token);
//查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方 SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
return new SimpleAuthenticationInfo(user, token, getName());
}
}
注意:
这里为了方便,自定义TokenUtils,用于生成和获取token;自定义SimpleAuthorizeUser,用于封装登录用户详情和权限信息,方便校验。
public class ShiroAuthorizeFilter extends AuthenticatingFilter {
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String token = getToken(request);
if (token == null) {
throw new UnauthorizedException("token非法无效");
}
return new AuthorizeToken(token);
}
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// OPTIONS方法直接返回true
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
//token不存在則返回失敗
String token = getToken(request);
if (token == null) {
return false;
}
try {
return executeLogin(request, response);
} catch (Exception e) {
throw new UnauthorizedException("Token失效,请重新登录", e);
}
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", "true");
httpResponse.setCharacterEncoding("UTF-8");
String json = JSON.toJSONString(R.unauthorized("token非法无效,请重新登录"));
httpResponse.getWriter().print(json);
return false;
}
/**
* @description: 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
private String getToken(ServletRequest request) {
//获取请求token
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(AuthConstants.HEADER);
//如果header中不存在token,则从参数中获取token
if (StringUtils.isEmpty(token)) {
token = httpServletRequest.getParameter(AuthConstants.HEADER);
}
return token;
}
}
public class AuthorizeToken extends UsernamePasswordToken {
private String token;
public AuthorizeToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
注意:
由于Shiro内部对Token进行UsernamePasswordToken.class类型判断,所以自定义Token要么继承UsernamePasswordToken,要么重写Realm的supports方法。
@Configuration
public class ShiroAutoConfiguration extends ShiroWebFilterConfiguration {
@Bean
public Realm realm(TokenUtils tokenUtils) {
return new ShiroAuthorizeRealm(tokenUtils);
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Override
protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
//采用父类的默认方法生成shiroFilterFactoryBean
ShiroFilterFactoryBean shiroFilterFactoryBean = super.shiroFilterFactoryBean();
//获取shiroFilterFactoryBean里的Filters集合
Map filters = shiroFilterFactoryBean.getFilters();
//put进一个自己编写的过滤器,并命名,上面会引用到
filters.put("auth", new ShiroAuthorizeFilter());
shiroFilterFactoryBean.setFilters(filters);
return shiroFilterFactoryBean;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
//-- 可以匿名访问的url
//登录接口排除
chainDefinition.addPathDefinition("/login", "anon");
//登出接口排除
chainDefinition.addPathDefinition("/logout", "anon");
//性能监控 TODO 存在安全漏洞
chainDefinition.addPathDefinition("/actuator/**", "anon");
//swagger
chainDefinition.addPathDefinition("/", "anon");
chainDefinition.addPathDefinition("/doc.html", "anon");
chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
chainDefinition.addPathDefinition("/swagger**/**", "anon");
chainDefinition.addPathDefinition("/webjars/**", "anon");
chainDefinition.addPathDefinition("/v2/**", "anon");
//-- 其余资源都需要认证
chainDefinition.addPathDefinition("/**", "auth");
return chainDefinition;
}
/**
* 下面的代码是添加注解支持
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/**
* 解决重复代理问题 github#994
* 添加前缀判断 不匹配 任何Advisor
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
return defaultAdvisorAutoProxyCreator;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
声明Bean securityManager,注入自定义Realm
继承ShiroWebFilterConfiguration,重写shiroFilterFactoryBean方法,并注入自定义filter
声明Bean shiroFilterChainDefinition,实现全局URL访问控制
添加注解Processor支持
/**
* @description: 权限认证全局异常处理,
* 由于common-core中定义了全局异常处理,所以@Order设置为1,优先执行
*/
@Order(1)
@RestControllerAdvice
@Slf4j
public class AuthorizeGlobalExceptionHandler {
@ExceptionHandler({AuthorizationException.class, UnauthorizedException.class})
public R> handleException(AuthorizationException e) {
log.error("权限异常处理:" + e.getMessage(), e);
String message = "您的权限有误:" + e.getMessage();
R result = R.unauthorized(message);
return result;
}
}
用户登录时生成token信息,设置过期时间,使用Redis存储,这步在登录时实现;
客户端调用接口时将token作为参数传给服务端,服务端根据token信息认证用户;
Shiro框架通过自定义Filter过滤器捕获并进行认证和权限校验;
校验成功处理后续请求并返回结果给客户端,失败则抛出异常并返回
详细流程请查看下图
请求token不存在或失效
2. 无权限
shiro.apache.org
一看就懂!Springboot +Shiro +VUE 前后端分离式权限管理系统
Springboot集成Shiro实现身份认证和权限控制